Merge branch 'master' into vtv-patch
This commit is contained in:
commit
e0f776b4b3
|
@ -4,279 +4,118 @@ on:
|
|||
schedule:
|
||||
- cron: '0 0,12 * * *'
|
||||
jobs:
|
||||
create-branch:
|
||||
setup:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
branch_name: ${{ steps.set-branch-name.outputs.branch_name }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
- name: Set Branch Name
|
||||
id: set-branch-name
|
||||
run: echo "::set-output name=branch_name::$(date +'bot/auto-update-%Y%m%d%H%M00')"
|
||||
- name: Create Branch
|
||||
uses: peterjgrainger/action-create-branch@v2.0.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
branch: ${{ steps.set-branch-name.outputs.branch_name }}
|
||||
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: ${{ needs.create-branch.outputs.branch_name }}
|
||||
- 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
|
||||
format:
|
||||
- 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/channels.db
|
||||
outputs:
|
||||
matrix: ${{ steps.create-matrix.outputs.matrix }}
|
||||
load:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [create-matrix, create-branch]
|
||||
needs: setup
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{fromJSON(needs.create-matrix.outputs.matrix)}}
|
||||
matrix: ${{ fromJson(needs.setup.outputs.matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
ref: ${{ needs.create-branch.outputs.branch_name }}
|
||||
- name: Setup FFmpeg
|
||||
uses: FedericoCarboni/setup-ffmpeg@v1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
name: database
|
||||
path: scripts
|
||||
- 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: Format Playlists
|
||||
run: node scripts/format.js --country=${{ matrix.country }} --debug
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
- run: npm install
|
||||
- run: node scripts/commands/check-streams.js --cluster-id=${{ matrix.cluster_id }}
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: channels
|
||||
path: channels/${{ matrix.country }}.m3u
|
||||
commit-changes:
|
||||
name: logs
|
||||
path: scripts/logs
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [format, create-branch]
|
||||
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]'
|
||||
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: ${{ needs.create-branch.outputs.branch_name }}
|
||||
- name: Download Artifacts
|
||||
uses: actions/download-artifact@v2
|
||||
name: database
|
||||
path: scripts
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: channels
|
||||
path: channels
|
||||
- name: Commit Changes
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
name: logs
|
||||
path: scripts/logs
|
||||
- run: npm install
|
||||
- run: node scripts/commands/update-database.js
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
commit_message: '[Bot] Format playlists'
|
||||
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: ${{ needs.create-branch.outputs.branch_name }}
|
||||
file_pattern: channels/*
|
||||
remove-duplicates:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [commit-changes, create-branch]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
name: database
|
||||
path: scripts/channels.db
|
||||
- run: node scripts/commands/update-playlists.js
|
||||
- run: |
|
||||
git add channels/*
|
||||
git commit -m "[Bot] Update playlists"
|
||||
- run: node scripts/commands/generate-playlists.js
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
ref: ${{ needs.create-branch.outputs.branch_name }}
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
cache: 'npm'
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
- name: Remove Duplicates
|
||||
run: node scripts/remove-duplicates.js
|
||||
- name: Commit Changes
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
commit_message: '[Bot] Remove duplicates'
|
||||
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: ${{ needs.create-branch.outputs.branch_name }}
|
||||
file_pattern: channels/*
|
||||
sort:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [remove-duplicates, create-branch]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ needs.create-branch.outputs.branch_name }}
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
cache: 'npm'
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
- name: Sort Channels
|
||||
run: node scripts/sort.js
|
||||
- name: Commit Changes
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
commit_message: '[Bot] Sort channels'
|
||||
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: ${{ needs.create-branch.outputs.branch_name }}
|
||||
file_pattern: channels/*
|
||||
filter:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [sort, create-branch]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ needs.create-branch.outputs.branch_name }}
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
cache: 'npm'
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
- name: Filter Playlists
|
||||
run: node scripts/filter.js
|
||||
- name: Commit Changes
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
commit_message: '[Bot] Filter channels'
|
||||
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: ${{ needs.create-branch.outputs.branch_name }}
|
||||
file_pattern: channels/*
|
||||
generate:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [filter, create-branch]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ needs.create-branch.outputs.branch_name }}
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
cache: 'npm'
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
- name: Generate Playlists
|
||||
run: node scripts/generate.js
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: gh-pages
|
||||
path: .gh-pages/
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [generate, create-branch]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ needs.create-branch.outputs.branch_name }}
|
||||
- name: Download Artifacts
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: gh-pages
|
||||
path: .gh-pages
|
||||
- name: Generate Token
|
||||
uses: tibdex/github-app-token@v1
|
||||
id: generate-token
|
||||
name: logs
|
||||
path: scripts/logs
|
||||
- run: node scripts/commands/update-readme.js
|
||||
- run: |
|
||||
git add README.md
|
||||
git commit -m "[Bot] Update README.md"
|
||||
- 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
|
||||
- uses: JamesIves/github-pages-deploy-action@4.1.1
|
||||
if: ${{ github.ref == 'refs/heads/master' }}
|
||||
uses: JamesIves/github-pages-deploy-action@4.1.1
|
||||
with:
|
||||
branch: gh-pages
|
||||
folder: .gh-pages
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
git-config-name: iptv-bot
|
||||
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'
|
||||
update-readme:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [generate, create-branch]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
commit-message: '[Bot] Generate playlists'
|
||||
- uses: repo-sync/pull-request@v2
|
||||
if: ${{ github.ref == 'refs/heads/master' }}
|
||||
id: pull-request
|
||||
with:
|
||||
ref: ${{ needs.create-branch.outputs.branch_name }}
|
||||
- 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: node scripts/update-readme.js
|
||||
- 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: ${{ needs.create-branch.outputs.branch_name }}
|
||||
file_pattern: README.md
|
||||
pull-request:
|
||||
if: ${{ github.ref == 'refs/heads/master' }}
|
||||
needs: [update-readme, create-branch]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ needs.create-branch.outputs.branch_name }}
|
||||
- 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: ${{ needs.create-branch.outputs.branch_name }}
|
||||
github_token: ${{ steps.create-app-token.outputs.token }}
|
||||
source_branch: ${{ steps.create-branch-name.outputs.branch_name }}
|
||||
destination_branch: 'master'
|
||||
pr_title: '[Bot] Update playlists'
|
||||
pr_title: '[Bot] Daily playlists update'
|
||||
pr_body: |
|
||||
This pull request is created by [auto-update][1] workflow.
|
||||
|
||||
[1]: https://github.com/iptv-org/iptv/actions/runs/${{ github.run_id }}
|
||||
github_token: ${{ steps.generate-token.outputs.token }}
|
||||
- name: Merge Pull Request
|
||||
uses: juliangruber/merge-pull-request-action@v1
|
||||
- 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
|
||||
|
|
|
@ -4,12 +4,15 @@ on:
|
|||
pull_request:
|
||||
types: [opened, synchronize, reopened, edited]
|
||||
jobs:
|
||||
lint:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
- name: Check Playlists
|
||||
run: npm run lint
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
if: ${{ !env.ACT }}
|
||||
with:
|
||||
node-version: '14'
|
||||
cache: 'npm'
|
||||
- run: npm install
|
||||
- run: npm run lint
|
||||
- run: npm run validate
|
||||
|
|
|
@ -2,65 +2,38 @@ name: cleanup
|
|||
on:
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
create-branch:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
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/cleanup'
|
||||
remove-broken-links:
|
||||
runs-on: ubuntu-latest
|
||||
needs: create-branch
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: bot/cleanup
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
- name: Remove Broken Links
|
||||
run: node scripts/remove-broken-links.js
|
||||
- name: Commit Changes
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
commit_message: '[Bot] Remove broken links'
|
||||
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/cleanup
|
||||
file_pattern: channels/*
|
||||
pull-request:
|
||||
if: ${{ github.ref == 'refs/heads/master' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: remove-broken-links
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: bot/cleanup
|
||||
- name: Generate Token
|
||||
uses: tibdex/github-app-token@v1
|
||||
id: generate-token
|
||||
- uses: actions/checkout@v2
|
||||
- run: echo "::set-output name=branch_name::$(date +'bot/cleanup-%s')"
|
||||
id: create-branch-name
|
||||
- run: |
|
||||
git config user.name 'iptv-bot[bot]'
|
||||
git config user.email '84861620+iptv-bot[bot]@users.noreply.github.com'
|
||||
- run: git checkout -b ${{ steps.create-branch-name.outputs.branch_name }}
|
||||
- run: npm install
|
||||
- run: node scripts/commands/create-database.js
|
||||
- run: node scripts/commands/cleanup-database.js
|
||||
- run: node scripts/commands/update-playlists.js
|
||||
- run: |
|
||||
git add channels/*
|
||||
git commit -m "[Bot] Update playlists"
|
||||
- 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: Create Pull Request
|
||||
id: pr
|
||||
uses: repo-sync/pull-request@v2
|
||||
- uses: repo-sync/pull-request@v2
|
||||
if: ${{ github.ref == 'refs/heads/master' }}
|
||||
id: pull-request
|
||||
with:
|
||||
source_branch: 'bot/cleanup'
|
||||
github_token: ${{ steps.create-app-token.outputs.token }}
|
||||
source_branch: ${{ steps.create-branch-name.outputs.branch_name }}
|
||||
destination_branch: 'master'
|
||||
pr_title: '[Bot] Cleaning playlists'
|
||||
pr_title: '[Bot] Remove broken links'
|
||||
pr_body: |
|
||||
This pull request is created by [cleanup][1] workflow.
|
||||
|
||||
[1]: https://github.com/iptv-org/iptv/actions/runs/${{ github.run_id }}
|
||||
pr_draft: true
|
||||
github_token: ${{ steps.generate-token.outputs.token }}
|
||||
|
|
|
@ -1 +1,6 @@
|
|||
node_modules
|
||||
node_modules
|
||||
database
|
||||
.artifacts
|
||||
.secrets
|
||||
.actrc
|
||||
.DS_Store
|
|
@ -1,3 +1,4 @@
|
|||
_categories.md
|
||||
_countries.md
|
||||
_languages.md
|
||||
_languages.md
|
||||
_regions.md
|
|
@ -1,4 +1,4 @@
|
|||
## Supported Region Codes
|
||||
## Supported Regions
|
||||
|
||||
| Code | Description |
|
||||
| ------------------------------------------------------------------------ | ---------------------------------- |
|
||||
|
@ -10,11 +10,11 @@
|
|||
| [CARIB](https://en.wikipedia.org/wiki/Caribbean) | Caribbean |
|
||||
| [CAS](https://en.wikipedia.org/wiki/Central_Asia) | Central Asia |
|
||||
| [CIS](https://en.wikipedia.org/wiki/Commonwealth_of_Independent_States) | Commonwealth of Independent States |
|
||||
| [EMEA](https://en.wikipedia.org/wiki/Europe,_the_Middle_East_and_Africa) | Europe, Middle East and Africa |
|
||||
| [EMEA](https://en.wikipedia.org/wiki/Europe,_the_Middle_East_and_Africa) | Europe, the Middle East and Africa |
|
||||
| [EUR](https://en.wikipedia.org/wiki/Europe) | Europe |
|
||||
| [HISPAM](https://en.wikipedia.org/wiki/Hispanic_America) | Hispanic America |
|
||||
| [LATAM](https://en.wikipedia.org/wiki/Latin_America) | Latin America |
|
||||
| [MAGHRIB](https://en.wikipedia.org/wiki/Maghreb) | Maghrib |
|
||||
| [MAGHREB](https://en.wikipedia.org/wiki/Maghreb) | Maghreb |
|
||||
| [MENA](https://en.wikipedia.org/wiki/MENA) | Middle East and North Africa |
|
||||
| [MIDEAST](https://en.wikipedia.org/wiki/Middle_East) | Middle East |
|
||||
| [NORAM](https://en.wikipedia.org/wiki/North_America) | North America |
|
|
@ -0,0 +1,8 @@
|
|||
## Supported Statuses
|
||||
|
||||
| Label | Description |
|
||||
| ----------- | ------------------------------------------------- |
|
||||
| Geo-blocked | Channel is only available in selected countries. |
|
||||
| Not 24/7 | Broadcast is not available 24 hours a day. |
|
||||
| Timeout | Server does not respond for more than 60 seconds. |
|
||||
| Offline | The broadcast does not work for any other reason. |
|
|
@ -14,9 +14,10 @@ To watch IPTV you just need to paste this link `https://iptv-org.github.io/iptv/
|
|||
|
||||
Also you can instead use one of these playlists:
|
||||
|
||||
- `https://iptv-org.github.io/iptv/index.country.m3u` (grouped by country)
|
||||
- `https://iptv-org.github.io/iptv/index.category.m3u` (grouped by category)
|
||||
- `https://iptv-org.github.io/iptv/index.language.m3u` (grouped by language)
|
||||
- `https://iptv-org.github.io/iptv/index.country.m3u` (grouped by country)
|
||||
- `https://iptv-org.github.io/iptv/index.region.m3u` (grouped by region)
|
||||
- `https://iptv-org.github.io/iptv/index.nsfw.m3u` (includes adult channels)
|
||||
|
||||
Or select one of the playlists from the list below.
|
||||
|
@ -43,6 +44,17 @@ Or select one of the playlists from the list below.
|
|||
|
||||
</details>
|
||||
|
||||
### Playlists by region
|
||||
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
<br>
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
#include "./.readme/_regions.md"
|
||||
|
||||
</details>
|
||||
|
||||
### Playlists by country
|
||||
|
||||
<details>
|
||||
|
@ -77,11 +89,10 @@ If successful, you should get the following response:
|
|||
"name": "CNN",
|
||||
"logo": "https://i.imgur.com/ilZJT5s.png",
|
||||
"url": "http://ott-cdn.ucom.am/s27/index.m3u8",
|
||||
"category": "News",
|
||||
"languages": [
|
||||
"categories": [
|
||||
{
|
||||
"code": "eng",
|
||||
"name": "English"
|
||||
"name": "News",
|
||||
"slug": "news"
|
||||
}
|
||||
],
|
||||
"countries": [
|
||||
|
@ -94,6 +105,12 @@ If successful, you should get the following response:
|
|||
"name": "Canada"
|
||||
}
|
||||
],
|
||||
"languages": [
|
||||
{
|
||||
"code": "eng",
|
||||
"name": "English"
|
||||
}
|
||||
],
|
||||
"tvg": {
|
||||
"id": "cnn.us",
|
||||
"name": "CNN",
|
||||
|
|
|
@ -11,15 +11,15 @@ Before submitting your contribution, please make sure to take a moment and read
|
|||
|
||||
### Request a Channel
|
||||
|
||||
To request a channel, create an [issue](https://github.com/iptv-org/iptv/issues/new?assignees=&labels=channel+request&template=------channel-request.md&title=Add%3A+xxx) and complete all details requested. (**IMPORTANT:** the issue should contain a request for only one channel, otherwise it will be closed immediately). Understand that our community of volunteers will try to help you, but if a public link cannot be found, there is little we can do.
|
||||
To request a channel, create an [issue](https://github.com/iptv-org/iptv/issues/new?assignees=&labels=channel+request&template=------channel-request.yml&title=Add%3A+) and complete all details requested. Understand that our community of volunteers will try to help you, but if a public link cannot be found, there is little we can do. (**IMPORTANT:** the issue should contain a request for only one channel, otherwise it will be closed immediately)
|
||||
|
||||
### Report a Broken Stream
|
||||
|
||||
To report a broadcast that is not working, create an [issue](https://github.com/iptv-org/iptv/issues/new?assignees=&labels=broken+stream&template=----broken-stream.md&title=Fix%3A+xxx) with a description of the channel (**IMPORTANT:** an issue should contain a report for only one channel, otherwise it will be closed immediately).
|
||||
To report a broadcast that is not working, create an [issue](https://github.com/iptv-org/iptv/issues/new?assignees=&labels=broken+stream&template=-----broken-stream.yml&title=Replace%3A+) with a description of the channel. (**IMPORTANT:** an issue should contain a report for only one channel, otherwise it will be closed immediately)
|
||||
|
||||
### Request Channel Removal
|
||||
|
||||
Publish your DMCA notice somewhere and send us a link to it through this [form](https://github.com/iptv-org/iptv/issues/new?assignees=&labels=DMCA&template=--remove-channel.md&title=Remove%3A+xxx).
|
||||
Publish your DMCA notice somewhere and send us a link to it through this [form](https://github.com/iptv-org/iptv/issues/new?assignees=&labels=DMCA&template=--remove-channel.yml&title=Remove%3A+).
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
|
@ -29,12 +29,13 @@ If you would like to replace a broken stream or add a new one, please do the fol
|
|||
|
||||
- make sure that the link you want to add works by using a program like [VLC media player](https://www.videolan.org/vlc/index.html)
|
||||
- check if the channel is working outside your country by using a VPN or use a service like [streamtest.in](https://streamtest.in/)
|
||||
- if the broadcast is not available outside of a certain country, add the label `[Geo-blocked]` to the end of the channel name
|
||||
- find out from which country the channel is being broadcasted. This information can usually be found on [lyngsat.com](https://www.lyngsat.com/search.html) or [wikipedia.org](https://www.wikipedia.org/). If you are unable to determine which country the channel belongs to, add the channel onto the `channels/unsorted.m3u` playlist
|
||||
- find the corresponding [ISO_3166-2 code](https://en.wikipedia.org/wiki/ISO_3166-2) for the country
|
||||
- open the `/channels` folder and find the file that has the same code in its name and open it
|
||||
- if broken, find the broken link in this file and replace it with working one
|
||||
- if new, at the very end of this file add a link to the channel with a description
|
||||
- if the broadcast is not available outside of a certain country, add the label `[Geo-blocked]` to the end of the channel name and list these countries in the `tvg-country` attribute
|
||||
- if the broadcast is not available 24 hours a day, add the label `[Not 24/7]`
|
||||
- commit all changes and send a pull request
|
||||
|
||||
### Add a Category to a Channel
|
||||
|
@ -77,7 +78,7 @@ If a channel is broadcasted in several countries at once, you can specify them a
|
|||
http://example.com/cnn.m3u8
|
||||
```
|
||||
|
||||
If a channel is broadcast for an entire region, you can use one of the [supported region code](https://github.com/iptv-org/iptv/blob/master/.readme/supported-region-codes.md) to avoid listing all countries. In this case the channel will be added to the playlists of all countries from that region.
|
||||
If a channel is broadcast for an entire region, you can use one of the [supported region code](https://github.com/iptv-org/iptv/blob/master/.readme/supported-regions.md) to avoid listing all countries. In this case the channel will be added to the playlists of all countries from that region.
|
||||
|
||||
In case the channel is broadcast worldwide you can use the code `INT`:
|
||||
|
||||
|
@ -117,31 +118,31 @@ For a channel to be approved, its description must follow this template:
|
|||
STREAM_URL
|
||||
```
|
||||
|
||||
| Attribute | Description |
|
||||
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `EPG_ID` | Channel ID that is used to load EPG. Must match `id` from the EPG file. (optional) |
|
||||
| `COUNTRY` | The code of the country in which the channel is broadcast. The code of the country must conform to the standard [ISO_3166-2](https://en.wikipedia.org/wiki/ISO_3166-2). If the channel is broadcast in several countries you can list them separated by a semicolon. You can also use one of these [region codes](#supported-region-codes). (optional) |
|
||||
| `LANGUAGE` | Channel language. The name of the language must conform to the standard [ISO 639-3](https://iso639-3.sil.org/code_tables/639/data?title=&field_iso639_cd_st_mmbrshp_639_1_tid=94671&name_3=&field_iso639_element_scope_tid=All&field_iso639_language_type_tid=51&items_per_page=500). If the channel is broadcast in several languages you can list them separated by a semicolon. (optional) |
|
||||
| `LOGO_URL` | The logo of the channel that will be displayed if the player supports it. Supports files in png, jpeg and gif format. (optional) |
|
||||
| `CATEGORY` | The category to which the channel belongs. The list of currently supported categories can be found [here](https://github.com/iptv-org/iptv#playlists-by-category). (optional) |
|
||||
| `FULL_NAME` | Full name of the channel. It is recommended to use the name listed on [lyngsat](https://www.lyngsat.com/search.html) or [wikipedia](https://www.wikipedia.org/) if possible. May contain any characters except round and square brackets. |
|
||||
| `STREAM_TIME_SHIFT` | Must be specified if the channel is broadcast with a shift in time relative to the main stream. Should only contain a number and a sign. (optional) |
|
||||
| `ALTERNATIVE_NAME` | Can be used to specify a short name or name in another language. May contain any characters except round and square brackets. (optional) |
|
||||
| `STREAM_RESOLUTION` | The maximum height of the frame with a "p" at the end. In case of VLC Player this information can be found in `Window > Media Information... > Codec Details`. (optional) |
|
||||
| `STREAM_STATUS` | Specified if the broadcast for some reason is interrupted or does not work in a particular application. May contain any characters except round and square brackets. (optional) |
|
||||
| `STREAM_URL` | Channel broadcast URL. |
|
||||
| Attribute | Description |
|
||||
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `EPG_ID` | Channel ID that is used to load EPG. The same ID is used in [iptv-org/epg](https://iptv-org.github.io/epg/index.html) to search for the corresponding program. (optional) |
|
||||
| `COUNTRY` | The code of the country in which the channel is broadcast. The code of the country must conform to the standard [ISO_3166-2](https://en.wikipedia.org/wiki/ISO_3166-2). If the channel is broadcast in several countries you can list them separated by a semicolon. You can also use one of these [region codes](https://github.com/iptv-org/iptv/blob/master/.readme/supported-regions.md). (optional) |
|
||||
| `LANGUAGE` | Channel language. The name of the language must conform to the standard [ISO 639-3](https://iso639-3.sil.org/code_tables/639/data?title=&field_iso639_cd_st_mmbrshp_639_1_tid=94671&name_3=&field_iso639_element_scope_tid=All&field_iso639_language_type_tid=51&items_per_page=500). If the channel is broadcast in several languages you can list them separated by a semicolon. (optional) |
|
||||
| `LOGO_URL` | The logo of the channel that will be displayed if the player supports it. Supports files in png, jpeg and gif format. (optional) |
|
||||
| `CATEGORY` | The category to which the channel belongs. The list of currently supported categories can be found [here](https://github.com/iptv-org/iptv/blob/master/.readme/supported-categories.md). (optional) |
|
||||
| `FULL_NAME` | Full name of the channel. It is recommended to use the name listed on [lyngsat](https://www.lyngsat.com/search.html) or [wikipedia](https://www.wikipedia.org/) if possible. May contain any characters except round and square brackets. |
|
||||
| `STREAM_TIME_SHIFT` | Must be specified if the channel is broadcast with a shift in time relative to the main stream. Should only contain a number and a sign. (optional) |
|
||||
| `ALTERNATIVE_NAME` | Can be used to specify a short name or name in another language. May contain any characters except round and square brackets. (optional) |
|
||||
| `STREAM_RESOLUTION` | The maximum height of the frame with a "p" at the end. In case of VLC Player this information can be found in `Window > Media Information... > Codec Details`. (optional) |
|
||||
| `STREAM_STATUS` | Specified if the broadcast for some reason is interrupted or does not work in a particular application. The list of currently supported statuses can be found [here](https://github.com/iptv-org/iptv/blob/master/.readme/supported-statuses.md). (optional) |
|
||||
| `STREAM_URL` | Channel broadcast URL. |
|
||||
|
||||
Example:
|
||||
|
||||
```xml
|
||||
#EXTINF:-1 tvg-id="example.ua" tvg-country="UA" tvg-language="Ukrainian;Russian" tvg-logo="https://i.imgur.com/bu12f89.png" group-title="Kids",Example TV +3 (Пример ТВ) (720p) [not 24/7]
|
||||
#EXTINF:-1 tvg-id="ExampleTVPlus3.ua" tvg-country="UA" tvg-language="Ukrainian;Russian" tvg-logo="https://i.imgur.com/bu12f89.png" group-title="Kids",Example TV +3 (Пример ТВ) (720p) [not 24/7]
|
||||
https://example.com/playlist.m3u8
|
||||
```
|
||||
|
||||
Also, if necessary, you can specify custom HTTP User-Agent or Referrer via the `#EXTVLCOPT` tag:
|
||||
|
||||
```xml
|
||||
#EXTINF:-1 tvg-id="exampletv.us" tvg-country="US" tvg-language="English" tvg-logo="http://example.com/channel-logo.png" group-title="News",Example TV
|
||||
#EXTINF:-1 tvg-id="ExampleTV.us" tvg-country="US" tvg-language="English" tvg-logo="http://example.com/channel-logo.png" group-title="News",Example TV
|
||||
#EXTVLCOPT:http-referrer=http://example.com/
|
||||
#EXTVLCOPT:http-user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64)
|
||||
http://example.com/stream.m3u8
|
||||
|
@ -152,29 +153,22 @@ http://example.com/stream.m3u8
|
|||
- `.github/`
|
||||
- `ISSUE_TEMPLATE/`: issue templates for this repository.
|
||||
- `workflows/`
|
||||
- `auto-update.yml`: GitHub workflow that automatically updates all playlists every day.
|
||||
- `check.yml`: GitHub workflow that automatically checks every pull request for syntax errors.
|
||||
- `cleanup.yml`: GitHub workflow that automatically removes broken links every week.
|
||||
- `auto-update.yml`: GitHub workflow that launches daily playlist updates (at 0:00 and 12:00 UTC).
|
||||
- `check.yml`: GitHub workflow that checks every pull request for syntax errors.
|
||||
- `cleanup.yml`: GitHub workflow that removes broken links by request.
|
||||
- `validate.yml`: GitHub workflow that compares channel names with the blocklist each time a pull request is made.
|
||||
- `CODE_OF_CONDUCT.md`: rules you shouldn't break if you don't want to get banned.
|
||||
- `.readme/`
|
||||
- `config.json`: config for the `markdown-include` package, which is used to compile everything into one `README.md` file.
|
||||
- `preview.png`: image displayed in the `README.md`.
|
||||
- `supported-categories.md`: list of supported categories.
|
||||
- `supported-region-codes.md`: list of supported region codes.
|
||||
- `supported-statuses.md`: list of supported statuses.
|
||||
- `supported-regions.md`: list of supported regions.
|
||||
- `template.md`: template for `README.md`.
|
||||
- `channels/`: contains all channels broken down by the country from which they are broadcast.
|
||||
- ...
|
||||
- `unsorted.m3u`: playlist with channels not yet sorted.
|
||||
- `scripts/`
|
||||
- `data/`: data used in scripts.
|
||||
- `helpers/`: helper scripts.
|
||||
- `create-matrix.js`: used within GitHub workflow to create matrix of files to process.
|
||||
- `filter.js`: used within GitHub workflow to remove blacklisted channels from playlists.
|
||||
- `format.js`: used within GitHub workflow to format channel descriptions.
|
||||
- `generate.js`: used within GitHub workflow to generate all additional playlists.
|
||||
- `remove-broken-links.js`: used in GitHub workflow to remove broken links from the playlist.
|
||||
- `remove-duplicates.js`: used in GitHub workflow to remove duplicates from the playlist.
|
||||
- `sort.js`: used within GitHub workflow to sort channels by name.
|
||||
- `update-readme.js`: used within GitHub workflow to update the `README.md` file.
|
||||
- `scripts/`: contains all the scripts used in GitHub workflows.
|
||||
- `tests/`: contains tests to check the scripts in the folder above.
|
||||
- `CONTRIBUTING.md`: file you are currently reading.
|
||||
- `README.md`: project description generated from the contents of the `.readme/` folder.
|
||||
|
|
File diff suppressed because it is too large
Load Diff
28
package.json
28
package.json
|
@ -1,25 +1,31 @@
|
|||
{
|
||||
"name": "iptv",
|
||||
"scripts": {
|
||||
"lint": "npx m3u-linter -c m3u-linter.json"
|
||||
"validate": "node scripts/commands/validate.js",
|
||||
"lint": "npx m3u-linter -c m3u-linter.json",
|
||||
"test": "jest --runInBand"
|
||||
},
|
||||
"jest": {
|
||||
"testRegex": "tests/(.*?/)?.*test.js$"
|
||||
},
|
||||
"pre-push": [
|
||||
"lint"
|
||||
],
|
||||
"author": "Arhey",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^0.21.4",
|
||||
"chunk": "^0.0.3",
|
||||
"commander": "^7.0.0",
|
||||
"iptv-checker": "^0.21.0",
|
||||
"iptv-playlist-parser": "^0.9.0",
|
||||
"crypto": "^1.0.1",
|
||||
"dayjs": "^1.10.7",
|
||||
"iptv-checker": "^0.22.0",
|
||||
"iptv-playlist-parser": "^0.10.2",
|
||||
"jest": "^27.4.3",
|
||||
"lodash": "^4.17.21",
|
||||
"m3u-linter": "^0.2.2",
|
||||
"markdown-include": "^0.4.3",
|
||||
"natural-orderby": "^2.0.3",
|
||||
"mz": "^2.7.0",
|
||||
"nedb-promises": "^5.0.2",
|
||||
"normalize-url": "^6.1.0",
|
||||
"pre-push": "^0.1.1",
|
||||
"progress": "^2.0.3",
|
||||
"transliteration": "^2.2.0"
|
||||
"transliteration": "^2.2.0",
|
||||
"winston": "^3.3.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
logs/
|
||||
channels.db
|
|
@ -0,0 +1,50 @@
|
|||
const { program } = require('commander')
|
||||
const { db, logger, timer, checker, store, file, parser } = require('../core')
|
||||
|
||||
const options = program
|
||||
.requiredOption('-c, --cluster-id <cluster-id>', 'The ID of cluster to load', parser.parseNumber)
|
||||
.option('-t, --timeout <timeout>', 'Set timeout for each request', parser.parseNumber, 60000)
|
||||
.option('-d, --delay <delay>', 'Set delay for each request', parser.parseNumber, 0)
|
||||
.option('--debug', 'Enable debug mode')
|
||||
.parse(process.argv)
|
||||
.opts()
|
||||
|
||||
const config = {
|
||||
timeout: options.timeout,
|
||||
delay: options.delay,
|
||||
debug: options.debug
|
||||
}
|
||||
|
||||
const LOGS_PATH = process.env.LOGS_PATH || 'scripts/logs'
|
||||
|
||||
async function main() {
|
||||
logger.info('Starting...')
|
||||
logger.info(`Timeout: ${options.timeout}ms`)
|
||||
logger.info(`Delay: ${options.delay}ms`)
|
||||
timer.start()
|
||||
|
||||
const clusterLog = `${LOGS_PATH}/check-streams/cluster_${options.clusterId}.log`
|
||||
logger.info(`Loading cluster: ${options.clusterId}`)
|
||||
logger.info(`Creating '${clusterLog}'...`)
|
||||
await file.create(clusterLog)
|
||||
const items = await db.find({ cluster_id: options.clusterId })
|
||||
const total = items.length
|
||||
logger.info(`Found ${total} links`)
|
||||
|
||||
logger.info('Checking...')
|
||||
const results = {}
|
||||
for (const [i, item] of items.entries()) {
|
||||
const message = `[${i + 1}/${total}] ${item.filepath}: ${item.url}`
|
||||
const result = await checker.check(item, config)
|
||||
if (!result.error) {
|
||||
logger.info(message)
|
||||
} else {
|
||||
logger.info(`${message} (${result.error})`)
|
||||
}
|
||||
await file.append(clusterLog, JSON.stringify(result) + '\n')
|
||||
}
|
||||
|
||||
logger.info(`Done in ${timer.format('HH[h] mm[m] ss[s]')}`)
|
||||
}
|
||||
|
||||
main()
|
|
@ -0,0 +1,24 @@
|
|||
const { db, logger } = require('../core')
|
||||
|
||||
async function main() {
|
||||
logger.info(`Loading database...`)
|
||||
let streams = await db.find({})
|
||||
|
||||
logger.info(`Removing broken links...`)
|
||||
let removed = 0
|
||||
const buffer = []
|
||||
for (const stream of streams) {
|
||||
const duplicate = buffer.find(i => i.id === stream.id)
|
||||
if (duplicate && ['offline', 'timeout'].includes(stream.status.code)) {
|
||||
await db.remove({ _id: stream._id })
|
||||
removed++
|
||||
} else {
|
||||
buffer.push(stream)
|
||||
}
|
||||
}
|
||||
db.compact()
|
||||
|
||||
logger.info(`Removed ${removed} links`)
|
||||
}
|
||||
|
||||
main()
|
|
@ -0,0 +1,104 @@
|
|||
const { db, file, parser, store, logger } = require('../core')
|
||||
const transliteration = require('transliteration')
|
||||
const { program } = require('commander')
|
||||
const _ = require('lodash')
|
||||
|
||||
const options = program
|
||||
.option(
|
||||
'--max-clusters <max-clusters>',
|
||||
'Set maximum number of clusters',
|
||||
parser.parseNumber,
|
||||
200
|
||||
)
|
||||
.option('--input-dir <input-dir>', 'Set path to input directory', 'channels')
|
||||
.parse(process.argv)
|
||||
.opts()
|
||||
|
||||
const links = []
|
||||
|
||||
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 links...`)
|
||||
|
||||
const files = await file.list(`${options.inputDir}/**/*.m3u`)
|
||||
for (const filepath of files) {
|
||||
const items = await parser.parsePlaylist(filepath)
|
||||
for (const item of items) {
|
||||
item.filepath = filepath
|
||||
links.push(item)
|
||||
}
|
||||
}
|
||||
logger.info(`Found ${links.length} links`)
|
||||
}
|
||||
|
||||
async function saveToDatabase() {
|
||||
logger.info('Saving to the database...')
|
||||
|
||||
await db.reset()
|
||||
const chunks = split(_.shuffle(links), options.maxClusters)
|
||||
for (const [i, chunk] of chunks.entries()) {
|
||||
for (const item of chunk) {
|
||||
const stream = store.create()
|
||||
stream.set('name', { title: item.name })
|
||||
stream.set('id', { id: item.tvg.id })
|
||||
stream.set('filepath', { filepath: item.filepath })
|
||||
stream.set('src_country', { filepath: item.filepath })
|
||||
stream.set('tvg_country', { tvg_country: item.tvg.country })
|
||||
stream.set('countries', { tvg_country: item.tvg.country })
|
||||
stream.set('regions', { countries: stream.get('countries') })
|
||||
stream.set('languages', { tvg_language: item.tvg.language })
|
||||
stream.set('categories', { group_title: item.group.title })
|
||||
stream.set('tvg_url', { tvg_url: item.tvg.url })
|
||||
stream.set('guides', { tvg_url: item.tvg.url })
|
||||
stream.set('logo', { logo: item.tvg.logo })
|
||||
stream.set('resolution', { title: item.name })
|
||||
stream.set('status', { title: item.name })
|
||||
stream.set('url', { url: item.url })
|
||||
stream.set('http', { http: item.http })
|
||||
stream.set('is_nsfw', { categories: stream.get('categories') })
|
||||
stream.set('is_broken', { status: stream.get('status') })
|
||||
stream.set('updated', { updated: false })
|
||||
stream.set('cluster_id', { cluster_id: i + 1 })
|
||||
|
||||
if (!stream.get('id')) {
|
||||
const id = generateChannelId(stream.get('name'), stream.get('src_country'))
|
||||
stream.set('id', { id })
|
||||
}
|
||||
|
||||
await db.insert(stream.data())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function generateChannelId(name, src_country) {
|
||||
if (name && src_country) {
|
||||
const slug = transliteration
|
||||
.transliterate(name)
|
||||
.replace(/\+/gi, 'Plus')
|
||||
.replace(/[^a-z\d]+/gi, '')
|
||||
const code = src_country.code.toLowerCase()
|
||||
|
||||
return `${slug}.${code}`
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
const { logger, db } = require('../core')
|
||||
|
||||
async function main() {
|
||||
const docs = await db.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()
|
|
@ -0,0 +1,321 @@
|
|||
const { db, logger, generator, file } = require('../core')
|
||||
const _ = require('lodash')
|
||||
|
||||
let languages = []
|
||||
let countries = []
|
||||
let categories = []
|
||||
let regions = []
|
||||
|
||||
const LOGS_PATH = process.env.LOGS_PATH || 'scripts/logs'
|
||||
const PUBLIC_PATH = process.env.PUBLIC_PATH || '.gh-pages'
|
||||
|
||||
async function main() {
|
||||
await setUp()
|
||||
|
||||
await generateCategories()
|
||||
await generateCountries()
|
||||
await generateLanguages()
|
||||
await generateRegions()
|
||||
await generateIndex()
|
||||
await generateIndexNSFW()
|
||||
await generateIndexCategory()
|
||||
await generateIndexCountry()
|
||||
await generateIndexLanguage()
|
||||
await generateIndexRegion()
|
||||
|
||||
await generateChannelsJson()
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
async function generateCategories() {
|
||||
logger.info(`Generating categories/...`)
|
||||
|
||||
for (const category of categories) {
|
||||
const { count } = await generator.generate(
|
||||
`${PUBLIC_PATH}/categories/${category.slug}.m3u`,
|
||||
{ categories: { $elemMatch: category } },
|
||||
{ saveEmpty: true, includeNSFW: true }
|
||||
)
|
||||
|
||||
await log('categories', {
|
||||
name: category.name,
|
||||
slug: category.slug,
|
||||
count
|
||||
})
|
||||
}
|
||||
|
||||
const { count: otherCount } = await generator.generate(
|
||||
`${PUBLIC_PATH}/categories/other.m3u`,
|
||||
{ categories: { $size: 0 } },
|
||||
{ saveEmpty: true }
|
||||
)
|
||||
|
||||
await log('categories', {
|
||||
name: 'Other',
|
||||
slug: 'other',
|
||||
count: otherCount
|
||||
})
|
||||
}
|
||||
|
||||
async function generateCountries() {
|
||||
logger.info(`Generating countries/...`)
|
||||
|
||||
for (const country of countries) {
|
||||
const { count } = await generator.generate(
|
||||
`${PUBLIC_PATH}/countries/${country.code.toLowerCase()}.m3u`,
|
||||
{
|
||||
countries: { $elemMatch: country }
|
||||
}
|
||||
)
|
||||
|
||||
await log('countries', {
|
||||
name: country.name,
|
||||
code: country.code,
|
||||
count
|
||||
})
|
||||
}
|
||||
|
||||
const { count: intCount } = await generator.generate(`${PUBLIC_PATH}/countries/int.m3u`, {
|
||||
tvg_country: 'INT'
|
||||
})
|
||||
|
||||
await log('countries', {
|
||||
name: 'International',
|
||||
code: 'INT',
|
||||
count: intCount
|
||||
})
|
||||
|
||||
const { count: undefinedCount } = await generator.generate(
|
||||
`${PUBLIC_PATH}/countries/undefined.m3u`,
|
||||
{
|
||||
countries: { $size: 0 }
|
||||
}
|
||||
)
|
||||
|
||||
await log('countries', {
|
||||
name: 'Undefined',
|
||||
code: 'UNDEFINED',
|
||||
count: undefinedCount
|
||||
})
|
||||
}
|
||||
|
||||
async function generateLanguages() {
|
||||
logger.info(`Generating languages/...`)
|
||||
|
||||
for (const language of _.uniqBy(languages, 'code')) {
|
||||
const { count } = await generator.generate(`${PUBLIC_PATH}/languages/${language.code}.m3u`, {
|
||||
languages: { $elemMatch: language }
|
||||
})
|
||||
|
||||
await log('languages', {
|
||||
name: language.name,
|
||||
code: language.code,
|
||||
count
|
||||
})
|
||||
}
|
||||
|
||||
const { count: undefinedCount } = await generator.generate(
|
||||
`${PUBLIC_PATH}/languages/undefined.m3u`,
|
||||
{
|
||||
languages: { $size: 0 }
|
||||
}
|
||||
)
|
||||
|
||||
await log('languages', {
|
||||
name: 'Undefined',
|
||||
code: 'undefined',
|
||||
count: undefinedCount
|
||||
})
|
||||
}
|
||||
|
||||
async function generateRegions() {
|
||||
logger.info(`Generating regions/...`)
|
||||
|
||||
for (const region of regions) {
|
||||
const { count } = await generator.generate(
|
||||
`${PUBLIC_PATH}/regions/${region.code.toLowerCase()}.m3u`,
|
||||
{
|
||||
regions: { $elemMatch: region }
|
||||
}
|
||||
)
|
||||
|
||||
await log('regions', {
|
||||
name: region.name,
|
||||
code: region.code,
|
||||
count
|
||||
})
|
||||
}
|
||||
|
||||
const { count: undefinedCount } = await generator.generate(
|
||||
`${PUBLIC_PATH}/regions/undefined.m3u`,
|
||||
{ regions: { $size: 0 } },
|
||||
{ saveEmpty: true }
|
||||
)
|
||||
|
||||
await log('regions', {
|
||||
name: 'Undefined',
|
||||
code: 'UNDEFINED',
|
||||
count: undefinedCount
|
||||
})
|
||||
}
|
||||
|
||||
async function generateIndexNSFW() {
|
||||
logger.info(`Generating index.nsfw.m3u...`)
|
||||
|
||||
await generator.generate(`${PUBLIC_PATH}/index.nsfw.m3u`, {}, { includeNSFW: true })
|
||||
}
|
||||
|
||||
async function generateIndex() {
|
||||
logger.info(`Generating index.m3u...`)
|
||||
|
||||
await generator.generate(`${PUBLIC_PATH}/index.m3u`, {})
|
||||
}
|
||||
|
||||
async function generateIndexCategory() {
|
||||
logger.info(`Generating index.category.m3u...`)
|
||||
|
||||
await generator.generate(
|
||||
`${PUBLIC_PATH}/index.category.m3u`,
|
||||
{},
|
||||
{ sortBy: item => item.group_title }
|
||||
)
|
||||
}
|
||||
|
||||
async function generateIndexCountry() {
|
||||
logger.info(`Generating index.country.m3u...`)
|
||||
|
||||
await generator.generate(
|
||||
`${PUBLIC_PATH}/index.country.m3u`,
|
||||
{},
|
||||
{
|
||||
onLoad: function (items) {
|
||||
let results = items
|
||||
.filter(item => !item.countries.length)
|
||||
.map(item => {
|
||||
const newItem = _.cloneDeep(item)
|
||||
newItem.group_title = ''
|
||||
return newItem
|
||||
})
|
||||
for (const country of _.sortBy(Object.values(countries), ['name'])) {
|
||||
let filtered = items
|
||||
.filter(item => {
|
||||
return item.countries.map(c => c.code).includes(country.code)
|
||||
})
|
||||
.map(item => {
|
||||
const newItem = _.cloneDeep(item)
|
||||
newItem.group_title = country.name
|
||||
return newItem
|
||||
})
|
||||
results = results.concat(filtered)
|
||||
}
|
||||
|
||||
return results
|
||||
},
|
||||
sortBy: item => item.group_title
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function generateIndexLanguage() {
|
||||
logger.info(`Generating index.language.m3u...`)
|
||||
|
||||
await generator.generate(
|
||||
`${PUBLIC_PATH}/index.language.m3u`,
|
||||
{},
|
||||
{
|
||||
onLoad: function (items) {
|
||||
let results = items
|
||||
.filter(item => !item.languages.length)
|
||||
.map(item => {
|
||||
const newItem = _.cloneDeep(item)
|
||||
newItem.group_title = ''
|
||||
return newItem
|
||||
})
|
||||
for (const language of languages) {
|
||||
let filtered = items
|
||||
.filter(item => {
|
||||
return item.languages.map(c => c.code).includes(language.code)
|
||||
})
|
||||
.map(item => {
|
||||
const newItem = _.cloneDeep(item)
|
||||
newItem.group_title = language.name
|
||||
return newItem
|
||||
})
|
||||
results = results.concat(filtered)
|
||||
}
|
||||
|
||||
return results
|
||||
},
|
||||
sortBy: item => item.group_title
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function generateIndexRegion() {
|
||||
logger.info(`Generating index.region.m3u...`)
|
||||
|
||||
await generator.generate(
|
||||
`${PUBLIC_PATH}/index.region.m3u`,
|
||||
{},
|
||||
{
|
||||
onLoad: function (items) {
|
||||
let results = items
|
||||
.filter(item => !item.regions.length)
|
||||
.map(item => {
|
||||
const newItem = _.cloneDeep(item)
|
||||
newItem.group_title = ''
|
||||
return newItem
|
||||
})
|
||||
for (const region of regions) {
|
||||
let filtered = items
|
||||
.filter(item => {
|
||||
return item.regions.map(c => c.code).includes(region.code)
|
||||
})
|
||||
.map(item => {
|
||||
const newItem = _.cloneDeep(item)
|
||||
newItem.group_title = region.name
|
||||
return newItem
|
||||
})
|
||||
results = results.concat(filtered)
|
||||
}
|
||||
|
||||
return results
|
||||
},
|
||||
sortBy: item => item.group_title
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function generateChannelsJson() {
|
||||
logger.info('Generating channels.json...')
|
||||
|
||||
await generator.generate(`${PUBLIC_PATH}/channels.json`, {}, { format: 'json' })
|
||||
}
|
||||
|
||||
async function setUp() {
|
||||
logger.info(`Loading database...`)
|
||||
const items = await db.find({})
|
||||
categories = _.sortBy(_.uniqBy(_.flatten(items.map(i => i.categories)), 'slug'), ['name'])
|
||||
countries = _.sortBy(_.uniqBy(_.flatten(items.map(i => i.countries)), 'code'), ['name'])
|
||||
languages = _.sortBy(_.uniqBy(_.flatten(items.map(i => i.languages)), 'code'), ['name'])
|
||||
regions = _.sortBy(_.uniqBy(_.flatten(items.map(i => i.regions)), 'code'), ['name'])
|
||||
|
||||
const categoriesLog = `${LOGS_PATH}/generate-playlists/categories.log`
|
||||
const countriesLog = `${LOGS_PATH}/generate-playlists/countries.log`
|
||||
const languagesLog = `${LOGS_PATH}/generate-playlists/languages.log`
|
||||
const regionsLog = `${LOGS_PATH}/generate-playlists/regions.log`
|
||||
|
||||
logger.info(`Creating '${categoriesLog}'...`)
|
||||
await file.create(categoriesLog)
|
||||
logger.info(`Creating '${countriesLog}'...`)
|
||||
await file.create(countriesLog)
|
||||
logger.info(`Creating '${languagesLog}'...`)
|
||||
await file.create(languagesLog)
|
||||
logger.info(`Creating '${regionsLog}'...`)
|
||||
await file.create(regionsLog)
|
||||
}
|
||||
|
||||
async function log(type, data) {
|
||||
await file.append(`${LOGS_PATH}/generate-playlists/${type}.log`, JSON.stringify(data) + '\n')
|
||||
}
|
|
@ -0,0 +1,232 @@
|
|||
const _ = require('lodash')
|
||||
const statuses = require('../data/statuses')
|
||||
const languages = require('../data/languages')
|
||||
const { db, store, parser, file, logger } = require('../core')
|
||||
|
||||
let epgCodes = []
|
||||
let streams = []
|
||||
let checkResults = {}
|
||||
const origins = {}
|
||||
const items = []
|
||||
|
||||
const LOGS_PATH = process.env.LOGS_PATH || 'scripts/logs'
|
||||
const EPG_CODES_FILEPATH = process.env.EPG_CODES_FILEPATH || 'scripts/data/codes.json'
|
||||
|
||||
async function main() {
|
||||
await setUp()
|
||||
await loadDatabase()
|
||||
await removeDuplicates()
|
||||
await loadCheckResults()
|
||||
await findStreamOrigins()
|
||||
await updateStreams()
|
||||
await updateDatabase()
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
async function loadDatabase() {
|
||||
logger.info('Loading database...')
|
||||
|
||||
streams = await db.find({})
|
||||
|
||||
logger.info(`Found ${streams.length} streams`)
|
||||
}
|
||||
|
||||
async function removeDuplicates() {
|
||||
logger.info('Removing duplicates...')
|
||||
|
||||
const before = streams.length
|
||||
streams = _.uniqBy(streams, 'id')
|
||||
const after = streams.length
|
||||
|
||||
logger.info(`Removed ${before - after} links`)
|
||||
}
|
||||
|
||||
async function loadCheckResults() {
|
||||
logger.info('Loading check results from logs/...')
|
||||
|
||||
const files = await file.list(`${LOGS_PATH}/check-streams/cluster_*.log`)
|
||||
for (const filepath of files) {
|
||||
const results = await parser.parseLogs(filepath)
|
||||
for (const result of results) {
|
||||
checkResults[result._id] = result
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Found ${Object.values(checkResults).length} results`)
|
||||
}
|
||||
|
||||
async function findStreamOrigins() {
|
||||
logger.info('Searching for stream origins...')
|
||||
|
||||
for (const { error, requests } of Object.values(checkResults)) {
|
||||
if (error || !Array.isArray(requests) || !requests.length) continue
|
||||
|
||||
let origin = requests.shift()
|
||||
origin = new URL(origin.url)
|
||||
for (const request of requests) {
|
||||
const curr = new URL(request.url)
|
||||
const key = curr.href.replace(/(^\w+:|^)/, '')
|
||||
if (!origins[key] && curr.host === origin.host) {
|
||||
origins[key] = origin.href
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Found ${_.uniq(Object.values(origins)).length} origins`)
|
||||
}
|
||||
|
||||
async function updateStreams() {
|
||||
logger.info('Updating streams...')
|
||||
|
||||
let updated = 0
|
||||
for (const item of streams) {
|
||||
const stream = store.create(item)
|
||||
const result = checkResults[item._id]
|
||||
|
||||
if (result) {
|
||||
const { error, streams, requests } = result
|
||||
const status = parseStatus(error)
|
||||
const resolution = parseResolution(streams)
|
||||
const origin = findOrigin(requests)
|
||||
|
||||
if (status) {
|
||||
stream.set('status', { status })
|
||||
stream.set('is_broken', { status: stream.get('status') })
|
||||
}
|
||||
|
||||
if (resolution) {
|
||||
stream.set('resolution', { resolution })
|
||||
}
|
||||
|
||||
if (origin) {
|
||||
stream.set('url', { url: origin })
|
||||
}
|
||||
}
|
||||
|
||||
if (!stream.has('logo')) {
|
||||
const logo = findLogo(stream.get('id'))
|
||||
stream.set('logo', { logo })
|
||||
}
|
||||
|
||||
if (!stream.has('guides')) {
|
||||
const guides = findGuides(stream.get('id'))
|
||||
stream.set('guides', { guides })
|
||||
}
|
||||
|
||||
if (!stream.has('countries') && stream.get('src_country')) {
|
||||
const countries = [stream.get('src_country')]
|
||||
stream.set('countries', { countries })
|
||||
}
|
||||
|
||||
if (!stream.has('languages')) {
|
||||
const languages = findLanguages(stream.get('countries'), stream.get('src_country'))
|
||||
stream.set('languages', { languages })
|
||||
}
|
||||
|
||||
if (stream.changed) {
|
||||
stream.set('updated', true)
|
||||
items.push(stream.data())
|
||||
updated++
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Updated ${updated} items`)
|
||||
}
|
||||
|
||||
async function updateDatabase() {
|
||||
logger.info('Updating database...')
|
||||
|
||||
for (const item of items) {
|
||||
await db.update({ _id: item._id }, item)
|
||||
}
|
||||
db.compact()
|
||||
|
||||
logger.info('Done')
|
||||
}
|
||||
|
||||
async function setUp() {
|
||||
try {
|
||||
const codes = await file.read(EPG_CODES_FILEPATH)
|
||||
epgCodes = JSON.parse(codes)
|
||||
} catch (err) {
|
||||
logger.error(err.message)
|
||||
}
|
||||
}
|
||||
|
||||
function findLanguages(countries, src_country) {
|
||||
if (countries && Array.isArray(countries)) {
|
||||
let codes = countries.map(country => country.lang)
|
||||
codes = _.uniq(codes)
|
||||
|
||||
return codes.map(code => languages.find(l => l.code === code)).filter(l => l)
|
||||
}
|
||||
|
||||
if (src_country) {
|
||||
const code = src_country.lang
|
||||
const lang = languages.find(l => l.code === code)
|
||||
|
||||
return lang ? [lang] : []
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
function findOrigin(requests) {
|
||||
if (origins && Array.isArray(requests)) {
|
||||
requests = requests.map(r => r.url.replace(/(^\w+:|^)/, ''))
|
||||
for (const url of requests) {
|
||||
if (origins[url]) {
|
||||
return origins[url]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function parseResolution(streams) {
|
||||
const resolution = streams
|
||||
.filter(s => s.codec_type === 'video')
|
||||
.reduce(
|
||||
(acc, curr) => {
|
||||
if (curr.height > acc.height) return { width: curr.width, height: curr.height }
|
||||
return acc
|
||||
},
|
||||
{ width: 0, height: 0 }
|
||||
)
|
||||
|
||||
if (resolution.width > 0 && resolution.height > 0) return resolution
|
||||
return null
|
||||
}
|
||||
|
||||
function parseStatus(error) {
|
||||
if (error) {
|
||||
if (error.includes('timed out')) {
|
||||
return statuses['timeout']
|
||||
} else if (error.includes('403')) {
|
||||
return statuses['geo_blocked']
|
||||
}
|
||||
return statuses['offline']
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function findLogo(id) {
|
||||
const item = epgCodes.find(i => i.tvg_id === id)
|
||||
if (item && item.logo) {
|
||||
return item.logo
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function findGuides(id) {
|
||||
const item = epgCodes.find(i => i.tvg_id === id)
|
||||
if (item && Array.isArray(item.guides)) {
|
||||
return item.guides
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
const _ = require('lodash')
|
||||
const { generator, db, logger } = require('../core')
|
||||
|
||||
async function main() {
|
||||
let items = await db
|
||||
.find({})
|
||||
.sort({ name: 1, 'status.level': 1, 'resolution.height': -1, url: 1 })
|
||||
const files = _.groupBy(items, 'filepath')
|
||||
|
||||
for (const filepath in files) {
|
||||
const items = files[filepath]
|
||||
await generator.saveAsM3U(filepath, items, { includeGuides: false })
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
|
@ -0,0 +1,142 @@
|
|||
const { file, markdown, parser, logger } = require('../core')
|
||||
const { program } = require('commander')
|
||||
|
||||
let categories = []
|
||||
let countries = []
|
||||
let languages = []
|
||||
let regions = []
|
||||
|
||||
const LOGS_PATH = process.env.LOGS_PATH || 'scripts/logs'
|
||||
|
||||
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 generateCategoryTable()
|
||||
await generateLanguageTable()
|
||||
await generateRegionTable()
|
||||
await generateCountryTable()
|
||||
|
||||
await updateReadme()
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
async function generateCategoryTable() {
|
||||
logger.info('Generating category table...')
|
||||
|
||||
const rows = []
|
||||
for (const category of categories) {
|
||||
rows.push({
|
||||
category: category.name,
|
||||
channels: category.count,
|
||||
playlist: `<code>https://iptv-org.github.io/iptv/categories/${category.slug}.m3u</code>`
|
||||
})
|
||||
}
|
||||
|
||||
const table = markdown.createTable(rows, [
|
||||
{ name: 'Category', align: 'left' },
|
||||
{ name: 'Channels', align: 'right' },
|
||||
{ name: 'Playlist', align: 'left' }
|
||||
])
|
||||
|
||||
await file.create('./.readme/_categories.md', table)
|
||||
}
|
||||
|
||||
async function generateCountryTable() {
|
||||
logger.info('Generating country table...')
|
||||
|
||||
const rows = []
|
||||
for (const country of countries) {
|
||||
const flag = getCountryFlag(country.code)
|
||||
const prefix = flag ? `${flag} ` : ''
|
||||
|
||||
rows.push({
|
||||
country: prefix + country.name,
|
||||
channels: country.count,
|
||||
playlist: `<code>https://iptv-org.github.io/iptv/countries/${country.code.toLowerCase()}.m3u</code>`
|
||||
})
|
||||
}
|
||||
|
||||
const table = markdown.createTable(rows, [
|
||||
{ name: 'Country', align: 'left' },
|
||||
{ name: 'Channels', align: 'right' },
|
||||
{ name: 'Playlist', align: 'left' }
|
||||
])
|
||||
|
||||
await file.create('./.readme/_countries.md', table)
|
||||
}
|
||||
|
||||
async function generateRegionTable() {
|
||||
logger.info('Generating region table...')
|
||||
|
||||
const rows = []
|
||||
for (const region of regions) {
|
||||
rows.push({
|
||||
region: region.name,
|
||||
channels: region.count,
|
||||
playlist: `<code>https://iptv-org.github.io/iptv/regions/${region.code.toLowerCase()}.m3u</code>`
|
||||
})
|
||||
}
|
||||
|
||||
const table = markdown.createTable(rows, [
|
||||
{ name: 'Region', align: 'left' },
|
||||
{ name: 'Channels', align: 'right' },
|
||||
{ name: 'Playlist', align: 'left' }
|
||||
])
|
||||
|
||||
await file.create('./.readme/_regions.md', table)
|
||||
}
|
||||
|
||||
async function generateLanguageTable() {
|
||||
logger.info('Generating language table...')
|
||||
|
||||
const rows = []
|
||||
for (const language of languages) {
|
||||
rows.push({
|
||||
language: language.name,
|
||||
channels: language.count,
|
||||
playlist: `<code>https://iptv-org.github.io/iptv/languages/${language.code}.m3u</code>`
|
||||
})
|
||||
}
|
||||
|
||||
const table = markdown.createTable(rows, [
|
||||
{ name: 'Language', align: 'left' },
|
||||
{ name: 'Channels', align: 'right' },
|
||||
{ name: 'Playlist', align: 'left' }
|
||||
])
|
||||
|
||||
await file.create('./.readme/_languages.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() {
|
||||
categories = await parser.parseLogs(`${LOGS_PATH}/generate-playlists/categories.log`)
|
||||
countries = await parser.parseLogs(`${LOGS_PATH}/generate-playlists/countries.log`)
|
||||
languages = await parser.parseLogs(`${LOGS_PATH}/generate-playlists/languages.log`)
|
||||
regions = await parser.parseLogs(`${LOGS_PATH}/generate-playlists/regions.log`)
|
||||
}
|
||||
|
||||
function getCountryFlag(code) {
|
||||
switch (code) {
|
||||
case 'UK':
|
||||
return '🇬🇧'
|
||||
case 'INT':
|
||||
return '🌍'
|
||||
case 'UNDEFINED':
|
||||
return ''
|
||||
default:
|
||||
return code.replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
const blocklist = require('../data/blocklist')
|
||||
const parser = require('iptv-playlist-parser')
|
||||
const { file, logger } = require('../core')
|
||||
const { program } = require('commander')
|
||||
|
||||
const options = program
|
||||
.option('--input-dir <input-dir>', 'Set path to input directory', 'channels')
|
||||
.parse(process.argv)
|
||||
.opts()
|
||||
|
||||
async function main() {
|
||||
const files = await file.list(`${options.inputDir}/**/*.m3u`)
|
||||
const errors = []
|
||||
for (const filepath of files) {
|
||||
const content = await file.read(filepath)
|
||||
const playlist = parser.parse(content)
|
||||
const basename = file.basename(filepath)
|
||||
const [_, country] = basename.match(/([a-z]{2})(|_.*)\.m3u/i) || [null, null]
|
||||
|
||||
const items = playlist.items
|
||||
.map(item => {
|
||||
const details = check(item, country)
|
||||
|
||||
return details ? { ...item, details } : null
|
||||
})
|
||||
.filter(i => i)
|
||||
|
||||
items.forEach(item => {
|
||||
errors.push(
|
||||
`${filepath}:${item.line} '${item.details.name}' is on the blocklist due to claims of copyright holders (${item.details.reference})`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
errors.forEach(error => {
|
||||
logger.error(error)
|
||||
})
|
||||
|
||||
if (errors.length) {
|
||||
logger.info('')
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
function check(channel, country) {
|
||||
return blocklist.find(item => {
|
||||
const regexp = new RegExp(item.regex, 'i')
|
||||
const hasSameName = regexp.test(channel.name)
|
||||
const fromSameCountry = country === item.country.toLowerCase()
|
||||
|
||||
return hasSameName && fromSameCountry
|
||||
})
|
||||
}
|
||||
|
||||
main()
|
|
@ -0,0 +1,19 @@
|
|||
const IPTVChecker = require('iptv-checker')
|
||||
|
||||
const checker = {}
|
||||
|
||||
checker.check = async function (item, config) {
|
||||
const ic = new IPTVChecker(config)
|
||||
const result = await ic.checkStream({ url: item.url, http: item.http })
|
||||
|
||||
return {
|
||||
_id: item._id,
|
||||
url: item.url,
|
||||
http: item.http,
|
||||
error: !result.status.ok ? result.status.reason : null,
|
||||
streams: result.status.ok ? result.status.metadata.streams : [],
|
||||
requests: result.status.ok ? result.status.metadata.requests : []
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = checker
|
|
@ -0,0 +1,61 @@
|
|||
const Database = require('nedb-promises')
|
||||
const file = require('./file')
|
||||
|
||||
const DB_FILEPATH = process.env.DB_FILEPATH || './scripts/channels.db'
|
||||
|
||||
const nedb = Database.create({
|
||||
filename: file.resolve(DB_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
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const db = {}
|
||||
|
||||
db.removeIndex = function (field) {
|
||||
return nedb.removeIndex(field)
|
||||
}
|
||||
|
||||
db.addIndex = function (options) {
|
||||
return nedb.ensureIndex(options)
|
||||
}
|
||||
|
||||
db.compact = function () {
|
||||
return nedb.persistence.compactDatafile()
|
||||
}
|
||||
|
||||
db.reset = function () {
|
||||
return file.clear(DB_FILEPATH)
|
||||
}
|
||||
|
||||
db.count = function (query) {
|
||||
return nedb.count(query)
|
||||
}
|
||||
|
||||
db.insert = function (doc) {
|
||||
return nedb.insert(doc)
|
||||
}
|
||||
|
||||
db.update = function (query, update) {
|
||||
return nedb.update(query, update)
|
||||
}
|
||||
|
||||
db.find = function (query) {
|
||||
return nedb.find(query)
|
||||
}
|
||||
|
||||
db.remove = function (query, options) {
|
||||
return nedb.remove(query, options)
|
||||
}
|
||||
|
||||
module.exports = db
|
|
@ -0,0 +1,67 @@
|
|||
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(() => fs.writeFile(filepath, data, { encoding: 'utf8', flag: 'w' }))
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
file.write = function (filepath, data = '') {
|
||||
return fs.writeFile(path.resolve(filepath), data).catch(console.error)
|
||||
}
|
||||
|
||||
file.clear = function (filepath) {
|
||||
return file.write(filepath, '')
|
||||
}
|
||||
|
||||
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
|
|
@ -0,0 +1,114 @@
|
|||
const { create: createPlaylist } = require('./playlist')
|
||||
const store = require('./store')
|
||||
const file = require('./file')
|
||||
const logger = require('./logger')
|
||||
const db = require('./db')
|
||||
const _ = require('lodash')
|
||||
|
||||
const generator = {}
|
||||
|
||||
generator.generate = async function (filepath, query = {}, options = {}) {
|
||||
options = {
|
||||
...{
|
||||
format: 'm3u',
|
||||
saveEmpty: false,
|
||||
includeNSFW: false,
|
||||
includeGuides: true,
|
||||
includeBroken: false,
|
||||
onLoad: r => r,
|
||||
uniqBy: item => item.id || _.uniqueId(),
|
||||
sortBy: null
|
||||
},
|
||||
...options
|
||||
}
|
||||
|
||||
query['is_nsfw'] = options.includeNSFW ? { $in: [true, false] } : false
|
||||
query['is_broken'] = options.includeBroken ? { $in: [true, false] } : false
|
||||
|
||||
let items = await db
|
||||
.find(query)
|
||||
.sort({ name: 1, 'status.level': 1, 'resolution.height': -1, url: 1 })
|
||||
|
||||
items = _.uniqBy(items, 'url')
|
||||
if (!options.saveEmpty && !items.length) return { filepath, query, options, count: 0 }
|
||||
if (options.uniqBy) items = _.uniqBy(items, options.uniqBy)
|
||||
|
||||
items = options.onLoad(items)
|
||||
|
||||
if (options.sortBy) items = _.sortBy(items, options.sortBy)
|
||||
|
||||
switch (options.format) {
|
||||
case 'json':
|
||||
await saveAsJSON(filepath, items, options)
|
||||
break
|
||||
case 'm3u':
|
||||
default:
|
||||
await saveAsM3U(filepath, items, options)
|
||||
break
|
||||
}
|
||||
|
||||
return { filepath, query, options, count: items.length }
|
||||
}
|
||||
|
||||
async function saveAsM3U(filepath, items, options) {
|
||||
const playlist = await createPlaylist(filepath)
|
||||
|
||||
const header = {}
|
||||
if (options.includeGuides) {
|
||||
let guides = items.map(item => item.guides)
|
||||
guides = _.uniq(_.flatten(guides)).sort().join(',')
|
||||
|
||||
header['x-tvg-url'] = guides
|
||||
}
|
||||
|
||||
await playlist.header(header)
|
||||
for (const item of items) {
|
||||
const stream = store.create(item)
|
||||
await playlist.link(
|
||||
stream.get('url'),
|
||||
stream.get('title'),
|
||||
{
|
||||
'tvg-id': stream.get('tvg_id'),
|
||||
'tvg-country': stream.get('tvg_country'),
|
||||
'tvg-language': stream.get('tvg_language'),
|
||||
'tvg-logo': stream.get('tvg_logo'),
|
||||
// 'tvg-url': stream.get('tvg_url') || undefined,
|
||||
'user-agent': stream.get('http.user-agent') || undefined,
|
||||
'group-title': stream.get('group_title')
|
||||
},
|
||||
{
|
||||
'http-referrer': stream.get('http.referrer') || undefined,
|
||||
'http-user-agent': stream.get('http.user-agent') || undefined
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAsJSON(filepath, items, options) {
|
||||
const output = items.map(item => {
|
||||
const stream = store.create(item)
|
||||
const categories = stream.get('categories').map(c => ({ name: c.name, slug: c.slug }))
|
||||
const countries = stream.get('countries').map(c => ({ name: c.name, code: c.code }))
|
||||
|
||||
return {
|
||||
name: stream.get('name'),
|
||||
logo: stream.get('logo'),
|
||||
url: stream.get('url'),
|
||||
categories,
|
||||
countries,
|
||||
languages: stream.get('languages'),
|
||||
tvg: {
|
||||
id: stream.get('tvg_id'),
|
||||
name: stream.get('name'),
|
||||
url: stream.get('tvg_url')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await file.create(filepath, JSON.stringify(output))
|
||||
}
|
||||
|
||||
generator.saveAsM3U = saveAsM3U
|
||||
generator.saveAsJSON = saveAsJSON
|
||||
|
||||
module.exports = generator
|
|
@ -0,0 +1,10 @@
|
|||
exports.db = require('./db')
|
||||
exports.logger = require('./logger')
|
||||
exports.file = require('./file')
|
||||
exports.timer = require('./timer')
|
||||
exports.parser = require('./parser')
|
||||
exports.checker = require('./checker')
|
||||
exports.generator = require('./generator')
|
||||
exports.playlist = require('./playlist')
|
||||
exports.store = require('./store')
|
||||
exports.markdown = require('./markdown')
|
|
@ -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
|
|
@ -0,0 +1,39 @@
|
|||
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="${column.align}">${column.name}</th>`
|
||||
}
|
||||
output += '</tr>\n </thead>\n'
|
||||
|
||||
output += ' <tbody>\n'
|
||||
for (let item of data) {
|
||||
output += ' <tr>'
|
||||
let i = 0
|
||||
for (let prop in item) {
|
||||
const column = cols[i]
|
||||
let nowrap = column.nowrap
|
||||
let align = column.align
|
||||
output += `<td align="${align}"${nowrap ? ' nowrap' : ''}>${item[prop]}</td>`
|
||||
i++
|
||||
}
|
||||
output += '</tr>\n'
|
||||
}
|
||||
output += ' </tbody>\n'
|
||||
|
||||
output += '</table>'
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
markdown.compile = function (filepath) {
|
||||
markdownInclude.compileFiles(file.resolve(filepath))
|
||||
}
|
||||
|
||||
module.exports = markdown
|
|
@ -0,0 +1,31 @@
|
|||
const ipp = require('iptv-playlist-parser')
|
||||
const logger = require('./logger')
|
||||
const file = require('./file')
|
||||
|
||||
const parser = {}
|
||||
|
||||
parser.parsePlaylist = async function (filepath) {
|
||||
const content = await file.read(filepath)
|
||||
const playlist = ipp.parse(content)
|
||||
|
||||
return playlist.items
|
||||
}
|
||||
|
||||
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
|
|
@ -0,0 +1,49 @@
|
|||
const file = require('./file')
|
||||
|
||||
const playlist = {}
|
||||
|
||||
playlist.create = async function (filepath) {
|
||||
playlist.filepath = filepath
|
||||
const dir = file.dirname(filepath)
|
||||
file.createDir(dir)
|
||||
await file.create(filepath, '')
|
||||
|
||||
return playlist
|
||||
}
|
||||
|
||||
playlist.header = async function (attrs) {
|
||||
let header = `#EXTM3U`
|
||||
for (const name in attrs) {
|
||||
const value = attrs[name]
|
||||
header += ` ${name}="${value}"`
|
||||
}
|
||||
header += `\n`
|
||||
|
||||
await file.append(playlist.filepath, header)
|
||||
|
||||
return playlist
|
||||
}
|
||||
|
||||
playlist.link = async function (url, title, attrs, vlcOpts) {
|
||||
let link = `#EXTINF:-1`
|
||||
for (const name in attrs) {
|
||||
const value = attrs[name]
|
||||
if (value !== undefined) {
|
||||
link += ` ${name}="${value}"`
|
||||
}
|
||||
}
|
||||
link += `,${title}\n`
|
||||
for (const name in vlcOpts) {
|
||||
const value = vlcOpts[name]
|
||||
if (value !== undefined) {
|
||||
link += `#EXTVLCOPT:${name}=${value}\n`
|
||||
}
|
||||
}
|
||||
link += `${url}\n`
|
||||
|
||||
await file.append(playlist.filepath, link)
|
||||
|
||||
return playlist
|
||||
}
|
||||
|
||||
module.exports = playlist
|
|
@ -0,0 +1,56 @@
|
|||
const _ = require('lodash')
|
||||
const logger = require('./logger')
|
||||
const setters = require('../store/setters')
|
||||
const getters = require('../store/getters')
|
||||
|
||||
module.exports = {
|
||||
create(state = {}) {
|
||||
return {
|
||||
state,
|
||||
changed: false,
|
||||
set: function (prop, value) {
|
||||
const prevState = JSON.stringify(this.state)
|
||||
|
||||
const setter = setters[prop]
|
||||
if (typeof setter === 'function') {
|
||||
try {
|
||||
this.state[prop] = setter.bind()(value)
|
||||
} catch (error) {
|
||||
logger.error(`store/setters/${prop}.js: ${error.message}`)
|
||||
}
|
||||
} else if (typeof value === 'object') {
|
||||
this.state[prop] = value[prop]
|
||||
} else {
|
||||
this.state[prop] = value
|
||||
}
|
||||
|
||||
const newState = JSON.stringify(this.state)
|
||||
if (prevState !== newState) {
|
||||
this.changed = true
|
||||
}
|
||||
|
||||
return this
|
||||
},
|
||||
get: function (prop) {
|
||||
const getter = getters[prop]
|
||||
if (typeof getter === 'function') {
|
||||
try {
|
||||
return getter.bind(this.state)()
|
||||
} catch (error) {
|
||||
logger.error(`store/getters/${prop}.js: ${error.message}`)
|
||||
}
|
||||
} else {
|
||||
return prop.split('.').reduce((o, i) => (o ? o[i] : undefined), this.state)
|
||||
}
|
||||
},
|
||||
has: function (prop) {
|
||||
const value = this.get(prop)
|
||||
|
||||
return !_.isEmpty(value)
|
||||
},
|
||||
data: function () {
|
||||
return this.state
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -1,9 +0,0 @@
|
|||
const file = require('./helpers/file')
|
||||
|
||||
file.list().then(files => {
|
||||
files = files.filter(file => file !== 'channels/unsorted.m3u')
|
||||
const country = files.map(file => file.replace(/channels\/|\.m3u/gi, ''))
|
||||
const matrix = { country }
|
||||
const output = `::set-output name=matrix::${JSON.stringify(matrix)}`
|
||||
console.log(output)
|
||||
})
|
|
@ -0,0 +1 @@
|
|||
codes.json
|
|
@ -1,632 +0,0 @@
|
|||
[
|
||||
{
|
||||
"name": "Animal Planet",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Animal Planet$"
|
||||
},
|
||||
{
|
||||
"name": "Arena 4",
|
||||
"country": "hu",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Arena( |)4$"
|
||||
},
|
||||
{
|
||||
"name": "Asian Food Network",
|
||||
"country": "sg",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Asian Food Network$"
|
||||
},
|
||||
{
|
||||
"name": "Astro SuperSport",
|
||||
"country": "my",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Astro SuperSport$"
|
||||
},
|
||||
{
|
||||
"name": "Azteca 7",
|
||||
"country": "mx",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Azteca 7$"
|
||||
},
|
||||
{
|
||||
"name": "beIN Sports",
|
||||
"country": "qa",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^beIN Sports"
|
||||
},
|
||||
{
|
||||
"name": "Canal+ Sport",
|
||||
"country": "fr",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Canal( |)+ Sport"
|
||||
},
|
||||
{
|
||||
"name": "Cooking Channel",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Cooking Channel$"
|
||||
},
|
||||
{
|
||||
"name": "DAZN",
|
||||
"country": "uk",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^DAZN($| [1-4] .*)$"
|
||||
},
|
||||
{
|
||||
"name": "Diema Sport",
|
||||
"country": "bg",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Diema Sport($| [1-3])$"
|
||||
},
|
||||
{
|
||||
"name": "Digi Sport",
|
||||
"country": "ro",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Digi Sport($| [1-4].*)"
|
||||
},
|
||||
{
|
||||
"name": "Discovery Asia",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery Asia$"
|
||||
},
|
||||
{
|
||||
"name": "Discovery Channel",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery Channel$"
|
||||
},
|
||||
{
|
||||
"name": "Discovery Civiliztion",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery Civiliztion$"
|
||||
},
|
||||
{
|
||||
"name": "Discovery en Espanol",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery en Espanol$"
|
||||
},
|
||||
{
|
||||
"name": "Discovery Family",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery Family$"
|
||||
},
|
||||
{
|
||||
"name": "Discovery Historia",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery Historia$"
|
||||
},
|
||||
{
|
||||
"name": "Discovery History",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery History$"
|
||||
},
|
||||
{
|
||||
"name": "Discovery Home and Health",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery Home and Health$"
|
||||
},
|
||||
{
|
||||
"name": "Discovery Life",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery Life$"
|
||||
},
|
||||
{
|
||||
"name": "Discovery Science",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery Science$"
|
||||
},
|
||||
{
|
||||
"name": "Discovery Shed",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery Shed$"
|
||||
},
|
||||
{
|
||||
"name": "Discovery Theater",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery Theater$"
|
||||
},
|
||||
{
|
||||
"name": "Discovery Travel and Living",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery Travel and Living$"
|
||||
},
|
||||
{
|
||||
"name": "Discovery Turbo Xtra",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery Turbo Xtra$"
|
||||
},
|
||||
{
|
||||
"name": "Discovery World",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery World$"
|
||||
},
|
||||
{
|
||||
"name": "Discovery",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery$"
|
||||
},
|
||||
{
|
||||
"name": "DIY Network",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^DIY Network$"
|
||||
},
|
||||
{
|
||||
"name": "DKiss",
|
||||
"country": "es",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^DKiss$"
|
||||
},
|
||||
{
|
||||
"name": "DMax",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^DMax$"
|
||||
},
|
||||
{
|
||||
"name": "Eleven Sports",
|
||||
"country": "uk",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Eleven Sports($| [1-6] .*)$"
|
||||
},
|
||||
{
|
||||
"name": "ESPN",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^ESPN($|[1-3]| .*)$"
|
||||
},
|
||||
{
|
||||
"name": "Eurosport",
|
||||
"country": "fr",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Eurosport($| [1-2])$"
|
||||
},
|
||||
{
|
||||
"name": "eve",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^eve$"
|
||||
},
|
||||
{
|
||||
"name": "Familia Discovery",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Familia Discovery$"
|
||||
},
|
||||
{
|
||||
"name": "Fatafeat",
|
||||
"country": "eg",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Fatafeat$"
|
||||
},
|
||||
{
|
||||
"name": "FEM",
|
||||
"country": "no",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^FEM$"
|
||||
},
|
||||
{
|
||||
"name": "Fine Living",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Fine Living$"
|
||||
},
|
||||
{
|
||||
"name": "Flow Sports",
|
||||
"country": "uk",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Flow Sports$"
|
||||
},
|
||||
{
|
||||
"name": "Food Network",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Food Network$"
|
||||
},
|
||||
{
|
||||
"name": "food tv",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^food( |)tv$"
|
||||
},
|
||||
{
|
||||
"name": "Fox Sports",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Fox Sports$"
|
||||
},
|
||||
{
|
||||
"name": "Frisbee",
|
||||
"country": "it",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Frisbee$"
|
||||
},
|
||||
{
|
||||
"name": "Futbol",
|
||||
"country": "tj",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^(Futbol|Football TV|ТВ Футбол|Футбол)$"
|
||||
},
|
||||
{
|
||||
"name": "GTV Sports",
|
||||
"country": "gh",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^GTV Sports"
|
||||
},
|
||||
{
|
||||
"name": "Giallo",
|
||||
"country": "it",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Giallo$"
|
||||
},
|
||||
{
|
||||
"name": "GolfTV",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Golf( |)TV$"
|
||||
},
|
||||
{
|
||||
"name": "HGTV",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^HGTV$"
|
||||
},
|
||||
{
|
||||
"name": "Investigation Discovery",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^(Investigation Discovery|ID Investigation Discovery|ID Investigation|ID)$"
|
||||
},
|
||||
{
|
||||
"name": "K2",
|
||||
"country": "it",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^K2$"
|
||||
},
|
||||
{
|
||||
"name": "Living Channel",
|
||||
"country": "nz",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Living Channel$"
|
||||
},
|
||||
{
|
||||
"name": "LookSport",
|
||||
"country": "ro",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Look( |)Sport"
|
||||
},
|
||||
{
|
||||
"name": "Mango",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Mango$"
|
||||
},
|
||||
{
|
||||
"name": "Match!",
|
||||
"country": "ru",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^(Match|Матч)"
|
||||
},
|
||||
{
|
||||
"name": "Motortrend",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Motortrend$"
|
||||
},
|
||||
{
|
||||
"name": "Mola TV",
|
||||
"country": "id",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Mola TV($| .*)$"
|
||||
},
|
||||
{
|
||||
"name": "Movistar Liga de Campeones",
|
||||
"country": "es",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Movistar Liga de Campeones [1-8]$"
|
||||
},
|
||||
{
|
||||
"name": "Nova Sport",
|
||||
"country": "cz",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Nova Sport [1-3]$"
|
||||
},
|
||||
{
|
||||
"name": "Nova Sports",
|
||||
"country": "gr",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Nova Sports [1-6]$"
|
||||
},
|
||||
{
|
||||
"name": "Nove",
|
||||
"country": "it",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Nove($| .*)$"
|
||||
},
|
||||
{
|
||||
"name": "PPTV HD 36",
|
||||
"country": "th",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^PPTV($| HD)$"
|
||||
},
|
||||
{
|
||||
"name": "One",
|
||||
"country": "il",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^One$"
|
||||
},
|
||||
{
|
||||
"name": "OWN",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^(OWN|Oprah)$"
|
||||
},
|
||||
{
|
||||
"name": "Quest Red",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Quest Red$"
|
||||
},
|
||||
{
|
||||
"name": "Quest",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Quest$"
|
||||
},
|
||||
{
|
||||
"name": "Real Time",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Real Time$"
|
||||
},
|
||||
{
|
||||
"name": "SABC Sport ",
|
||||
"country": "za",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^SABC Sport"
|
||||
},
|
||||
{
|
||||
"name": "Setanta Sports",
|
||||
"country": "ie",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Setanta Sports($| .*)$"
|
||||
},
|
||||
{
|
||||
"name": "Sky Sports",
|
||||
"country": "uk",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Sky Sports$"
|
||||
},
|
||||
{
|
||||
"name": "Sky Sport Bundesliga",
|
||||
"country": "de",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Sky Sport Bundesliga [0-9]+$"
|
||||
},
|
||||
{
|
||||
"name": "Sky TG24",
|
||||
"country": "it",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/pull/2294",
|
||||
"regex": "^Sky TG24$"
|
||||
},
|
||||
{
|
||||
"name": "Sony Ten",
|
||||
"country": "in",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Sony Ten [1-4]$"
|
||||
},
|
||||
{
|
||||
"name": "Spíler TV",
|
||||
"country": "hu",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Spíler( |[1-2] )TV$"
|
||||
},
|
||||
{
|
||||
"name": "Šport TV",
|
||||
"country": "si",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^(Šport|Sport) TV"
|
||||
},
|
||||
{
|
||||
"name": "Sport Klub",
|
||||
"country": "hu",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^(Sport Klub|SK[1-9])"
|
||||
},
|
||||
{
|
||||
"name": "SportsNet",
|
||||
"country": "ca",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^SportsNet$"
|
||||
},
|
||||
{
|
||||
"name": "StarHub TV",
|
||||
"country": "sg",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^StarHub TV$"
|
||||
},
|
||||
{
|
||||
"name": "StarSat",
|
||||
"country": "za",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^StarSat$"
|
||||
},
|
||||
{
|
||||
"name": "StarTimes TV",
|
||||
"country": "mz",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^StarTimes TV"
|
||||
},
|
||||
{
|
||||
"name": "SuperSport",
|
||||
"country": "al",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^SuperSport [1-7]$"
|
||||
},
|
||||
{
|
||||
"name": "Tivibu Spor",
|
||||
"country": "tr",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Tivibu Spor"
|
||||
},
|
||||
{
|
||||
"name": "TLC",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^TLC$"
|
||||
},
|
||||
{
|
||||
"name": "Trvl Channel",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Trvl Channel$"
|
||||
},
|
||||
{
|
||||
"name": "TSN",
|
||||
"country": "mt",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^TSN$"
|
||||
},
|
||||
{
|
||||
"name": "TTV",
|
||||
"country": "pl",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^TTV$"
|
||||
},
|
||||
{
|
||||
"name": "TV Norge",
|
||||
"country": "no",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^TV Norge$"
|
||||
},
|
||||
{
|
||||
"name": "TV Varzish",
|
||||
"country": "tj",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^(TV Varzish|Varzish TV|Варзиш ТВ)$"
|
||||
},
|
||||
{
|
||||
"name": "TV3 Sport",
|
||||
"country": "dk",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^TV( |)3 Sport$"
|
||||
},
|
||||
{
|
||||
"name": "tvN Asia",
|
||||
"country": "kr",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^tvN($| Asia)$"
|
||||
},
|
||||
{
|
||||
"name": "Tvn 24 Bis",
|
||||
"country": "pl",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Tvn 24 Bis$"
|
||||
},
|
||||
{
|
||||
"name": "TVN 24",
|
||||
"country": "pl",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^TVN 24$"
|
||||
},
|
||||
{
|
||||
"name": "Tvn 7",
|
||||
"country": "pl",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Tvn 7$"
|
||||
},
|
||||
{
|
||||
"name": "TVN Extra",
|
||||
"country": "pl",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^TVN Extra$"
|
||||
},
|
||||
{
|
||||
"name": "TVN Fabula",
|
||||
"country": "pl",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^TVN Fabula$"
|
||||
},
|
||||
{
|
||||
"name": "TVN Meteo",
|
||||
"country": "pl",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^TVN Meteo$"
|
||||
},
|
||||
{
|
||||
"name": "TVN Style",
|
||||
"country": "pl",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^TVN Style$"
|
||||
},
|
||||
{
|
||||
"name": "TVN Turbo",
|
||||
"country": "pl",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^TVN Turbo$"
|
||||
},
|
||||
{
|
||||
"name": "TVN Warszawa",
|
||||
"country": "pl",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^TVN Warszawa$"
|
||||
},
|
||||
{
|
||||
"name": "TVN",
|
||||
"country": "pl",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^TVN$"
|
||||
},
|
||||
{
|
||||
"name": "V Sport",
|
||||
"country": "no",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^V Sport"
|
||||
},
|
||||
{
|
||||
"name": "Vox",
|
||||
"country": "no",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Vox$"
|
||||
},
|
||||
{
|
||||
"name": "VTV Cab",
|
||||
"country": "kr",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^VTV( |)Cab$"
|
||||
},
|
||||
{
|
||||
"name": "World Discovery",
|
||||
"country": "us",
|
||||
"dmca_notice": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^World Discovery$"
|
||||
},
|
||||
{
|
||||
"name": "Xee",
|
||||
"country": "dk",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Xee$"
|
||||
},
|
||||
{
|
||||
"name": "XtvN",
|
||||
"country": "kr",
|
||||
"dmca_notice": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^X( |)tvN$"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,632 @@
|
|||
[
|
||||
{
|
||||
"name": "Animal Planet",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Animal Planet\\b"
|
||||
},
|
||||
{
|
||||
"name": "Arena 4",
|
||||
"country": "HU",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Arena( |)4\\b"
|
||||
},
|
||||
{
|
||||
"name": "Asian Food Network",
|
||||
"country": "SG",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Asian Food Network\\b"
|
||||
},
|
||||
{
|
||||
"name": "Astro SuperSport",
|
||||
"country": "MY",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Astro SuperSport\\b"
|
||||
},
|
||||
{
|
||||
"name": "Azteca 7",
|
||||
"country": "MX",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Azteca 7\\b"
|
||||
},
|
||||
{
|
||||
"name": "beIN Sports",
|
||||
"country": "QA",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^beIN Sports\\b"
|
||||
},
|
||||
{
|
||||
"name": "Canal+ Sport",
|
||||
"country": "FR",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Canal( |)+ Sport\\b"
|
||||
},
|
||||
{
|
||||
"name": "Cooking Channel",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Cooking Channel\\b"
|
||||
},
|
||||
{
|
||||
"name": "DAZN",
|
||||
"country": "UK",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^DAZN($| [1-4] .*)\\b"
|
||||
},
|
||||
{
|
||||
"name": "Diema Sport",
|
||||
"country": "BG",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Diema Sport\\b"
|
||||
},
|
||||
{
|
||||
"name": "Digi Sport",
|
||||
"country": "RO",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Digi Sport\\b"
|
||||
},
|
||||
{
|
||||
"name": "Discovery Asia",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery Asia\\b"
|
||||
},
|
||||
{
|
||||
"name": "Discovery Channel",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery Channel\\b"
|
||||
},
|
||||
{
|
||||
"name": "Discovery Civiliztion",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery Civiliztion\\b"
|
||||
},
|
||||
{
|
||||
"name": "Discovery en Espanol",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery en Espanol\\b"
|
||||
},
|
||||
{
|
||||
"name": "Discovery Family",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery Family\\b"
|
||||
},
|
||||
{
|
||||
"name": "Discovery Historia",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery Historia\\b"
|
||||
},
|
||||
{
|
||||
"name": "Discovery History",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery History\\b"
|
||||
},
|
||||
{
|
||||
"name": "Discovery Home and Health",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery Home and Health\\b"
|
||||
},
|
||||
{
|
||||
"name": "Discovery Life",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery Life\\b"
|
||||
},
|
||||
{
|
||||
"name": "Discovery Science",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery Science\\b"
|
||||
},
|
||||
{
|
||||
"name": "Discovery Shed",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery Shed\\b"
|
||||
},
|
||||
{
|
||||
"name": "Discovery Theater",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery Theater\\b"
|
||||
},
|
||||
{
|
||||
"name": "Discovery Travel and Living",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery Travel and Living\\b"
|
||||
},
|
||||
{
|
||||
"name": "Discovery Turbo Xtra",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery Turbo Xtra\\b"
|
||||
},
|
||||
{
|
||||
"name": "Discovery World",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery World\\b"
|
||||
},
|
||||
{
|
||||
"name": "Discovery",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Discovery\\b"
|
||||
},
|
||||
{
|
||||
"name": "DIY Network",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^DIY Network\\b"
|
||||
},
|
||||
{
|
||||
"name": "DKiss",
|
||||
"country": "ES",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^DKiss\\b"
|
||||
},
|
||||
{
|
||||
"name": "DMax",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^DMax\\b"
|
||||
},
|
||||
{
|
||||
"name": "Eleven Sports",
|
||||
"country": "UK",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Eleven Sports($| [1-6] .*)\\b"
|
||||
},
|
||||
{
|
||||
"name": "ESPN",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^ESPN($|[1-3]| .*)\\b"
|
||||
},
|
||||
{
|
||||
"name": "Eurosport",
|
||||
"country": "FR",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Eurosport($| [1-2])\\b"
|
||||
},
|
||||
{
|
||||
"name": "eve",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^eve\\b"
|
||||
},
|
||||
{
|
||||
"name": "Familia Discovery",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Familia Discovery\\b"
|
||||
},
|
||||
{
|
||||
"name": "Fatafeat",
|
||||
"country": "EG",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Fatafeat\\b"
|
||||
},
|
||||
{
|
||||
"name": "FEM",
|
||||
"country": "NO",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^FEM\\b"
|
||||
},
|
||||
{
|
||||
"name": "Fine Living",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Fine Living\\b"
|
||||
},
|
||||
{
|
||||
"name": "Flow Sports",
|
||||
"country": "UK",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Flow Sports\\b"
|
||||
},
|
||||
{
|
||||
"name": "Food Network",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Food Network\\b"
|
||||
},
|
||||
{
|
||||
"name": "food tv",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^food( |)tv\\b"
|
||||
},
|
||||
{
|
||||
"name": "Fox Sports",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Fox Sports\\b"
|
||||
},
|
||||
{
|
||||
"name": "Frisbee",
|
||||
"country": "IT",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Frisbee\\b"
|
||||
},
|
||||
{
|
||||
"name": "Futbol",
|
||||
"country": "TJ",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^(Futbol|Football TV|ТВ Футбол|Футбол)\\b"
|
||||
},
|
||||
{
|
||||
"name": "GTV Sports",
|
||||
"country": "GH",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^GTV Sports\\b"
|
||||
},
|
||||
{
|
||||
"name": "Giallo",
|
||||
"country": "IT",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Giallo\\b"
|
||||
},
|
||||
{
|
||||
"name": "GolfTV",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Golf( |)TV\\b"
|
||||
},
|
||||
{
|
||||
"name": "HGTV",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^HGTV\\b"
|
||||
},
|
||||
{
|
||||
"name": "Investigation Discovery",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^(Investigation Discovery|ID Investigation Discovery|ID Investigation|ID)\\b"
|
||||
},
|
||||
{
|
||||
"name": "K2",
|
||||
"country": "IT",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^K2\\b"
|
||||
},
|
||||
{
|
||||
"name": "Living Channel",
|
||||
"country": "NZ",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Living Channel\\b"
|
||||
},
|
||||
{
|
||||
"name": "LookSport",
|
||||
"country": "RO",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Look( |)Sport\\b"
|
||||
},
|
||||
{
|
||||
"name": "Mango",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Mango\\b"
|
||||
},
|
||||
{
|
||||
"name": "Match!",
|
||||
"country": "RU",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^(Match|Матч)\\b"
|
||||
},
|
||||
{
|
||||
"name": "Motortrend",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Motortrend\\b"
|
||||
},
|
||||
{
|
||||
"name": "Mola TV",
|
||||
"country": "ID",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Mola TV($| .*)\\b"
|
||||
},
|
||||
{
|
||||
"name": "Movistar Liga de Campeones",
|
||||
"country": "ES",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Movistar Liga de Campeones [1-8]\\b"
|
||||
},
|
||||
{
|
||||
"name": "Nova Sport",
|
||||
"country": "CZ",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Nova Sport [1-3]\\b"
|
||||
},
|
||||
{
|
||||
"name": "Nova Sports",
|
||||
"country": "GR",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Nova Sports [1-6]\\b"
|
||||
},
|
||||
{
|
||||
"name": "Nove",
|
||||
"country": "IT",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Nove($| .*)\\b"
|
||||
},
|
||||
{
|
||||
"name": "PPTV HD 36",
|
||||
"country": "TH",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^PPTV($| HD)\\b"
|
||||
},
|
||||
{
|
||||
"name": "One",
|
||||
"country": "IL",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^One\\b"
|
||||
},
|
||||
{
|
||||
"name": "OWN",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^(OWN|Oprah)\\b"
|
||||
},
|
||||
{
|
||||
"name": "Quest Red",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Quest Red\\b"
|
||||
},
|
||||
{
|
||||
"name": "Quest",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Quest\\b"
|
||||
},
|
||||
{
|
||||
"name": "Real Time",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Real Time\\b"
|
||||
},
|
||||
{
|
||||
"name": "SABC Sport ",
|
||||
"country": "ZA",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^SABC Sport\\b"
|
||||
},
|
||||
{
|
||||
"name": "Setanta Sports",
|
||||
"country": "IE",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Setanta Sports($| .*)\\b"
|
||||
},
|
||||
{
|
||||
"name": "Sky Sports",
|
||||
"country": "UK",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Sky Sports\\b"
|
||||
},
|
||||
{
|
||||
"name": "Sky Sport Bundesliga",
|
||||
"country": "DE",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Sky Sport Bundesliga [0-9]+\\b"
|
||||
},
|
||||
{
|
||||
"name": "Sky TG24",
|
||||
"country": "IT",
|
||||
"reference": "https://github.com/iptv-org/iptv/pull/2294",
|
||||
"regex": "^Sky TG24\\b"
|
||||
},
|
||||
{
|
||||
"name": "Sony Ten",
|
||||
"country": "IN",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Sony Ten [1-4]\\b"
|
||||
},
|
||||
{
|
||||
"name": "Spíler TV",
|
||||
"country": "HU",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Spíler( |[1-2] )TV\\b"
|
||||
},
|
||||
{
|
||||
"name": "Šport TV",
|
||||
"country": "SI",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^(Šport|Sport) TV\\b"
|
||||
},
|
||||
{
|
||||
"name": "Sport Klub",
|
||||
"country": "HU",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^(Sport Klub|SK[1-9])\\b"
|
||||
},
|
||||
{
|
||||
"name": "SportsNet",
|
||||
"country": "CA",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^SportsNet\\b"
|
||||
},
|
||||
{
|
||||
"name": "StarHub TV",
|
||||
"country": "SG",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^StarHub TV\\b"
|
||||
},
|
||||
{
|
||||
"name": "StarSat",
|
||||
"country": "ZA",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^StarSat\\b"
|
||||
},
|
||||
{
|
||||
"name": "StarTimes TV",
|
||||
"country": "MZ",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^StarTimes TV\\b"
|
||||
},
|
||||
{
|
||||
"name": "SuperSport",
|
||||
"country": "AL",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^SuperSport [1-7]\\b"
|
||||
},
|
||||
{
|
||||
"name": "Tivibu Spor",
|
||||
"country": "TR",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Tivibu Spor\\b"
|
||||
},
|
||||
{
|
||||
"name": "TLC",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^TLC\\b"
|
||||
},
|
||||
{
|
||||
"name": "Trvl Channel",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Trvl Channel\\b"
|
||||
},
|
||||
{
|
||||
"name": "TSN",
|
||||
"country": "MT",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^TSN\\b"
|
||||
},
|
||||
{
|
||||
"name": "TTV",
|
||||
"country": "PL",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^TTV\\b"
|
||||
},
|
||||
{
|
||||
"name": "TV Norge",
|
||||
"country": "NO",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^TV Norge\\b"
|
||||
},
|
||||
{
|
||||
"name": "TV Varzish",
|
||||
"country": "TJ",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^(TV Varzish|Varzish TV|Варзиш ТВ)\\b"
|
||||
},
|
||||
{
|
||||
"name": "TV3 Sport",
|
||||
"country": "DK",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^TV( |)3 Sport\\b"
|
||||
},
|
||||
{
|
||||
"name": "tvN Asia",
|
||||
"country": "KR",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^tvN($| Asia)\\b"
|
||||
},
|
||||
{
|
||||
"name": "Tvn 24 Bis",
|
||||
"country": "PL",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Tvn(| )24 Bis\\b"
|
||||
},
|
||||
{
|
||||
"name": "TVN 24",
|
||||
"country": "PL",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^TVN 24\\b"
|
||||
},
|
||||
{
|
||||
"name": "Tvn 7",
|
||||
"country": "PL",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Tvn 7\\b"
|
||||
},
|
||||
{
|
||||
"name": "TVN Extra",
|
||||
"country": "PL",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^TVN Extra\\b"
|
||||
},
|
||||
{
|
||||
"name": "TVN Fabula",
|
||||
"country": "PL",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^TVN Fabula\\b"
|
||||
},
|
||||
{
|
||||
"name": "TVN Meteo",
|
||||
"country": "PL",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^TVN Meteo\\b"
|
||||
},
|
||||
{
|
||||
"name": "TVN Style",
|
||||
"country": "PL",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^TVN Style\\b"
|
||||
},
|
||||
{
|
||||
"name": "TVN Turbo",
|
||||
"country": "PL",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^TVN Turbo\\b"
|
||||
},
|
||||
{
|
||||
"name": "TVN Warszawa",
|
||||
"country": "PL",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^TVN Warszawa\\b"
|
||||
},
|
||||
{
|
||||
"name": "TVN",
|
||||
"country": "PL",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^TVN\\b"
|
||||
},
|
||||
{
|
||||
"name": "V Sport",
|
||||
"country": "NO",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^V Sport\\b"
|
||||
},
|
||||
{
|
||||
"name": "Vox",
|
||||
"country": "NO",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^Vox\\b"
|
||||
},
|
||||
{
|
||||
"name": "VTV Cab",
|
||||
"country": "KR",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^VTV( |)Cab\\b"
|
||||
},
|
||||
{
|
||||
"name": "World Discovery",
|
||||
"country": "US",
|
||||
"reference": "https://github.com/iptv-org/iptv/issues/1831",
|
||||
"regex": "^World Discovery\\b"
|
||||
},
|
||||
{
|
||||
"name": "Xee",
|
||||
"country": "DK",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^Xee\\b"
|
||||
},
|
||||
{
|
||||
"name": "XtvN",
|
||||
"country": "KR",
|
||||
"reference": "https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md",
|
||||
"regex": "^X( |)tvN\\b"
|
||||
}
|
||||
]
|
|
@ -1,147 +1,147 @@
|
|||
[
|
||||
{
|
||||
{
|
||||
"auto": {
|
||||
"name": "Auto",
|
||||
"id": "auto",
|
||||
"slug": "auto",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"animation": {
|
||||
"name": "Animation",
|
||||
"id": "animation",
|
||||
"slug": "animation",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"business": {
|
||||
"name": "Business",
|
||||
"id": "business",
|
||||
"slug": "business",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"classic": {
|
||||
"name": "Classic",
|
||||
"id": "classic",
|
||||
"slug": "classic",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"comedy": {
|
||||
"name": "Comedy",
|
||||
"id": "comedy",
|
||||
"slug": "comedy",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"cooking": {
|
||||
"name": "Cooking",
|
||||
"id": "cooking",
|
||||
"slug": "cooking",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"culture": {
|
||||
"name": "Culture",
|
||||
"id": "culture",
|
||||
"slug": "culture",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"documentary": {
|
||||
"name": "Documentary",
|
||||
"id": "documentary",
|
||||
"slug": "documentary",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"education": {
|
||||
"name": "Education",
|
||||
"id": "education",
|
||||
"slug": "education",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"entertainment": {
|
||||
"name": "Entertainment",
|
||||
"id": "entertainment",
|
||||
"slug": "entertainment",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"family": {
|
||||
"name": "Family",
|
||||
"id": "family",
|
||||
"slug": "family",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"general": {
|
||||
"name": "General",
|
||||
"id": "general",
|
||||
"slug": "general",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"kids": {
|
||||
"name": "Kids",
|
||||
"id": "kids",
|
||||
"slug": "kids",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"legislative": {
|
||||
"name": "Legislative",
|
||||
"id": "legislative",
|
||||
"slug": "legislative",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"lifestyle": {
|
||||
"name": "Lifestyle",
|
||||
"id": "lifestyle",
|
||||
"slug": "lifestyle",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"local": {
|
||||
"name": "Local",
|
||||
"id": "local",
|
||||
"slug": "local",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"movies": {
|
||||
"name": "Movies",
|
||||
"id": "movies",
|
||||
"slug": "movies",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"music": {
|
||||
"name": "Music",
|
||||
"id": "music",
|
||||
"slug": "music",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"news": {
|
||||
"name": "News",
|
||||
"id": "news",
|
||||
"slug": "news",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"outdoor": {
|
||||
"name": "Outdoor",
|
||||
"id": "outdoor",
|
||||
"slug": "outdoor",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"relax": {
|
||||
"name": "Relax",
|
||||
"id": "relax",
|
||||
"slug": "relax",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"religious": {
|
||||
"name": "Religious",
|
||||
"id": "religious",
|
||||
"slug": "religious",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"series": {
|
||||
"name": "Series",
|
||||
"id": "series",
|
||||
"slug": "series",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"science": {
|
||||
"name": "Science",
|
||||
"id": "science",
|
||||
"slug": "science",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"shop": {
|
||||
"name": "Shop",
|
||||
"id": "shop",
|
||||
"slug": "shop",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"sports": {
|
||||
"name": "Sports",
|
||||
"id": "sports",
|
||||
"slug": "sports",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"travel": {
|
||||
"name": "Travel",
|
||||
"id": "travel",
|
||||
"slug": "travel",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"weather": {
|
||||
"name": "Weather",
|
||||
"id": "weather",
|
||||
"slug": "weather",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"xxx": {
|
||||
"name": "XXX",
|
||||
"id": "xxx",
|
||||
"slug": "xxx",
|
||||
"nsfw": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"AFR": {
|
||||
"name": "Africa",
|
||||
"codes": [
|
||||
"code": "AFR",
|
||||
"country_codes": [
|
||||
"AO",
|
||||
"BF",
|
||||
"BI",
|
||||
|
@ -65,7 +66,8 @@
|
|||
},
|
||||
"AMER": {
|
||||
"name": "Americas",
|
||||
"codes": [
|
||||
"code": "AMER",
|
||||
"country_codes": [
|
||||
"AG",
|
||||
"AI",
|
||||
"AR",
|
||||
|
@ -126,7 +128,8 @@
|
|||
},
|
||||
"APAC": {
|
||||
"name": "Asia-Pacific",
|
||||
"codes": [
|
||||
"code": "APAC",
|
||||
"country_codes": [
|
||||
"AF",
|
||||
"AS",
|
||||
"AU",
|
||||
|
@ -181,7 +184,8 @@
|
|||
},
|
||||
"ARAB": {
|
||||
"name": "Arab world",
|
||||
"codes": [
|
||||
"code": "ARAB",
|
||||
"country_codes": [
|
||||
"AE",
|
||||
"BH",
|
||||
"DJ",
|
||||
|
@ -208,7 +212,8 @@
|
|||
},
|
||||
"ASIA": {
|
||||
"name": "Asia",
|
||||
"codes": [
|
||||
"code": "ASIA",
|
||||
"country_codes": [
|
||||
"AE",
|
||||
"AF",
|
||||
"AM",
|
||||
|
@ -263,7 +268,8 @@
|
|||
},
|
||||
"CARIB": {
|
||||
"name": "Caribbean",
|
||||
"codes": [
|
||||
"code": "CARIB",
|
||||
"country_codes": [
|
||||
"AG",
|
||||
"AI",
|
||||
"AW",
|
||||
|
@ -295,15 +301,18 @@
|
|||
},
|
||||
"CAS": {
|
||||
"name": "Central Asia",
|
||||
"codes": ["KG", "KZ", "TJ", "TM", "UZ"]
|
||||
"code": "CAS",
|
||||
"country_codes": ["KG", "KZ", "TJ", "TM", "UZ"]
|
||||
},
|
||||
"CIS": {
|
||||
"name": "Commonwealth of Independent States",
|
||||
"codes": ["AM", "AZ", "BY", "KG", "KZ", "MD", "RU", "TJ", "UZ"]
|
||||
"code": "CIS",
|
||||
"country_codes": ["AM", "AZ", "BY", "KG", "KZ", "MD", "RU", "TJ", "UZ"]
|
||||
},
|
||||
"EMEA": {
|
||||
"name": "Europe, Middle East and Africa",
|
||||
"codes": [
|
||||
"name": "Europe, the Middle East and Africa",
|
||||
"code": "EMEA",
|
||||
"country_codes": [
|
||||
"AD",
|
||||
"AE",
|
||||
"AL",
|
||||
|
@ -430,7 +439,8 @@
|
|||
},
|
||||
"EUR": {
|
||||
"name": "Europe",
|
||||
"codes": [
|
||||
"code": "EUR",
|
||||
"country_codes": [
|
||||
"AD",
|
||||
"AL",
|
||||
"AM",
|
||||
|
@ -485,7 +495,8 @@
|
|||
},
|
||||
"HISPAM": {
|
||||
"name": "Hispanic America",
|
||||
"codes": [
|
||||
"code": "HISPAM",
|
||||
"country_codes": [
|
||||
"AR",
|
||||
"BO",
|
||||
"CL",
|
||||
|
@ -509,7 +520,8 @@
|
|||
},
|
||||
"LATAM": {
|
||||
"name": "Latin America",
|
||||
"codes": [
|
||||
"code": "LATAM",
|
||||
"country_codes": [
|
||||
"AR",
|
||||
"BL",
|
||||
"BO",
|
||||
|
@ -538,90 +550,15 @@
|
|||
"VE"
|
||||
]
|
||||
},
|
||||
"MAGHRIB": {
|
||||
"name": "Maghrib",
|
||||
"codes": ["DZ", "LY", "MA", "MR", "TN"]
|
||||
},
|
||||
"MEA": {
|
||||
"name": "Middle East and Africa",
|
||||
"codes": [
|
||||
"AE",
|
||||
"AO",
|
||||
"BF",
|
||||
"BH",
|
||||
"BI",
|
||||
"BJ",
|
||||
"BW",
|
||||
"CD",
|
||||
"CF",
|
||||
"CG",
|
||||
"CI",
|
||||
"CM",
|
||||
"CV",
|
||||
"DJ",
|
||||
"DZ",
|
||||
"EG",
|
||||
"EH",
|
||||
"ER",
|
||||
"ET",
|
||||
"GA",
|
||||
"GH",
|
||||
"GM",
|
||||
"GN",
|
||||
"GQ",
|
||||
"GW",
|
||||
"IQ",
|
||||
"IR",
|
||||
"JO",
|
||||
"KE",
|
||||
"KM",
|
||||
"KW",
|
||||
"LB",
|
||||
"LR",
|
||||
"LS",
|
||||
"LY",
|
||||
"MA",
|
||||
"MG",
|
||||
"ML",
|
||||
"MR",
|
||||
"MU",
|
||||
"MW",
|
||||
"MZ",
|
||||
"NA",
|
||||
"NE",
|
||||
"NG",
|
||||
"OM",
|
||||
"PS",
|
||||
"QA",
|
||||
"RE",
|
||||
"RW",
|
||||
"SA",
|
||||
"SC",
|
||||
"SD",
|
||||
"SH",
|
||||
"SL",
|
||||
"SN",
|
||||
"SO",
|
||||
"SS",
|
||||
"ST",
|
||||
"SY",
|
||||
"SZ",
|
||||
"TD",
|
||||
"TF",
|
||||
"TG",
|
||||
"TN",
|
||||
"TZ",
|
||||
"UG",
|
||||
"YE",
|
||||
"YT",
|
||||
"ZA",
|
||||
"ZM",
|
||||
"ZW"
|
||||
]
|
||||
"MAGHREB": {
|
||||
"name": "Maghreb",
|
||||
"code": "MAGHREB",
|
||||
"country_codes": ["DZ", "LY", "MA", "MR", "TN"]
|
||||
},
|
||||
"MENA": {
|
||||
"name": "Middle East and North Africa",
|
||||
"codes": [
|
||||
"code": "MENA",
|
||||
"country_codes": [
|
||||
"AE",
|
||||
"BH",
|
||||
"DJ",
|
||||
|
@ -645,43 +582,10 @@
|
|||
"YE"
|
||||
]
|
||||
},
|
||||
"NORD": {
|
||||
"name": "Nordics",
|
||||
"codes": ["AX", "DK", "FO", "FI", "IS", "NO", "SE"]
|
||||
},
|
||||
"OCE": {
|
||||
"name": "Oceania",
|
||||
"codes": [
|
||||
"AS",
|
||||
"AU",
|
||||
"CK",
|
||||
"FJ",
|
||||
"FM",
|
||||
"GU",
|
||||
"KI",
|
||||
"MH",
|
||||
"MP",
|
||||
"NC",
|
||||
"NF",
|
||||
"NR",
|
||||
"NU",
|
||||
"NZ",
|
||||
"PF",
|
||||
"PG",
|
||||
"PN",
|
||||
"PW",
|
||||
"SB",
|
||||
"TK",
|
||||
"TO",
|
||||
"TV",
|
||||
"VU",
|
||||
"WF",
|
||||
"WS"
|
||||
]
|
||||
},
|
||||
"MIDEAST": {
|
||||
"name": "Middle East",
|
||||
"codes": [
|
||||
"code": "MIDEAST",
|
||||
"country_codes": [
|
||||
"AE",
|
||||
"BH",
|
||||
"CY",
|
||||
|
@ -703,7 +607,8 @@
|
|||
},
|
||||
"NORAM": {
|
||||
"name": "North America",
|
||||
"codes": [
|
||||
"code": "NORAM",
|
||||
"country_codes": [
|
||||
"AG",
|
||||
"AI",
|
||||
"AW",
|
||||
|
@ -746,13 +651,51 @@
|
|||
"VI"
|
||||
]
|
||||
},
|
||||
"NORD": {
|
||||
"name": "Nordics",
|
||||
"code": "NORD",
|
||||
"country_codes": ["AX", "DK", "FO", "FI", "IS", "NO", "SE"]
|
||||
},
|
||||
"OCE": {
|
||||
"name": "Oceania",
|
||||
"code": "OCE",
|
||||
"country_codes": [
|
||||
"AS",
|
||||
"AU",
|
||||
"CK",
|
||||
"FJ",
|
||||
"FM",
|
||||
"GU",
|
||||
"KI",
|
||||
"MH",
|
||||
"MP",
|
||||
"NC",
|
||||
"NF",
|
||||
"NR",
|
||||
"NU",
|
||||
"NZ",
|
||||
"PF",
|
||||
"PG",
|
||||
"PN",
|
||||
"PW",
|
||||
"SB",
|
||||
"TK",
|
||||
"TO",
|
||||
"TV",
|
||||
"VU",
|
||||
"WF",
|
||||
"WS"
|
||||
]
|
||||
},
|
||||
"SAS": {
|
||||
"name": "South Asia",
|
||||
"codes": ["AF", "BD", "BT", "IN", "LK", "MV", "NP", "PK"]
|
||||
"code": "SAS",
|
||||
"country_codes": ["AF", "BD", "BT", "IN", "LK", "MV", "NP", "PK"]
|
||||
},
|
||||
"SSA": {
|
||||
"name": "Sub-Saharan Africa",
|
||||
"codes": [
|
||||
"code": "SSA",
|
||||
"country_codes": [
|
||||
"AO",
|
||||
"BF",
|
||||
"BI",
|
||||
|
@ -806,7 +749,8 @@
|
|||
},
|
||||
"WAFR": {
|
||||
"name": "West Africa",
|
||||
"codes": [
|
||||
"code": "WAFR",
|
||||
"country_codes": [
|
||||
"BF",
|
||||
"BJ",
|
||||
"CI",
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"online": {
|
||||
"label": "",
|
||||
"code": "online",
|
||||
"level": 1
|
||||
},
|
||||
"geo_blocked": {
|
||||
"label": "Geo-blocked",
|
||||
"code": "geo_blocked",
|
||||
"level": 2
|
||||
},
|
||||
"not_247": {
|
||||
"label": "Not 24/7",
|
||||
"code": "not_247",
|
||||
"level": 3
|
||||
},
|
||||
"timeout": {
|
||||
"label": "Timeout",
|
||||
"code": "timeout",
|
||||
"level": 4
|
||||
},
|
||||
"offline": {
|
||||
"label": "Offline",
|
||||
"code": "offline",
|
||||
"level": 5
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
const blacklist = require('./data/blacklist.json')
|
||||
const parser = require('./helpers/parser')
|
||||
const file = require('./helpers/file')
|
||||
const log = require('./helpers/log')
|
||||
|
||||
async function main() {
|
||||
log.start()
|
||||
|
||||
const files = await file.list()
|
||||
if (!files.length) log.print(`No files is selected\n`)
|
||||
for (const file of files) {
|
||||
log.print(`\nProcessing '${file}'...`)
|
||||
await parser
|
||||
.parsePlaylist(file)
|
||||
.then(removeBlacklisted)
|
||||
.then(p => p.save())
|
||||
}
|
||||
|
||||
log.print('\n')
|
||||
log.finish()
|
||||
}
|
||||
|
||||
function removeBlacklisted(playlist) {
|
||||
const channels = playlist.channels.filter(channel => {
|
||||
return !blacklist.find(item => {
|
||||
const regexp = new RegExp(item.regex, 'i')
|
||||
const hasSameName = regexp.test(channel.name)
|
||||
const fromSameCountry = playlist.country.code === item.country
|
||||
|
||||
return hasSameName && fromSameCountry
|
||||
})
|
||||
})
|
||||
|
||||
if (playlist.channels.length !== channels.length) {
|
||||
log.print(`updated`)
|
||||
playlist.channels = channels
|
||||
playlist.updated = true
|
||||
}
|
||||
|
||||
return playlist
|
||||
}
|
||||
|
||||
main()
|
|
@ -1,307 +0,0 @@
|
|||
const axios = require('axios')
|
||||
const { program } = require('commander')
|
||||
const normalize = require('normalize-url')
|
||||
const IPTVChecker = require('iptv-checker')
|
||||
const parser = require('./helpers/parser')
|
||||
const utils = require('./helpers/utils')
|
||||
const file = require('./helpers/file')
|
||||
const log = require('./helpers/log')
|
||||
const epg = require('./helpers/epg')
|
||||
|
||||
const ignoreStatus = ['Geo-blocked']
|
||||
|
||||
program
|
||||
.usage('[OPTIONS]...')
|
||||
.option('--debug', 'Enable debug mode')
|
||||
.option('--offline', 'Enable offline mode')
|
||||
.option('-d, --delay <delay>', 'Set delay for each request', parseNumber, 0)
|
||||
.option('-t, --timeout <timeout>', 'Set timeout for each request', parseNumber, 5000)
|
||||
.option('-c, --country <country>', 'Comma-separated list of country codes', '')
|
||||
.option('-e, --exclude <exclude>', 'Comma-separated list of country codes to be excluded', '')
|
||||
.parse(process.argv)
|
||||
|
||||
const config = program.opts()
|
||||
const checker = new IPTVChecker({
|
||||
timeout: config.timeout
|
||||
})
|
||||
|
||||
let buffer, origins
|
||||
async function main() {
|
||||
log.start()
|
||||
|
||||
const include = config.country.split(',').filter(i => i)
|
||||
const exclude = config.exclude.split(',').filter(i => i)
|
||||
let files = await file.list(include, exclude)
|
||||
if (!files.length) log.print(`No files is selected\n`)
|
||||
for (const file of files) {
|
||||
await parser.parsePlaylist(file).then(updatePlaylist).then(savePlaylist)
|
||||
}
|
||||
|
||||
log.finish()
|
||||
}
|
||||
|
||||
function savePlaylist(playlist) {
|
||||
if (file.read(playlist.url) !== playlist.toString()) {
|
||||
log.print(`File '${playlist.url}' has been updated\n`)
|
||||
playlist.updated = true
|
||||
}
|
||||
|
||||
playlist.save()
|
||||
}
|
||||
|
||||
async function updatePlaylist(playlist) {
|
||||
const total = playlist.channels.length
|
||||
log.print(`Processing '${playlist.url}'...\n`)
|
||||
|
||||
let channels = {}
|
||||
let codes = {}
|
||||
if (!config.offline) {
|
||||
channels = await loadChannelsJson()
|
||||
codes = await loadCodes()
|
||||
}
|
||||
|
||||
buffer = {}
|
||||
origins = {}
|
||||
for (const [i, channel] of playlist.channels.entries()) {
|
||||
const curr = i + 1
|
||||
updateTvgName(channel)
|
||||
updateTvgId(channel, playlist)
|
||||
updateTvgCountry(channel)
|
||||
normalizeUrl(channel)
|
||||
|
||||
const data = channels[channel.tvg.id]
|
||||
const epgData = codes[channel.tvg.id]
|
||||
updateLogo(channel, data, epgData)
|
||||
updateGroupTitle(channel, data)
|
||||
updateTvgLanguage(channel, data)
|
||||
|
||||
if (config.offline || ignoreStatus.includes(channel.status)) {
|
||||
continue
|
||||
}
|
||||
|
||||
await checker
|
||||
.checkStream(channel.data)
|
||||
.then(parseResult)
|
||||
.then(result => {
|
||||
updateStatus(channel, result.status)
|
||||
if (result.status === 'online') {
|
||||
buffer[i] = result
|
||||
updateOrigins(channel, result.requests)
|
||||
updateResolution(channel, result.resolution)
|
||||
} else {
|
||||
buffer[i] = null
|
||||
if (config.debug) {
|
||||
log.print(` INFO: ${channel.url} (${result.error})\n`)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
buffer[i] = null
|
||||
if (config.debug) {
|
||||
log.print(` ERR: ${channel.data.url} (${err.message})\n`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for (const [i, channel] of playlist.channels.entries()) {
|
||||
if (!buffer[i]) continue
|
||||
const { requests } = buffer[i]
|
||||
updateUrl(channel, requests)
|
||||
}
|
||||
|
||||
return playlist
|
||||
}
|
||||
|
||||
function updateOrigins(channel, requests) {
|
||||
if (!requests) return
|
||||
const origin = new URL(channel.url)
|
||||
const target = new URL(requests[0])
|
||||
const type = origin.host === target.host ? 'origin' : 'redirect'
|
||||
requests.forEach(url => {
|
||||
const key = utils.removeProtocol(url)
|
||||
if (!origins[key] && type === 'origin') {
|
||||
origins[key] = channel.url
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function updateStatus(channel, status) {
|
||||
switch (status) {
|
||||
case 'online':
|
||||
if (channel.status !== 'Not 24/7')
|
||||
channel.status = channel.status === 'Offline' ? 'Not 24/7' : null
|
||||
break
|
||||
case 'error_403':
|
||||
if (!channel.status) channel.status = 'Geo-blocked'
|
||||
break
|
||||
case 'offline':
|
||||
if (channel.status !== 'Not 24/7') channel.status = 'Offline'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function updateResolution(channel, resolution) {
|
||||
if (!channel.resolution.height && resolution) {
|
||||
channel.resolution = resolution
|
||||
}
|
||||
}
|
||||
|
||||
function updateUrl(channel, requests) {
|
||||
for (const request of requests) {
|
||||
let key = utils.removeProtocol(channel.url)
|
||||
if (origins[key]) {
|
||||
channel.updateUrl(origins[key])
|
||||
break
|
||||
}
|
||||
|
||||
key = utils.removeProtocol(request)
|
||||
if (origins[key]) {
|
||||
channel.updateUrl(origins[key])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseResult(result) {
|
||||
return {
|
||||
status: parseStatus(result.status),
|
||||
resolution: result.status.ok ? parseResolution(result.status.metadata.streams) : null,
|
||||
requests: result.status.ok ? parseRequests(result.status.metadata.requests) : [],
|
||||
error: !result.status.ok ? result.status.reason : null
|
||||
}
|
||||
}
|
||||
|
||||
function parseStatus(status) {
|
||||
if (status.ok) {
|
||||
return 'online'
|
||||
} else if (status.reason.includes('timed out')) {
|
||||
return 'timeout'
|
||||
} else if (status.reason.includes('403')) {
|
||||
return 'error_403'
|
||||
} else if (status.reason.includes('not one of 40{0,1,3,4}')) {
|
||||
return 'error_40x' // 402, 451
|
||||
} else {
|
||||
return 'offline'
|
||||
}
|
||||
}
|
||||
|
||||
function parseResolution(streams) {
|
||||
const resolution = streams
|
||||
.filter(stream => stream.codec_type === 'video')
|
||||
.reduce(
|
||||
(acc, curr) => {
|
||||
if (curr.height > acc.height) return { width: curr.width, height: curr.height }
|
||||
return acc
|
||||
},
|
||||
{ width: 0, height: 0 }
|
||||
)
|
||||
|
||||
return resolution.width > 0 && resolution.height > 0 ? resolution : null
|
||||
}
|
||||
|
||||
function parseRequests(requests) {
|
||||
requests = requests.map(r => r.url)
|
||||
requests.shift()
|
||||
|
||||
return requests
|
||||
}
|
||||
|
||||
function updateTvgName(channel) {
|
||||
if (!channel.tvg.name) {
|
||||
channel.tvg.name = channel.name.replace(/\"/gi, '')
|
||||
}
|
||||
}
|
||||
|
||||
function updateTvgId(channel, playlist) {
|
||||
const code = playlist.country.code
|
||||
if (!channel.tvg.id && channel.tvg.name) {
|
||||
const id = utils.name2id(channel.tvg.name)
|
||||
channel.tvg.id = id ? `${id}.${code}` : ''
|
||||
}
|
||||
}
|
||||
|
||||
function updateTvgCountry(channel) {
|
||||
if (!channel.countries.length && channel.tvg.id) {
|
||||
const code = channel.tvg.id.split('.')[1] || null
|
||||
const name = utils.code2name(code)
|
||||
channel.countries = name ? [{ code, name }] : []
|
||||
channel.tvg.country = channel.countries.map(c => c.code.toUpperCase()).join(';')
|
||||
}
|
||||
}
|
||||
|
||||
function updateLogo(channel, data, epgData) {
|
||||
if (!channel.logo) {
|
||||
if (data && data.logo) {
|
||||
channel.logo = data.logo
|
||||
} else if (epgData && epgData.logo) {
|
||||
channel.logo = epgData.logo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateTvgLanguage(channel, data) {
|
||||
if (!channel.tvg.language) {
|
||||
if (data && data.languages.length) {
|
||||
channel.tvg.language = data.languages.map(l => l.name).join(';')
|
||||
} else if (channel.countries.length) {
|
||||
const countryCode = channel.countries[0].code
|
||||
channel.tvg.language = utils.country2language(countryCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateGroupTitle(channel, data) {
|
||||
if (!channel.group.title) {
|
||||
if (channel.category) {
|
||||
channel.group.title = channel.category
|
||||
} else if (data && data.category) {
|
||||
channel.group.title = data.category
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeUrl(channel) {
|
||||
const normalized = normalize(channel.url, { stripWWW: false })
|
||||
const decoded = decodeURIComponent(normalized).replace(/\s/g, '+')
|
||||
channel.updateUrl(decoded)
|
||||
}
|
||||
|
||||
function parseNumber(str) {
|
||||
return parseInt(str)
|
||||
}
|
||||
|
||||
function loadCodes() {
|
||||
return epg.codes
|
||||
.load()
|
||||
.then(codes => {
|
||||
let output = {}
|
||||
codes.forEach(item => {
|
||||
output[item['tvg_id']] = item
|
||||
})
|
||||
return output
|
||||
})
|
||||
.catch(console.log)
|
||||
}
|
||||
|
||||
function loadChannelsJson() {
|
||||
return axios
|
||||
.get('https://iptv-org.github.io/iptv/channels.json')
|
||||
.then(r => r.data)
|
||||
.then(channels => {
|
||||
let output = {}
|
||||
channels.forEach(channel => {
|
||||
const item = output[channel.tvg.id]
|
||||
if (!item) {
|
||||
output[channel.tvg.id] = channel
|
||||
} else {
|
||||
item.logo = item.logo || channel.logo
|
||||
item.languages = item.languages.length ? item.languages : channel.languages
|
||||
item.category = item.category || channel.category
|
||||
}
|
||||
})
|
||||
return output
|
||||
})
|
||||
.catch(console.log)
|
||||
}
|
||||
|
||||
main()
|
|
@ -1,232 +0,0 @@
|
|||
const file = require('./helpers/file')
|
||||
const log = require('./helpers/log')
|
||||
const db = require('./helpers/db')
|
||||
|
||||
const ROOT_DIR = './.gh-pages'
|
||||
|
||||
async function main() {
|
||||
await loadDatabase()
|
||||
createRootDirectory()
|
||||
createNoJekyllFile()
|
||||
generateIndex()
|
||||
generateCategoryIndex()
|
||||
generateCountryIndex()
|
||||
generateLanguageIndex()
|
||||
generateCategories()
|
||||
generateCountries()
|
||||
generateLanguages()
|
||||
generateChannelsJson()
|
||||
showResults()
|
||||
}
|
||||
|
||||
async function loadDatabase() {
|
||||
log.print('Loading database...\n')
|
||||
await db.load()
|
||||
}
|
||||
|
||||
function createRootDirectory() {
|
||||
log.print('Creating .gh-pages folder...\n')
|
||||
file.createDir(ROOT_DIR)
|
||||
}
|
||||
|
||||
function createNoJekyllFile() {
|
||||
log.print('Creating .nojekyll...\n')
|
||||
file.create(`${ROOT_DIR}/.nojekyll`)
|
||||
}
|
||||
|
||||
function generateIndex() {
|
||||
log.print('Generating index.m3u...\n')
|
||||
const channels = db.channels
|
||||
.sortBy(['name', 'status', 'resolution.height', 'url'], ['asc', 'asc', 'desc', 'asc'])
|
||||
.removeDuplicates()
|
||||
.removeOffline()
|
||||
.get()
|
||||
const guides = channels.map(channel => channel.tvg.url)
|
||||
|
||||
const filename = `${ROOT_DIR}/index.m3u`
|
||||
const urlTvg = generateUrlTvg(guides)
|
||||
file.create(filename, `#EXTM3U url-tvg="${urlTvg}"\n`)
|
||||
|
||||
const nsfwFilename = `${ROOT_DIR}/index.nsfw.m3u`
|
||||
file.create(nsfwFilename, `#EXTM3U url-tvg="${urlTvg}"\n`)
|
||||
|
||||
for (const channel of channels) {
|
||||
if (!channel.isNSFW()) {
|
||||
file.append(filename, channel.toString())
|
||||
}
|
||||
file.append(nsfwFilename, channel.toString())
|
||||
}
|
||||
}
|
||||
|
||||
function generateCategoryIndex() {
|
||||
log.print('Generating index.category.m3u...\n')
|
||||
const channels = db.channels
|
||||
.sortBy(
|
||||
['category', 'name', 'status', 'resolution.height', 'url'],
|
||||
['asc', 'asc', 'asc', 'desc', 'asc']
|
||||
)
|
||||
.removeDuplicates()
|
||||
.removeOffline()
|
||||
.get()
|
||||
const guides = channels.map(channel => channel.tvg.url)
|
||||
|
||||
const filename = `${ROOT_DIR}/index.category.m3u`
|
||||
const urlTvg = generateUrlTvg(guides)
|
||||
file.create(filename, `#EXTM3U url-tvg="${urlTvg}"\n`)
|
||||
|
||||
for (const channel of channels) {
|
||||
file.append(filename, channel.toString())
|
||||
}
|
||||
}
|
||||
|
||||
function generateCountryIndex() {
|
||||
log.print('Generating index.country.m3u...\n')
|
||||
|
||||
const guides = []
|
||||
const lines = []
|
||||
for (const country of [{ code: 'undefined' }, ...db.countries.sortBy(['name']).all()]) {
|
||||
const channels = db.channels
|
||||
.sortBy(['name', 'status', 'resolution.height', 'url'], ['asc', 'asc', 'desc', 'asc'])
|
||||
.forCountry(country)
|
||||
.removeDuplicates()
|
||||
.removeNSFW()
|
||||
.removeOffline()
|
||||
.get()
|
||||
for (const channel of channels) {
|
||||
const groupTitle = channel.group.title
|
||||
channel.group.title = country.name || ''
|
||||
lines.push(channel.toString())
|
||||
channel.group.title = groupTitle
|
||||
guides.push(channel.tvg.url)
|
||||
}
|
||||
}
|
||||
|
||||
const filename = `${ROOT_DIR}/index.country.m3u`
|
||||
const urlTvg = generateUrlTvg(guides)
|
||||
file.create(filename, `#EXTM3U url-tvg="${urlTvg}"\n${lines.join('')}`)
|
||||
}
|
||||
|
||||
function generateLanguageIndex() {
|
||||
log.print('Generating index.language.m3u...\n')
|
||||
|
||||
const guides = []
|
||||
const lines = []
|
||||
for (const language of [{ code: 'undefined' }, ...db.languages.sortBy(['name']).all()]) {
|
||||
const channels = db.channels
|
||||
.sortBy(['name', 'status', 'resolution.height', 'url'], ['asc', 'asc', 'desc', 'asc'])
|
||||
.forLanguage(language)
|
||||
.removeDuplicates()
|
||||
.removeNSFW()
|
||||
.removeOffline()
|
||||
.get()
|
||||
for (const channel of channels) {
|
||||
const groupTitle = channel.group.title
|
||||
channel.group.title = language.name || ''
|
||||
lines.push(channel.toString())
|
||||
channel.group.title = groupTitle
|
||||
guides.push(channel.tvg.url)
|
||||
}
|
||||
}
|
||||
|
||||
const filename = `${ROOT_DIR}/index.language.m3u`
|
||||
const urlTvg = generateUrlTvg(guides)
|
||||
file.create(filename, `#EXTM3U url-tvg="${urlTvg}"\n${lines.join('')}`)
|
||||
}
|
||||
|
||||
function generateCategories() {
|
||||
log.print(`Generating /categories...\n`)
|
||||
const outputDir = `${ROOT_DIR}/categories`
|
||||
file.createDir(outputDir)
|
||||
|
||||
for (const category of [...db.categories.all(), { id: 'other' }]) {
|
||||
const channels = db.channels
|
||||
.sortBy(['name', 'status', 'resolution.height', 'url'], ['asc', 'asc', 'desc', 'asc'])
|
||||
.forCategory(category)
|
||||
.removeDuplicates()
|
||||
.removeOffline()
|
||||
.get()
|
||||
const guides = channels.map(channel => channel.tvg.url)
|
||||
|
||||
const filename = `${outputDir}/${category.id}.m3u`
|
||||
const urlTvg = generateUrlTvg(guides)
|
||||
file.create(filename, `#EXTM3U url-tvg="${urlTvg}"\n`)
|
||||
for (const channel of channels) {
|
||||
file.append(filename, channel.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generateCountries() {
|
||||
log.print(`Generating /countries...\n`)
|
||||
const outputDir = `${ROOT_DIR}/countries`
|
||||
file.createDir(outputDir)
|
||||
|
||||
for (const country of [...db.countries.all(), { code: 'undefined' }]) {
|
||||
const channels = db.channels
|
||||
.sortBy(['name', 'status', 'resolution.height', 'url'], ['asc', 'asc', 'desc', 'asc'])
|
||||
.forCountry(country)
|
||||
.removeDuplicates()
|
||||
.removeOffline()
|
||||
.removeNSFW()
|
||||
.get()
|
||||
const guides = channels.map(channel => channel.tvg.url)
|
||||
|
||||
const filename = `${outputDir}/${country.code}.m3u`
|
||||
const urlTvg = generateUrlTvg(guides)
|
||||
file.create(filename, `#EXTM3U url-tvg="${urlTvg}"\n`)
|
||||
for (const channel of channels) {
|
||||
file.append(filename, channel.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generateLanguages() {
|
||||
log.print(`Generating /languages...\n`)
|
||||
const outputDir = `${ROOT_DIR}/languages`
|
||||
file.createDir(outputDir)
|
||||
|
||||
for (const language of [...db.languages.all(), { code: 'undefined' }]) {
|
||||
const channels = db.channels
|
||||
.sortBy(['name', 'status', 'resolution.height', 'url'], ['asc', 'asc', 'desc', 'asc'])
|
||||
.forLanguage(language)
|
||||
.removeDuplicates()
|
||||
.removeOffline()
|
||||
.removeNSFW()
|
||||
.get()
|
||||
const guides = channels.map(channel => channel.tvg.url)
|
||||
|
||||
const filename = `${outputDir}/${language.code}.m3u`
|
||||
const urlTvg = generateUrlTvg(guides)
|
||||
file.create(filename, `#EXTM3U url-tvg="${urlTvg}"\n`)
|
||||
for (const channel of channels) {
|
||||
file.append(filename, channel.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generateChannelsJson() {
|
||||
log.print('Generating channels.json...\n')
|
||||
const filename = `${ROOT_DIR}/channels.json`
|
||||
const channels = db.channels
|
||||
.sortBy(['name', 'status', 'resolution.height', 'url'], ['asc', 'asc', 'desc', 'asc'])
|
||||
.get()
|
||||
.map(c => c.toObject())
|
||||
file.create(filename, JSON.stringify(channels))
|
||||
}
|
||||
|
||||
function showResults() {
|
||||
log.print(
|
||||
`Total: ${db.channels.count()} channels, ${db.countries.count()} countries, ${db.languages.count()} languages, ${db.categories.count()} categories.\n`
|
||||
)
|
||||
}
|
||||
|
||||
function generateUrlTvg(guides) {
|
||||
const output = guides.reduce((acc, curr) => {
|
||||
if (curr && !acc.includes(curr)) acc.push(curr)
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
return output.sort().join(',')
|
||||
}
|
||||
|
||||
main()
|
|
@ -1,162 +0,0 @@
|
|||
const categories = require('../data/categories')
|
||||
const utils = require('./utils')
|
||||
const file = require('./file')
|
||||
|
||||
const sfwCategories = categories.filter(c => !c.nsfw).map(c => c.name)
|
||||
const nsfwCategories = categories.filter(c => c.nsfw).map(c => c.name)
|
||||
|
||||
module.exports = class Channel {
|
||||
constructor(data) {
|
||||
this.data = data
|
||||
this.raw = data.raw
|
||||
this.tvg = data.tvg
|
||||
this.http = data.http
|
||||
this.url = data.url
|
||||
this.logo = data.tvg.logo
|
||||
this.group = data.group
|
||||
this.name = this.parseName(data.name)
|
||||
this.status = this.parseStatus(data.name)
|
||||
this.resolution = this.parseResolution(data.name)
|
||||
this.category = this.parseCategory(data.group.title)
|
||||
this.countries = this.parseCountries(data.tvg.country)
|
||||
this.languages = this.parseLanguages(data.tvg.language)
|
||||
this.hash = this.generateHash()
|
||||
}
|
||||
|
||||
generateHash() {
|
||||
return `${this.tvg.id}:${this.tvg.country}:${this.tvg.language}:${this.logo}:${this.group.title}:${this.name}`.toLowerCase()
|
||||
}
|
||||
|
||||
updateUrl(url) {
|
||||
this.url = url
|
||||
this.data.url = url
|
||||
}
|
||||
|
||||
parseName(title) {
|
||||
return title
|
||||
.trim()
|
||||
.split(' ')
|
||||
.map(s => s.trim())
|
||||
.filter(s => {
|
||||
return !/\[|\]/i.test(s) && !/\((\d+)P\)/i.test(s)
|
||||
})
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
parseStatus(title) {
|
||||
const match = title.match(/\[(.*)\]/i)
|
||||
return match ? match[1] : null
|
||||
}
|
||||
|
||||
parseResolution(title) {
|
||||
const match = title.match(/\((\d+)P\)/i)
|
||||
const height = match ? parseInt(match[1]) : null
|
||||
|
||||
return { width: null, height }
|
||||
}
|
||||
|
||||
parseCategory(string) {
|
||||
const category = categories.find(c => c.id === string.toLowerCase())
|
||||
if (!category) return ''
|
||||
|
||||
return category.name
|
||||
}
|
||||
|
||||
parseCountries(string) {
|
||||
const list = string.split(';')
|
||||
return list
|
||||
.reduce((acc, curr) => {
|
||||
const codes = utils.region2codes(curr)
|
||||
if (codes.length) {
|
||||
for (let code of codes) {
|
||||
if (!acc.includes(code)) {
|
||||
acc.push(code)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
acc.push(curr)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
.map(code => {
|
||||
const name = code ? utils.code2name(code) : null
|
||||
if (!name) return null
|
||||
|
||||
return { code: code.toLowerCase(), name }
|
||||
})
|
||||
.filter(c => c)
|
||||
}
|
||||
|
||||
parseLanguages(string) {
|
||||
const list = string.split(';')
|
||||
return list
|
||||
.map(name => {
|
||||
const code = name ? utils.language2code(name) : null
|
||||
if (!code) return null
|
||||
|
||||
return { code, name }
|
||||
})
|
||||
.filter(l => l)
|
||||
}
|
||||
|
||||
isSFW() {
|
||||
return sfwCategories.includes(this.category)
|
||||
}
|
||||
|
||||
isNSFW() {
|
||||
return nsfwCategories.includes(this.category)
|
||||
}
|
||||
|
||||
getInfo() {
|
||||
let info = `-1 tvg-id="${this.tvg.id}" tvg-country="${this.tvg.country || ''}" tvg-language="${
|
||||
this.tvg.language || ''
|
||||
}" tvg-logo="${this.logo || ''}"`
|
||||
|
||||
if (this.http['user-agent']) {
|
||||
info += ` user-agent="${this.http['user-agent']}"`
|
||||
}
|
||||
|
||||
info += ` group-title="${this.group.title || ''}",${this.name}`
|
||||
|
||||
if (this.resolution.height) {
|
||||
info += ` (${this.resolution.height}p)`
|
||||
}
|
||||
|
||||
if (this.status) {
|
||||
info += ` [${this.status}]`
|
||||
}
|
||||
|
||||
if (this.http['referrer']) {
|
||||
info += `\n#EXTVLCOPT:http-referrer=${this.http['referrer']}`
|
||||
}
|
||||
|
||||
if (this.http['user-agent']) {
|
||||
info += `\n#EXTVLCOPT:http-user-agent=${this.http['user-agent']}`
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
toString(raw = false) {
|
||||
if (raw) return this.raw + '\n'
|
||||
|
||||
return '#EXTINF:' + this.getInfo() + '\n' + this.url + '\n'
|
||||
}
|
||||
|
||||
toObject() {
|
||||
return {
|
||||
name: this.name,
|
||||
logo: this.logo || null,
|
||||
url: this.url,
|
||||
category: this.category || null,
|
||||
languages: this.languages,
|
||||
countries: this.countries,
|
||||
tvg: {
|
||||
id: this.tvg.id || null,
|
||||
name: this.tvg.name || this.name.replace(/\"/gi, ''),
|
||||
url: this.tvg.url || null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
const Channel = require('./Channel')
|
||||
const file = require('./file')
|
||||
|
||||
module.exports = class Playlist {
|
||||
constructor({ header, items, url, name, country }) {
|
||||
this.url = url
|
||||
this.name = name
|
||||
this.country = country
|
||||
this.header = header
|
||||
this.channels = items.map(item => new Channel(item)).filter(channel => channel.url)
|
||||
this.updated = false
|
||||
}
|
||||
|
||||
getHeader() {
|
||||
let header = ['#EXTM3U']
|
||||
for (let key in this.header.attrs) {
|
||||
let value = this.header.attrs[key]
|
||||
if (value) {
|
||||
header.push(`${key}="${value}"`)
|
||||
}
|
||||
}
|
||||
|
||||
return header.join(' ')
|
||||
}
|
||||
|
||||
toString(options = {}) {
|
||||
const config = { raw: false, ...options }
|
||||
let output = `${this.getHeader()}\n`
|
||||
for (let channel of this.channels) {
|
||||
output += channel.toString(config.raw)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
save() {
|
||||
if (this.updated) {
|
||||
file.create(this.url, this.toString())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,237 +0,0 @@
|
|||
const categories = require('../data/categories')
|
||||
const parser = require('./parser')
|
||||
const utils = require('./utils')
|
||||
const file = require('./file')
|
||||
const epg = require('./epg')
|
||||
|
||||
const db = {}
|
||||
|
||||
db.load = async function () {
|
||||
const files = await file.list()
|
||||
const codes = await epg.codes.load()
|
||||
for (const file of files) {
|
||||
const playlist = await parser.parsePlaylist(file)
|
||||
for (const channel of playlist.channels) {
|
||||
const code = codes.find(ch => ch['tvg_id'] === channel.tvg.id)
|
||||
if (code && Array.isArray(code.guides) && code.guides.length) {
|
||||
channel.tvg.url = code.guides[0]
|
||||
}
|
||||
|
||||
db.channels.add(channel)
|
||||
|
||||
for (const country of channel.countries) {
|
||||
if (!db.countries.has(country)) {
|
||||
db.countries.add(country)
|
||||
}
|
||||
}
|
||||
|
||||
for (const language of channel.languages) {
|
||||
if (!db.languages.has(language)) {
|
||||
db.languages.add(language)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.playlists.add(playlist)
|
||||
}
|
||||
}
|
||||
|
||||
db.channels = {
|
||||
list: [],
|
||||
filter: null,
|
||||
duplicates: true,
|
||||
offline: true,
|
||||
nsfw: true,
|
||||
add(channel) {
|
||||
this.list.push(channel)
|
||||
},
|
||||
get() {
|
||||
let output
|
||||
if (this.filter) {
|
||||
switch (this.filter.field) {
|
||||
case 'countries':
|
||||
if (this.filter.value === 'undefined') {
|
||||
output = this.list.filter(channel => !channel.countries.length)
|
||||
} else {
|
||||
output = this.list.filter(channel =>
|
||||
channel.countries.map(c => c.code).includes(this.filter.value)
|
||||
)
|
||||
}
|
||||
break
|
||||
case 'languages':
|
||||
if (this.filter.value === 'undefined') {
|
||||
output = this.list.filter(channel => !channel.languages.length)
|
||||
} else {
|
||||
output = this.list.filter(channel =>
|
||||
channel.languages.map(c => c.code).includes(this.filter.value)
|
||||
)
|
||||
}
|
||||
break
|
||||
case 'category':
|
||||
if (this.filter.value === 'other') {
|
||||
output = this.list.filter(channel => !channel.category)
|
||||
} else {
|
||||
output = this.list.filter(
|
||||
channel => channel.category.toLowerCase() === this.filter.value
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
} else {
|
||||
output = this.list
|
||||
}
|
||||
|
||||
if (!this.duplicates) {
|
||||
const buffer = []
|
||||
output = output.filter(channel => {
|
||||
if (buffer.includes(channel.hash)) return false
|
||||
buffer.push(channel.hash)
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
if (!this.nsfw) {
|
||||
output = output.filter(channel => !channel.isNSFW())
|
||||
}
|
||||
|
||||
if (!this.offline) {
|
||||
output = output.filter(channel => channel.status !== 'Offline')
|
||||
}
|
||||
|
||||
this.nsfw = true
|
||||
this.duplicates = true
|
||||
this.offline = true
|
||||
this.filter = null
|
||||
|
||||
return output
|
||||
},
|
||||
removeDuplicates() {
|
||||
this.duplicates = false
|
||||
|
||||
return this
|
||||
},
|
||||
removeNSFW() {
|
||||
this.nsfw = false
|
||||
|
||||
return this
|
||||
},
|
||||
removeOffline() {
|
||||
this.offline = false
|
||||
|
||||
return this
|
||||
},
|
||||
all() {
|
||||
return this.list
|
||||
},
|
||||
forCountry(country) {
|
||||
this.filter = {
|
||||
field: 'countries',
|
||||
value: country.code
|
||||
}
|
||||
|
||||
return this
|
||||
},
|
||||
forLanguage(language) {
|
||||
this.filter = {
|
||||
field: 'languages',
|
||||
value: language.code
|
||||
}
|
||||
|
||||
return this
|
||||
},
|
||||
forCategory(category) {
|
||||
this.filter = {
|
||||
field: 'category',
|
||||
value: category.id
|
||||
}
|
||||
|
||||
return this
|
||||
},
|
||||
count() {
|
||||
return this.get().length
|
||||
},
|
||||
sortBy(fields, order) {
|
||||
this.list = utils.sortBy(this.list, fields, order)
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
db.countries = {
|
||||
list: [],
|
||||
has(country) {
|
||||
return this.list.map(c => c.code).includes(country.code)
|
||||
},
|
||||
add(country) {
|
||||
this.list.push(country)
|
||||
},
|
||||
all() {
|
||||
return this.list
|
||||
},
|
||||
count() {
|
||||
return this.list.length
|
||||
},
|
||||
sortBy(fields, order) {
|
||||
this.list = utils.sortBy(this.list, fields, order)
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
db.languages = {
|
||||
list: [],
|
||||
has(language) {
|
||||
return this.list.map(c => c.code).includes(language.code)
|
||||
},
|
||||
add(language) {
|
||||
this.list.push(language)
|
||||
},
|
||||
all() {
|
||||
return this.list
|
||||
},
|
||||
count() {
|
||||
return this.list.length
|
||||
},
|
||||
sortBy(fields, order) {
|
||||
this.list = utils.sortBy(this.list, fields, order)
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
db.categories = {
|
||||
list: categories,
|
||||
all() {
|
||||
return this.list
|
||||
},
|
||||
count() {
|
||||
return this.list.length
|
||||
}
|
||||
}
|
||||
|
||||
db.playlists = {
|
||||
list: [],
|
||||
add(playlist) {
|
||||
this.list.push(playlist)
|
||||
},
|
||||
all() {
|
||||
return this.list
|
||||
},
|
||||
only(list = []) {
|
||||
return this.list.filter(playlist => list.includes(playlist.filename))
|
||||
},
|
||||
except(list = []) {
|
||||
return this.list.filter(playlist => !list.includes(playlist.filename))
|
||||
},
|
||||
sortBy(fields, order) {
|
||||
this.list = utils.sortBy(this.list, fields, order)
|
||||
|
||||
return this
|
||||
},
|
||||
count() {
|
||||
return this.list.length
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = db
|
|
@ -1,12 +0,0 @@
|
|||
const axios = require('axios')
|
||||
|
||||
module.exports = {
|
||||
codes: {
|
||||
async load() {
|
||||
return await axios
|
||||
.get('https://iptv-org.github.io/epg/codes.json')
|
||||
.then(r => r.data)
|
||||
.catch(console.log)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
const markdownInclude = require('markdown-include')
|
||||
const path = require('path')
|
||||
const glob = require('glob')
|
||||
const fs = require('fs')
|
||||
|
||||
const rootPath = path.resolve(__dirname) + '/../../'
|
||||
const file = {}
|
||||
|
||||
file.list = function (include = [], exclude = []) {
|
||||
return new Promise(resolve => {
|
||||
glob('channels/**/*.m3u', function (err, files) {
|
||||
if (include.length) {
|
||||
include = include.map(filename => `channels/${filename}.m3u`)
|
||||
files = files.filter(filename => include.includes(filename))
|
||||
}
|
||||
|
||||
if (exclude.length) {
|
||||
exclude = exclude.map(filename => `channels/${filename}.m3u`)
|
||||
files = files.filter(filename => !exclude.includes(filename))
|
||||
}
|
||||
|
||||
resolve(files)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
file.getFilename = function (filename) {
|
||||
return path.parse(filename).name
|
||||
}
|
||||
|
||||
file.createDir = function (dir) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir)
|
||||
}
|
||||
}
|
||||
|
||||
file.read = function (filename) {
|
||||
return fs.readFileSync(rootPath + filename, { encoding: 'utf8' })
|
||||
}
|
||||
|
||||
file.append = function (filename, data) {
|
||||
fs.appendFileSync(rootPath + filename, data)
|
||||
}
|
||||
|
||||
file.create = function (filename, data = '') {
|
||||
fs.writeFileSync(rootPath + filename, data)
|
||||
}
|
||||
|
||||
file.compileMarkdown = function (filename) {
|
||||
markdownInclude.compileFiles(rootPath + filename)
|
||||
}
|
||||
|
||||
module.exports = file
|
|
@ -1,17 +0,0 @@
|
|||
const log = {}
|
||||
|
||||
log.print = function (message) {
|
||||
if (typeof message === 'object') message = JSON.stringify(message, null, 2)
|
||||
process.stdout.write(message)
|
||||
}
|
||||
|
||||
log.start = function () {
|
||||
this.print('Starting...\n')
|
||||
console.time('Done in')
|
||||
}
|
||||
|
||||
log.finish = function () {
|
||||
console.timeEnd('Done in')
|
||||
}
|
||||
|
||||
module.exports = log
|
|
@ -1,20 +0,0 @@
|
|||
const playlistParser = require('iptv-playlist-parser')
|
||||
const Playlist = require('./Playlist')
|
||||
const utils = require('./utils')
|
||||
const file = require('./file')
|
||||
|
||||
const parser = {}
|
||||
|
||||
parser.parsePlaylist = async function (url) {
|
||||
const content = file.read(url)
|
||||
const result = playlistParser.parse(content)
|
||||
const filename = file.getFilename(url)
|
||||
const country = {
|
||||
code: filename,
|
||||
name: utils.code2name(filename)
|
||||
}
|
||||
|
||||
return new Playlist({ header: result.header, items: result.items, url, filename, country })
|
||||
}
|
||||
|
||||
module.exports = parser
|
|
@ -1,86 +0,0 @@
|
|||
const { orderBy } = require('natural-orderby')
|
||||
const transliteration = require('transliteration')
|
||||
const countries = require('../data/countries')
|
||||
const categories = require('../data/categories')
|
||||
const languages = require('../data/languages')
|
||||
const regions = require('../data/regions')
|
||||
|
||||
const utils = {}
|
||||
const intlDisplayNames = new Intl.DisplayNames(['en'], {
|
||||
style: 'narrow',
|
||||
type: 'region'
|
||||
})
|
||||
|
||||
utils.name2id = function (name) {
|
||||
return transliteration
|
||||
.transliterate(name)
|
||||
.replace(/\+/gi, 'Plus')
|
||||
.replace(/[^a-z\d]+/gi, '')
|
||||
}
|
||||
|
||||
utils.code2flag = function (code) {
|
||||
code = code.toUpperCase()
|
||||
switch (code) {
|
||||
case 'UK':
|
||||
return '🇬🇧'
|
||||
case 'INT':
|
||||
return '🌍'
|
||||
case 'UNDEFINED':
|
||||
return ''
|
||||
default:
|
||||
return code.replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397))
|
||||
}
|
||||
}
|
||||
|
||||
utils.region2codes = function (region) {
|
||||
region = region.toUpperCase()
|
||||
|
||||
return regions[region] ? regions[region].codes : []
|
||||
}
|
||||
|
||||
utils.code2name = function (code) {
|
||||
try {
|
||||
code = code.toUpperCase()
|
||||
if (regions[code]) return regions[code].name
|
||||
if (code === 'US') return 'United States'
|
||||
if (code === 'INT') return 'International'
|
||||
return intlDisplayNames.of(code)
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
utils.language2code = function (name) {
|
||||
const lang = languages.find(l => l.name === name)
|
||||
|
||||
return lang && lang.code ? lang.code : null
|
||||
}
|
||||
|
||||
utils.country2language = function (code) {
|
||||
const country = countries[code.toUpperCase()]
|
||||
if (!country.languages.length) return ''
|
||||
const language = languages.find(l => l.code === country.languages[0])
|
||||
|
||||
return language ? language.name : ''
|
||||
}
|
||||
|
||||
utils.sortBy = function (arr, fields, order = null) {
|
||||
fields = fields.map(field => {
|
||||
if (field === 'resolution.height') return channel => channel.resolution.height || 0
|
||||
if (field === 'status') return channel => channel.status || ''
|
||||
return channel => channel[field]
|
||||
})
|
||||
return orderBy(arr, fields, order)
|
||||
}
|
||||
|
||||
utils.removeProtocol = function (string) {
|
||||
return string.replace(/(^\w+:|^)\/\//, '')
|
||||
}
|
||||
|
||||
utils.sleep = function (ms) {
|
||||
return function (x) {
|
||||
return new Promise(resolve => setTimeout(() => resolve(x), ms))
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = utils
|
|
@ -1,42 +0,0 @@
|
|||
const parser = require('./helpers/parser')
|
||||
const file = require('./helpers/file')
|
||||
const log = require('./helpers/log')
|
||||
|
||||
async function main() {
|
||||
log.start()
|
||||
|
||||
let files = await file.list()
|
||||
if (!files.length) log.print(`No files is selected\n`)
|
||||
files = files.filter(file => file !== 'channels/unsorted.m3u')
|
||||
for (const file of files) {
|
||||
log.print(`\nProcessing '${file}'...`)
|
||||
await parser
|
||||
.parsePlaylist(file)
|
||||
.then(removeBrokenLinks)
|
||||
.then(p => p.save())
|
||||
}
|
||||
|
||||
log.print('\n')
|
||||
log.finish()
|
||||
}
|
||||
|
||||
async function removeBrokenLinks(playlist) {
|
||||
const buffer = []
|
||||
const channels = playlist.channels.filter(channel => {
|
||||
const sameHash = buffer.find(item => item.hash === channel.hash)
|
||||
if (sameHash && channel.status === 'Offline') return false
|
||||
|
||||
buffer.push(channel)
|
||||
return true
|
||||
})
|
||||
|
||||
if (playlist.channels.length !== channels.length) {
|
||||
log.print('updated')
|
||||
playlist.channels = channels
|
||||
playlist.updated = true
|
||||
}
|
||||
|
||||
return playlist
|
||||
}
|
||||
|
||||
main()
|
|
@ -1,81 +0,0 @@
|
|||
const parser = require('./helpers/parser')
|
||||
const utils = require('./helpers/utils')
|
||||
const file = require('./helpers/file')
|
||||
const log = require('./helpers/log')
|
||||
|
||||
let globalBuffer = []
|
||||
|
||||
async function main() {
|
||||
log.start()
|
||||
|
||||
let files = await file.list()
|
||||
if (!files.length) log.print(`No files is selected\n`)
|
||||
files = files.filter(file => file !== 'channels/unsorted.m3u')
|
||||
for (const file of files) {
|
||||
log.print(`\nProcessing '${file}'...`)
|
||||
await parser
|
||||
.parsePlaylist(file)
|
||||
.then(addToGlobalBuffer)
|
||||
.then(removeDuplicates)
|
||||
.then(p => p.save())
|
||||
}
|
||||
|
||||
if (files.length) {
|
||||
log.print(`\nProcessing 'channels/unsorted.m3u'...`)
|
||||
await parser
|
||||
.parsePlaylist('channels/unsorted.m3u')
|
||||
.then(removeDuplicates)
|
||||
.then(removeGlobalDuplicates)
|
||||
.then(p => p.save())
|
||||
}
|
||||
|
||||
log.print('\n')
|
||||
log.finish()
|
||||
}
|
||||
|
||||
async function addToGlobalBuffer(playlist) {
|
||||
playlist.channels.forEach(channel => {
|
||||
const url = utils.removeProtocol(channel.url)
|
||||
globalBuffer.push(url)
|
||||
})
|
||||
|
||||
return playlist
|
||||
}
|
||||
|
||||
async function removeDuplicates(playlist) {
|
||||
const buffer = []
|
||||
const channels = playlist.channels.filter(channel => {
|
||||
const sameUrl = buffer.find(item => {
|
||||
return utils.removeProtocol(item.url) === utils.removeProtocol(channel.url)
|
||||
})
|
||||
if (sameUrl) return false
|
||||
|
||||
buffer.push(channel)
|
||||
return true
|
||||
})
|
||||
|
||||
if (playlist.channels.length !== channels.length) {
|
||||
log.print('updated')
|
||||
playlist.channels = channels
|
||||
playlist.updated = true
|
||||
}
|
||||
|
||||
return playlist
|
||||
}
|
||||
|
||||
async function removeGlobalDuplicates(playlist) {
|
||||
const channels = playlist.channels.filter(channel => {
|
||||
const url = utils.removeProtocol(channel.url)
|
||||
return !globalBuffer.includes(url)
|
||||
})
|
||||
|
||||
if (channels.length !== playlist.channels.length) {
|
||||
log.print('updated')
|
||||
playlist.channels = channels
|
||||
playlist.updated = true
|
||||
}
|
||||
|
||||
return playlist
|
||||
}
|
||||
|
||||
main()
|
|
@ -1,41 +0,0 @@
|
|||
const parser = require('./helpers/parser')
|
||||
const utils = require('./helpers/utils')
|
||||
const file = require('./helpers/file')
|
||||
const log = require('./helpers/log')
|
||||
|
||||
async function main() {
|
||||
log.start()
|
||||
|
||||
let files = await file.list()
|
||||
if (!files.length) log.print(`No files is selected\n`)
|
||||
files = files.filter(file => file !== 'channels/unsorted.m3u')
|
||||
for (const file of files) {
|
||||
log.print(`\nProcessing '${file}'...`)
|
||||
await parser
|
||||
.parsePlaylist(file)
|
||||
.then(sortChannels)
|
||||
.then(p => p.save())
|
||||
}
|
||||
|
||||
log.print('\n')
|
||||
log.finish()
|
||||
}
|
||||
|
||||
async function sortChannels(playlist) {
|
||||
let channels = [...playlist.channels]
|
||||
channels = utils.sortBy(
|
||||
channels,
|
||||
['name', 'status', 'resolution.height', 'url'],
|
||||
['asc', 'asc', 'desc', 'asc']
|
||||
)
|
||||
|
||||
if (JSON.stringify(channels) !== JSON.stringify(playlist.channels)) {
|
||||
log.print('updated')
|
||||
playlist.channels = channels
|
||||
playlist.updated = true
|
||||
}
|
||||
|
||||
return playlist
|
||||
}
|
||||
|
||||
main()
|
|
@ -0,0 +1,12 @@
|
|||
module.exports = function () {
|
||||
if (this.group_title) return this.group_title
|
||||
|
||||
if (Array.isArray(this.categories)) {
|
||||
return this.categories
|
||||
.map(i => i.name)
|
||||
.sort()
|
||||
.join(';')
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
exports.group_title = require('./group_title')
|
||||
exports.title = require('./title')
|
||||
exports.tvg_country = require('./tvg_country')
|
||||
exports.tvg_id = require('./tvg_id')
|
||||
exports.tvg_language = require('./tvg_language')
|
||||
exports.tvg_logo = require('./tvg_logo')
|
||||
exports.tvg_url = require('./tvg_url')
|
|
@ -0,0 +1,13 @@
|
|||
module.exports = function () {
|
||||
let title = this.name
|
||||
|
||||
if (this.resolution.height) {
|
||||
title += ` (${this.resolution.height}p)`
|
||||
}
|
||||
|
||||
if (this.status.label) {
|
||||
title += ` [${this.status.label}]`
|
||||
}
|
||||
|
||||
return title
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = function () {
|
||||
if (this.tvg_country) return this.tvg_country
|
||||
|
||||
return Array.isArray(this.countries) ? this.countries.map(i => i.code).join(';') : ''
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = function () {
|
||||
return this.id || ''
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = function () {
|
||||
return Array.isArray(this.languages) ? this.languages.map(i => i.name).join(';') : ''
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = function () {
|
||||
return this.logo || ''
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = function () {
|
||||
return this.guides.length ? this.guides[0] : ''
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
const categories = require('../../data/categories')
|
||||
|
||||
module.exports = function ({ group_title }) {
|
||||
return group_title
|
||||
.split(';')
|
||||
.map(i => categories[i.toLowerCase()])
|
||||
.filter(i => i)
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
const dataRegions = require('../../data/regions')
|
||||
const dataCountries = require('../../data/countries')
|
||||
|
||||
module.exports = function ({ tvg_country, countries = [] }) {
|
||||
if (tvg_country) {
|
||||
return tvg_country
|
||||
.split(';')
|
||||
.reduce((acc, curr) => {
|
||||
const region = dataRegions[curr]
|
||||
if (region) {
|
||||
for (let code of region.country_codes) {
|
||||
if (!acc.includes(code)) acc.push(code)
|
||||
}
|
||||
} else {
|
||||
acc.push(curr)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
.map(item => dataCountries[item])
|
||||
.filter(i => i)
|
||||
}
|
||||
|
||||
return countries
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = function ({ tvg_url, guides = [] }) {
|
||||
return tvg_url ? [tvg_url] : guides
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
exports.categories = require('./categories')
|
||||
exports.countries = require('./countries')
|
||||
exports.guides = require('./guides')
|
||||
exports.is_broken = require('./is_broken')
|
||||
exports.is_nsfw = require('./is_nsfw')
|
||||
exports.languages = require('./languages')
|
||||
exports.name = require('./name')
|
||||
exports.regions = require('./regions')
|
||||
exports.resolution = require('./resolution')
|
||||
exports.src_country = require('./src_country')
|
||||
exports.status = require('./status')
|
||||
exports.url = require('./url')
|
|
@ -0,0 +1,7 @@
|
|||
module.exports = function ({ is_broken = false, status }) {
|
||||
if (status) {
|
||||
return status.level > 3 ? true : false
|
||||
}
|
||||
|
||||
return is_broken
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = function ({ categories }) {
|
||||
return Array.isArray(categories) ? categories.filter(c => c.nsfw).length > 0 : false
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
const langs = require('../../data/languages')
|
||||
|
||||
module.exports = function ({ tvg_language, languages = [] }) {
|
||||
if (tvg_language) {
|
||||
return tvg_language
|
||||
.split(';')
|
||||
.map(name => langs.find(l => l.name === name))
|
||||
.filter(i => i)
|
||||
}
|
||||
|
||||
return languages
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
module.exports = function ({ title }) {
|
||||
return title
|
||||
.trim()
|
||||
.split(' ')
|
||||
.map(s => s.trim())
|
||||
.filter(s => {
|
||||
return !/\[|\]/i.test(s) && !/\((\d+)P\)/i.test(s)
|
||||
})
|
||||
.join(' ')
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
const _ = require('lodash')
|
||||
|
||||
let regions = require('../../data/regions')
|
||||
|
||||
module.exports = function ({ countries }) {
|
||||
if (!countries.length) return []
|
||||
|
||||
const output = []
|
||||
regions = Object.values(regions)
|
||||
countries.forEach(country => {
|
||||
regions
|
||||
.filter(region => region.country_codes.includes(country.code))
|
||||
.forEach(found => {
|
||||
output.push({
|
||||
name: found.name,
|
||||
code: found.code
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return _.uniqBy(output, 'code')
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
module.exports = function ({ title, resolution = {} }) {
|
||||
if (title) {
|
||||
const [_, h] = title.match(/\((\d+)P\)/i) || [null, null]
|
||||
|
||||
return h ? { height: parseInt(h), width: null } : resolution
|
||||
}
|
||||
|
||||
return resolution
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
const { file } = require('../../core')
|
||||
const countries = require('../../data/countries')
|
||||
|
||||
module.exports = function ({ filepath }) {
|
||||
if (filepath) {
|
||||
const basename = file.basename(filepath)
|
||||
const [_, code] = basename.match(/([a-z]{2})(|_.*)\.m3u/i) || [null, null]
|
||||
|
||||
return code ? countries[code.toUpperCase()] : null
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
const statuses = require('../../data/statuses')
|
||||
|
||||
module.exports = function ({ title, status = {} }) {
|
||||
if (title) {
|
||||
const [_, label] = title.match(/\[(.*)\]/i) || [null, null]
|
||||
|
||||
return Object.values(statuses).find(s => s.label === label) || statuses['online']
|
||||
}
|
||||
|
||||
if (status) {
|
||||
switch (status.code) {
|
||||
case 'not_247':
|
||||
case 'geo_blocked':
|
||||
return status
|
||||
case 'offline':
|
||||
return statuses['not_247']
|
||||
case 'timeout':
|
||||
return statuses['timeout']
|
||||
default:
|
||||
return statuses['online']
|
||||
}
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
const normalize = require('normalize-url')
|
||||
|
||||
module.exports = function ({ url }) {
|
||||
const normalized = normalize(url, { stripWWW: false })
|
||||
|
||||
return decodeURIComponent(normalized).replace(/\s/g, '+')
|
||||
}
|
|
@ -1,142 +0,0 @@
|
|||
const utils = require('./helpers/utils')
|
||||
const file = require('./helpers/file')
|
||||
const log = require('./helpers/log')
|
||||
const db = require('./helpers/db')
|
||||
|
||||
async function main() {
|
||||
log.start()
|
||||
await loadDatabase()
|
||||
generateCategoriesTable()
|
||||
generateCountriesTable()
|
||||
generateLanguagesTable()
|
||||
generateReadme()
|
||||
log.finish()
|
||||
}
|
||||
|
||||
async function loadDatabase() {
|
||||
log.print('Loading database...\n')
|
||||
await db.load()
|
||||
}
|
||||
|
||||
function generateCategoriesTable() {
|
||||
log.print('Generating categories table...\n')
|
||||
|
||||
const categories = []
|
||||
for (const category of [...db.categories.all(), { name: 'Other', id: 'other' }]) {
|
||||
categories.push({
|
||||
category: category.name,
|
||||
channels: db.channels.forCategory(category).removeOffline().removeDuplicates().count(),
|
||||
playlist: `<code>https://iptv-org.github.io/iptv/categories/${category.id}.m3u</code>`
|
||||
})
|
||||
}
|
||||
|
||||
const table = generateTable(categories, {
|
||||
columns: [
|
||||
{ name: 'Category', align: 'left' },
|
||||
{ name: 'Channels', align: 'right' },
|
||||
{ name: 'Playlist', align: 'left' }
|
||||
]
|
||||
})
|
||||
|
||||
file.create('./.readme/_categories.md', table)
|
||||
}
|
||||
|
||||
function generateCountriesTable() {
|
||||
log.print('Generating countries table...\n')
|
||||
|
||||
const countries = []
|
||||
for (const country of [
|
||||
...db.countries.sortBy(['name']).all(),
|
||||
{ name: 'Undefined', code: 'undefined' }
|
||||
]) {
|
||||
let flag = utils.code2flag(country.code)
|
||||
const prefix = flag ? `${flag} ` : ''
|
||||
countries.push({
|
||||
country: prefix + country.name,
|
||||
channels: db.channels
|
||||
.forCountry(country)
|
||||
.removeOffline()
|
||||
.removeDuplicates()
|
||||
.removeNSFW()
|
||||
.count(),
|
||||
playlist: `<code>https://iptv-org.github.io/iptv/countries/${country.code}.m3u</code>`
|
||||
})
|
||||
}
|
||||
|
||||
const table = generateTable(countries, {
|
||||
columns: [
|
||||
{ name: 'Country', align: 'left' },
|
||||
{ name: 'Channels', align: 'right' },
|
||||
{ name: 'Playlist', align: 'left', nowrap: true }
|
||||
]
|
||||
})
|
||||
|
||||
file.create('./.readme/_countries.md', table)
|
||||
}
|
||||
|
||||
function generateLanguagesTable() {
|
||||
log.print('Generating languages table...\n')
|
||||
const languages = []
|
||||
|
||||
for (const language of [
|
||||
...db.languages.sortBy(['name']).all(),
|
||||
{ name: 'Undefined', code: 'undefined' }
|
||||
]) {
|
||||
languages.push({
|
||||
language: language.name,
|
||||
channels: db.channels
|
||||
.forLanguage(language)
|
||||
.removeOffline()
|
||||
.removeDuplicates()
|
||||
.removeNSFW()
|
||||
.count(),
|
||||
playlist: `<code>https://iptv-org.github.io/iptv/languages/${language.code}.m3u</code>`
|
||||
})
|
||||
}
|
||||
|
||||
const table = generateTable(languages, {
|
||||
columns: [
|
||||
{ name: 'Language', align: 'left' },
|
||||
{ name: 'Channels', align: 'right' },
|
||||
{ name: 'Playlist', align: 'left' }
|
||||
]
|
||||
})
|
||||
|
||||
file.create('./.readme/_languages.md', table)
|
||||
}
|
||||
|
||||
function generateTable(data, options) {
|
||||
let output = '<table>\n'
|
||||
|
||||
output += '\t<thead>\n\t\t<tr>'
|
||||
for (let column of options.columns) {
|
||||
output += `<th align="${column.align}">${column.name}</th>`
|
||||
}
|
||||
output += '</tr>\n\t</thead>\n'
|
||||
|
||||
output += '\t<tbody>\n'
|
||||
for (let item of data) {
|
||||
output += '\t\t<tr>'
|
||||
let i = 0
|
||||
for (let prop in item) {
|
||||
const column = options.columns[i]
|
||||
let nowrap = column.nowrap
|
||||
let align = column.align
|
||||
output += `<td align="${align}"${nowrap ? ' nowrap' : ''}>${item[prop]}</td>`
|
||||
i++
|
||||
}
|
||||
output += '</tr>\n'
|
||||
}
|
||||
output += '\t</tbody>\n'
|
||||
|
||||
output += '</table>'
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
function generateReadme() {
|
||||
log.print('Generating README.md...\n')
|
||||
file.compileMarkdown('.readme/config.json')
|
||||
}
|
||||
|
||||
main()
|
|
@ -0,0 +1,2 @@
|
|||
output/
|
||||
temp/
|
|
@ -0,0 +1,3 @@
|
|||
#EXTM3U
|
||||
#EXTINF:-1 tvg-id="ATV.ad" tvg-country="AD" tvg-language="Catalan" tvg-logo="https://i.imgur.com/kJCjeQ4.png" group-title="General",ATV (720p) [Offline]
|
||||
https://iptv-all.lanesh4d0w.repl.co/andorra/atv
|
|
@ -0,0 +1,3 @@
|
|||
#EXTM3U
|
||||
#EXTINF:-1 tvg-id="FoxSports2Asia.us" tvg-country="TH" tvg-language="Thai" tvg-logo="" group-title="Sports",Fox Sports 2 Asia (Thai) (720p)
|
||||
https://example.com/playlist.m3u8
|
|
@ -0,0 +1 @@
|
|||
[{"tvg_id":"AndorraTV.ad","display_name":"Andorra TV","country":"ad","guides":["https://iptv-org.github.io/epg/guides/ad/andorradifusio.ad.epg.xml"],"logo":"https://www.andorradifusio.ad/images/logo/andorradifusio_logo_22122020091723.png"}]
|
|
@ -0,0 +1,3 @@
|
|||
{"_id":"I6cjG2xCBRFFP4sz","url":"https://iptv-all.lanesh4d0w.repl.co/andorra/atv","http":{"referrer":"","user-agent":""},"error":"Operation timed out","streams":[],"requests":[]}
|
||||
{"_id":"3TbieV1ptnZVCIdn","url":"http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8","http":{"referrer":"","user-agent":""},"error":"Server returned 404 Not Found","streams":[],"requests":[]}
|
||||
{"_id":"2ST8btby3mmsgPF0","url":"http://46.46.143.222:1935/live/mp4:ldpr.stream/playlist.m3u8","http":{"referrer":"","user-agent":""},"error":null,"streams":[{"index":0,"codec_name":"timed_id3","codec_long_name":"timed ID3 metadata","codec_type":"data","codec_tag_string":"ID3 ","codec_tag":"0x20334449","r_frame_rate":"0/0","avg_frame_rate":"0/0","time_base":"1/90000","disposition":{"default":0,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":0,"timed_thumbnails":0},"tags":{"variant_bitrate":"6527203"}},{"index":1,"codec_name":"h264","codec_long_name":"H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10","profile":"Main","codec_type":"video","codec_tag_string":"[27][0][0][0]","codec_tag":"0x001b","width":1920,"height":1080,"coded_width":1920,"coded_height":1080,"closed_captions":0,"has_b_frames":0,"sample_aspect_ratio":"1:1","display_aspect_ratio":"16:9","pix_fmt":"yuv420p","level":40,"chroma_location":"left","refs":1,"is_avc":"false","nal_length_size":"0","r_frame_rate":"50/1","avg_frame_rate":"50/1","time_base":"1/90000","start_pts":8171218184,"start_time":"90791.313156","bits_per_raw_sample":"8","disposition":{"default":0,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":0,"timed_thumbnails":0},"tags":{"variant_bitrate":"6527203"}},{"index":2,"codec_name":"aac","codec_long_name":"AAC (Advanced Audio Coding)","profile":"LC","codec_type":"audio","codec_tag_string":"[15][0][0][0]","codec_tag":"0x000f","sample_fmt":"fltp","sample_rate":"48000","channels":2,"channel_layout":"stereo","bits_per_sample":0,"r_frame_rate":"0/0","avg_frame_rate":"0/0","time_base":"1/90000","start_pts":8171229134,"start_time":"90791.434822","disposition":{"default":0,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":0,"timed_thumbnails":0},"tags":{"variant_bitrate":"6527203"}}],"requests":[{"method":"GET","url":"http://46.46.143.222:1935/live/mp4:ldpr.stream/playlist.m3u8","headers":{"User-Agent":"Lavf/58.76.100","Accept":"*/*","Range":"bytes=0-","Connection":"close","Host":"46.46.143.222:1935","Icy-MetaData":"1"}},{"method":"GET","url":"http://46.46.143.222:1935/live/mp4:ldpr.stream/chunklist_w1629502765.m3u8","headers":{"User-Agent":"Lavf/58.76.100","Accept":"*/*","Range":"bytes=0-","Connection":"keep-alive","Host":"46.46.143.222:1935","Icy-MetaData":"1"}},{"method":"GET","url":"http://46.46.143.222:1935/live/mp4:ldpr.stream/media_w1629502765_1085323.ts","headers":{"User-Agent":"Lavf/58.76.100","Accept":"*/*","Range":"bytes=0-","Connection":"keep-alive","Host":"46.46.143.222:1935","Icy-MetaData":"1"}},{"method":"GET","url":"http://46.46.143.222:1935/live/mp4:ldpr.stream/media_w1629502765_1085324.ts","headers":{"User-Agent":"Lavf/58.76.100","Accept":"*/*","Range":"bytes=0-","Connection":"keep-alive","Host":"46.46.143.222:1935","Icy-MetaData":"1"}}]}
|
|
@ -0,0 +1,3 @@
|
|||
{"name":"General","slug":"general","count":1}
|
||||
{"name":"News","slug":"news","count":1}
|
||||
{"name":"Other","slug":"other","count":0}
|
|
@ -0,0 +1,5 @@
|
|||
{"name":"Andorra","code":"AD","count":0}
|
||||
{"name":"Russia","code":"RU","count":1}
|
||||
{"name":"United Kingdom","code":"UK","count":1}
|
||||
{"name":"International","code":"INT","count":0}
|
||||
{"name":"Undefined","code":"UNDEFINED","count":0}
|
|
@ -0,0 +1,4 @@
|
|||
{"name":"Catalan","code":"cat","count":0}
|
||||
{"name":"English","code":"eng","count":1}
|
||||
{"name":"Russian","code":"rus","count":1}
|
||||
{"name":"Undefined","code":"undefined","count":0}
|
|
@ -0,0 +1,5 @@
|
|||
{"name":"Asia","code":"ASIA","count":1}
|
||||
{"name":"Commonwealth of Independent States","code":"CIS","count":1}
|
||||
{"name":"Europe","code":"EUR","count":2}
|
||||
{"name":"Europe, the Middle East and Africa","code":"EMEA","count":2}
|
||||
{"name":"Undefined","code":"UNDEFINED","count":0}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"build" : "tests/__data__/output/readme.md",
|
||||
"files" : ["./.readme/template.md"]
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
# IPTV
|
||||
|
||||
[![auto-update](https://github.com/iptv-org/iptv/actions/workflows/auto-update.yml/badge.svg)](https://github.com/iptv-org/iptv/actions/workflows/auto-update.yml)
|
||||
|
||||
Collection of publicly available IPTV channels from all over the world.
|
||||
|
||||
Internet Protocol television (IPTV) is the delivery of television content over Internet Protocol (IP) networks.
|
||||
|
||||
## Usage
|
||||
|
||||
To watch IPTV you just need to paste this link `https://iptv-org.github.io/iptv/index.m3u` to any player which supports M3U-playlists.
|
||||
|
||||
![VLC Network Panel](.readme/preview.png)
|
||||
|
||||
Also you can instead use one of these playlists:
|
||||
|
||||
- `https://iptv-org.github.io/iptv/index.category.m3u` (grouped by category)
|
||||
- `https://iptv-org.github.io/iptv/index.language.m3u` (grouped by language)
|
||||
- `https://iptv-org.github.io/iptv/index.country.m3u` (grouped by country)
|
||||
- `https://iptv-org.github.io/iptv/index.region.m3u` (grouped by region)
|
||||
- `https://iptv-org.github.io/iptv/index.nsfw.m3u` (includes adult channels)
|
||||
|
||||
Or select one of the playlists from the list below.
|
||||
|
||||
### Playlists by category
|
||||
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
<br>
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th align="left">Category</th><th align="right">Channels</th><th align="left">Playlist</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td align="left">General</td><td align="right">1</td><td align="left"><code>https://iptv-org.github.io/iptv/categories/general.m3u</code></td></tr>
|
||||
<tr><td align="left">News</td><td align="right">1</td><td align="left"><code>https://iptv-org.github.io/iptv/categories/news.m3u</code></td></tr>
|
||||
<tr><td align="left">Other</td><td align="right">0</td><td align="left"><code>https://iptv-org.github.io/iptv/categories/other.m3u</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</details>
|
||||
|
||||
### Playlists by language
|
||||
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
<br>
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th align="left">Language</th><th align="right">Channels</th><th align="left">Playlist</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td align="left">Catalan</td><td align="right">0</td><td align="left"><code>https://iptv-org.github.io/iptv/languages/cat.m3u</code></td></tr>
|
||||
<tr><td align="left">English</td><td align="right">1</td><td align="left"><code>https://iptv-org.github.io/iptv/languages/eng.m3u</code></td></tr>
|
||||
<tr><td align="left">Russian</td><td align="right">1</td><td align="left"><code>https://iptv-org.github.io/iptv/languages/rus.m3u</code></td></tr>
|
||||
<tr><td align="left">Undefined</td><td align="right">0</td><td align="left"><code>https://iptv-org.github.io/iptv/languages/undefined.m3u</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</details>
|
||||
|
||||
### Playlists by region
|
||||
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
<br>
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th align="left">Region</th><th align="right">Channels</th><th align="left">Playlist</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td align="left">Asia</td><td align="right">1</td><td align="left"><code>https://iptv-org.github.io/iptv/regions/asia.m3u</code></td></tr>
|
||||
<tr><td align="left">Commonwealth of Independent States</td><td align="right">1</td><td align="left"><code>https://iptv-org.github.io/iptv/regions/cis.m3u</code></td></tr>
|
||||
<tr><td align="left">Europe</td><td align="right">2</td><td align="left"><code>https://iptv-org.github.io/iptv/regions/eur.m3u</code></td></tr>
|
||||
<tr><td align="left">Europe, the Middle East and Africa</td><td align="right">2</td><td align="left"><code>https://iptv-org.github.io/iptv/regions/emea.m3u</code></td></tr>
|
||||
<tr><td align="left">Undefined</td><td align="right">0</td><td align="left"><code>https://iptv-org.github.io/iptv/regions/undefined.m3u</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</details>
|
||||
|
||||
### Playlists by country
|
||||
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
<br>
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th align="left">Country</th><th align="right">Channels</th><th align="left">Playlist</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td align="left">🇦🇩 Andorra</td><td align="right">0</td><td align="left"><code>https://iptv-org.github.io/iptv/countries/ad.m3u</code></td></tr>
|
||||
<tr><td align="left">🇷🇺 Russia</td><td align="right">1</td><td align="left"><code>https://iptv-org.github.io/iptv/countries/ru.m3u</code></td></tr>
|
||||
<tr><td align="left">🇬🇧 United Kingdom</td><td align="right">1</td><td align="left"><code>https://iptv-org.github.io/iptv/countries/uk.m3u</code></td></tr>
|
||||
<tr><td align="left">🌍 International</td><td align="right">0</td><td align="left"><code>https://iptv-org.github.io/iptv/countries/int.m3u</code></td></tr>
|
||||
<tr><td align="left">Undefined</td><td align="right">0</td><td align="left"><code>https://iptv-org.github.io/iptv/countries/undefined.m3u</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</details>
|
||||
|
||||
## For Developers
|
||||
|
||||
In addition to the above methods, you can also get a list of all available channels in JSON format.
|
||||
|
||||
To do this, you just have to make a GET request to:
|
||||
|
||||
```
|
||||
https://iptv-org.github.io/iptv/channels.json
|
||||
```
|
||||
|
||||
If successful, you should get the following response:
|
||||
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
<br>
|
||||
|
||||
```
|
||||
[
|
||||
...
|
||||
{
|
||||
"name": "CNN",
|
||||
"logo": "https://i.imgur.com/ilZJT5s.png",
|
||||
"url": "http://ott-cdn.ucom.am/s27/index.m3u8",
|
||||
"categories": [
|
||||
{
|
||||
"name": "News",
|
||||
"slug": "news"
|
||||
}
|
||||
],
|
||||
"countries": [
|
||||
{
|
||||
"code": "us",
|
||||
"name": "United States"
|
||||
},
|
||||
{
|
||||
"code": "ca",
|
||||
"name": "Canada"
|
||||
}
|
||||
],
|
||||
"languages": [
|
||||
{
|
||||
"code": "eng",
|
||||
"name": "English"
|
||||
}
|
||||
],
|
||||
"tvg": {
|
||||
"id": "cnn.us",
|
||||
"name": "CNN",
|
||||
"url": "http://epg.streamstv.me/epg/guide-usa.xml.gz"
|
||||
}
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
</details>
|
||||
|
||||
## EPG
|
||||
|
||||
Playlists already have a built-in list of EPG, so players that support the `url-tvg` tag should load it automatically. If not, you can find a list of available programs here:
|
||||
|
||||
https://github.com/iptv-org/epg
|
||||
|
||||
## Resources
|
||||
|
||||
You can find links to various IPTV related resources in this repository [iptv-org/awesome-iptv](https://github.com/iptv-org/awesome-iptv).
|
||||
|
||||
## Contribution
|
||||
|
||||
Please make sure to read the [Contributing Guide](CONTRIBUTING.md) before sending an issue or making a pull request.
|
||||
|
||||
## Legal
|
||||
|
||||
No video files are stored in this repository. The repository simply contains user-submitted links to publicly available video stream URLs, which to the best of our knowledge have been intentionally made publicly by the copyright holders. If any links in these playlists infringe on your rights as a copyright holder, they may be removed by sending a pull request or opening an issue. However, note that we have **no control** over the destination of the link, and just removing the link from the playlist will not remove its contents from the web. Note that linking does not directly infringe copyright because no copy is made on the site providing the link, and thus this is **not** a valid reason to send a DMCA notice to GitHub. To remove this content from the web, you should contact the web host that's actually hosting the content (**not** GitHub, nor the maintainers of this repository).
|
|
@ -0,0 +1,4 @@
|
|||
{"name":"ЛДПР ТВ","id":"LDPRTV.ru","filepath":"tests/__data__/output/channels/ru.m3u","src_country":{"name":"Russia","code":"RU","lang":"rus"},"tvg_country":"RU","countries":[{"name":"Russia","code":"RU","lang":"rus"}],"regions":[{"name":"Asia","code":"ASIA"},{"name":"Commonwealth of Independent States","code":"CIS"},{"name":"Europe, the Middle East and Africa","code":"EMEA"},{"name":"Europe","code":"EUR"}],"languages":[{"name":"Russian","code":"rus"}],"categories":[{"name":"General","slug":"general","nsfw":false}],"tvg_url":"","guides":["https://iptv-org.github.io/epg/guides/ru/tv.yandex.ru.epg.xml"],"logo":"https://iptvx.one/icn/ldpr-tv.png","resolution":{"height":1080,"width":null},"status":{"label":"","code":"online","level":1},"url":"http://46.46.143.222:1935/live/mp4:ldpr.stream/playlist.m3u8","http":{"referrer":"","user-agent":""},"is_nsfw":false,"is_broken":false,"updated":false,"cluster_id":1,"_id":"2ST8btby3mmsgPF0"}
|
||||
{"name":"BBC News HD","id":"BBCNews.uk","filepath":"tests/__data__/output/channels/uk.m3u","src_country":{"name":"United Kingdom","code":"UK","lang":"eng"},"tvg_country":"UK","countries":[{"name":"United Kingdom","code":"UK","lang":"eng"}],"regions":[{"name":"Europe, the Middle East and Africa","code":"EMEA"},{"name":"Europe","code":"EUR"}],"languages":[{"name":"English","code":"eng"}],"categories":[{"name":"News","slug":"news","nsfw":false}],"tvg_url":"","guides":[],"logo":"https://i.imgur.com/eNPIQ9f.png","resolution":{"height":720,"width":null},"status":{"label":"Not 24/7","code":"not_247","level":3},"url":"http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8","http":{"referrer":"","user-agent":""},"is_nsfw":false,"is_broken":false,"updated":false,"cluster_id":3,"_id":"3TbieV1ptnZVCIdn"}
|
||||
{"name":"ATV","id":"AndorraTV.ad","filepath":"tests/__data__/output/channels/ad.m3u","src_country":{"name":"Andorra","code":"AD","lang":"cat"},"tvg_country":"AD","countries":[{"name":"Andorra","code":"AD","lang":"cat"}],"regions":[{"name":"Europe, the Middle East and Africa","code":"EMEA"},{"name":"Europe","code":"EUR"}],"languages":[{"name":"Catalan","code":"cat"}],"categories":[{"name":"General","slug":"general","nsfw":false}],"tvg_url":"","guides":[],"logo":"https://i.imgur.com/kJCjeQ4.png","resolution":{"height":720,"width":null},"status":{"label":"Offline","code":"offline","level":5},"url":"https://iptv-all.lanesh4d0w.repl.co/andorra/atv","http":{"referrer":"","user-agent":""},"is_nsfw":false,"is_broken":true,"updated":false,"cluster_id":1,"_id":"I6cjG2xCBRFFP4sz"}
|
||||
{"name":"BBC News HD","id":"AndorraTV.ad","filepath":"tests/__data__/output/channels/uk.m3u","src_country":{"name":"United Kingdom","code":"UK","lang":"eng"},"tvg_country":"UK","countries":[{"name":"United Kingdom","code":"UK","lang":"eng"}],"regions":[{"name":"Europe, the Middle East and Africa","code":"EMEA"},{"name":"Europe","code":"EUR"}],"languages":[{"name":"English","code":"eng"}],"categories":[{"name":"News","slug":"news","nsfw":false}],"tvg_url":"","guides":[],"logo":"https://i.imgur.com/eNPIQ9f.png","resolution":{"height":720,"width":null},"status":{"label":"Not 24/7","code":"not_247","level":3},"url":"http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8","http":{"referrer":"","user-agent":""},"is_nsfw":false,"is_broken":false,"updated":false,"cluster_id":3,"_id":"WTbieV1ptnZVCIdn"}
|
|
@ -0,0 +1,44 @@
|
|||
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.copyFileSync('tests/__data__/input/test.db', 'tests/__data__/temp/test.db')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmdirSync('tests/__data__/temp', { recursive: true })
|
||||
fs.mkdirSync('tests/__data__/temp')
|
||||
})
|
||||
|
||||
it('return results if stream with error', () => {
|
||||
const result = execSync(
|
||||
'DB_FILEPATH=tests/__data__/temp/test.db LOGS_PATH=tests/__data__/output/logs node scripts/commands/check-streams.js --cluster-id=1 --timeout=1',
|
||||
{ encoding: 'utf8' }
|
||||
)
|
||||
const logs = fs.readFileSync(
|
||||
path.resolve('tests/__data__/output/logs/check-streams/cluster_1.log'),
|
||||
{
|
||||
encoding: 'utf8'
|
||||
}
|
||||
)
|
||||
const lines = logs.split('\n')
|
||||
expect(JSON.parse(lines[0])).toMatchObject({
|
||||
_id: '2ST8btby3mmsgPF0',
|
||||
url: 'http://46.46.143.222:1935/live/mp4:ldpr.stream/playlist.m3u8',
|
||||
http: { referrer: '', 'user-agent': '' },
|
||||
error: 'Operation timed out',
|
||||
streams: [],
|
||||
requests: []
|
||||
})
|
||||
expect(JSON.parse(lines[1])).toMatchObject({
|
||||
_id: 'I6cjG2xCBRFFP4sz',
|
||||
url: 'https://iptv-all.lanesh4d0w.repl.co/andorra/atv',
|
||||
http: { referrer: '', 'user-agent': '' },
|
||||
error: 'Operation timed out',
|
||||
streams: [],
|
||||
requests: []
|
||||
})
|
||||
})
|
|
@ -0,0 +1,25 @@
|
|||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { execSync } = require('child_process')
|
||||
|
||||
beforeEach(() => {
|
||||
fs.copyFileSync('tests/__data__/input/test.db', 'tests/__data__/temp/test.db')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmdirSync('tests/__data__/temp', { recursive: true })
|
||||
fs.mkdirSync('tests/__data__/temp')
|
||||
})
|
||||
|
||||
it('can remove broken links from database', () => {
|
||||
const result = execSync(
|
||||
'DB_FILEPATH=tests/__data__/temp/test.db node scripts/commands/cleanup-database.js',
|
||||
{ encoding: 'utf8' }
|
||||
)
|
||||
|
||||
const database = fs.readFileSync('tests/__data__/temp/test.db', { encoding: 'utf8' })
|
||||
const lines = database.split('\n')
|
||||
expect(lines[0]).toBe(
|
||||
`{"name":"ЛДПР ТВ","id":"LDPRTV.ru","filepath":"tests/__data__/output/channels/ru.m3u","src_country":{"name":"Russia","code":"RU","lang":"rus"},"tvg_country":"RU","countries":[{"name":"Russia","code":"RU","lang":"rus"}],"regions":[{"name":"Asia","code":"ASIA"},{"name":"Commonwealth of Independent States","code":"CIS"},{"name":"Europe, the Middle East and Africa","code":"EMEA"},{"name":"Europe","code":"EUR"}],"languages":[{"name":"Russian","code":"rus"}],"categories":[{"name":"General","slug":"general","nsfw":false}],"tvg_url":"","guides":["https://iptv-org.github.io/epg/guides/ru/tv.yandex.ru.epg.xml"],"logo":"https://iptvx.one/icn/ldpr-tv.png","resolution":{"height":1080,"width":null},"status":{"label":"","code":"online","level":1},"url":"http://46.46.143.222:1935/live/mp4:ldpr.stream/playlist.m3u8","http":{"referrer":"","user-agent":""},"is_nsfw":false,"is_broken":false,"updated":false,"cluster_id":1,"_id":"2ST8btby3mmsgPF0"}`
|
||||
)
|
||||
})
|
|
@ -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')
|
||||
})
|
||||
|
||||
it('can create database', () => {
|
||||
execSync(
|
||||
'DB_FILEPATH=tests/__data__/output/test.db node scripts/commands/create-database.js --input-dir=tests/__data__/input/channels --max-clusters=1',
|
||||
{ encoding: 'utf8' }
|
||||
)
|
||||
|
||||
const database = fs.readFileSync(path.resolve('tests/__data__/output/test.db'), {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
const item = database.split('\n').find(i => i.includes('ATV.ad'))
|
||||
expect(JSON.parse(item)).toMatchObject({
|
||||
name: 'ATV',
|
||||
id: 'ATV.ad',
|
||||
filepath: 'tests/__data__/input/channels/ad_example.m3u',
|
||||
src_country: { name: 'Andorra', code: 'AD', lang: 'cat' },
|
||||
tvg_country: 'AD',
|
||||
countries: [{ name: 'Andorra', code: 'AD', lang: 'cat' }],
|
||||
regions: [
|
||||
{ name: 'Europe, the Middle East and Africa', code: 'EMEA' },
|
||||
{ name: 'Europe', code: 'EUR' }
|
||||
],
|
||||
languages: [{ name: 'Catalan', code: 'cat' }],
|
||||
categories: [{ name: 'General', slug: 'general', nsfw: false }],
|
||||
tvg_url: '',
|
||||
guides: [],
|
||||
logo: 'https://i.imgur.com/kJCjeQ4.png',
|
||||
resolution: { height: 720, width: null },
|
||||
status: { label: 'Offline', code: 'offline', level: 5 },
|
||||
url: 'https://iptv-all.lanesh4d0w.repl.co/andorra/atv',
|
||||
http: { referrer: '', 'user-agent': '' },
|
||||
is_nsfw: false,
|
||||
is_broken: true,
|
||||
updated: false,
|
||||
cluster_id: 1
|
||||
})
|
||||
})
|
|
@ -0,0 +1,20 @@
|
|||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { execSync } = require('child_process')
|
||||
|
||||
beforeEach(() => {
|
||||
fs.copyFileSync('tests/__data__/input/test.db', 'tests/__data__/temp/test.db')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmdirSync('tests/__data__/temp', { recursive: true })
|
||||
fs.mkdirSync('tests/__data__/temp')
|
||||
})
|
||||
|
||||
it('can create valid matrix', () => {
|
||||
const result = execSync(
|
||||
'DB_FILEPATH=tests/__data__/temp/test.db node scripts/commands/create-matrix.js',
|
||||
{ encoding: 'utf8' }
|
||||
)
|
||||
expect(result).toBe('::set-output name=matrix::{"cluster_id":[1,3]}\n')
|
||||
})
|
|
@ -0,0 +1,204 @@
|
|||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { execSync } = require('child_process')
|
||||
|
||||
function content(filepath) {
|
||||
return fs.readFileSync(`tests/__data__/output/${filepath}`, {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
fs.rmdirSync('tests/__data__/output', { recursive: true })
|
||||
fs.copyFileSync('tests/__data__/input/test.db', 'tests/__data__/temp/test.db')
|
||||
|
||||
execSync(
|
||||
'DB_FILEPATH=tests/__data__/temp/test.db PUBLIC_PATH=tests/__data__/output/.gh-pages LOGS_PATH=tests/__data__/output/logs node scripts/commands/generate-playlists.js',
|
||||
{ encoding: 'utf8' }
|
||||
)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmdirSync('tests/__data__/temp', { recursive: true })
|
||||
fs.mkdirSync('tests/__data__/temp')
|
||||
})
|
||||
|
||||
it('can generate categories', () => {
|
||||
expect(content('.gh-pages/categories/general.m3u'))
|
||||
.toBe(`#EXTM3U x-tvg-url="https://iptv-org.github.io/epg/guides/ru/tv.yandex.ru.epg.xml"
|
||||
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-country="RU" tvg-language="Russian" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
|
||||
http://46.46.143.222:1935/live/mp4:ldpr.stream/playlist.m3u8
|
||||
`)
|
||||
|
||||
expect(content('.gh-pages/categories/news.m3u')).toBe(`#EXTM3U x-tvg-url=""
|
||||
#EXTINF:-1 tvg-id="BBCNews.uk" tvg-country="UK" tvg-language="English" tvg-logo="https://i.imgur.com/eNPIQ9f.png" group-title="News",BBC News HD (720p) [Not 24/7]
|
||||
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
|
||||
`)
|
||||
})
|
||||
|
||||
it('can generate countries', () => {
|
||||
expect(content('.gh-pages/countries/ru.m3u'))
|
||||
.toBe(`#EXTM3U x-tvg-url="https://iptv-org.github.io/epg/guides/ru/tv.yandex.ru.epg.xml"
|
||||
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-country="RU" tvg-language="Russian" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
|
||||
http://46.46.143.222:1935/live/mp4:ldpr.stream/playlist.m3u8
|
||||
`)
|
||||
|
||||
expect(content('.gh-pages/countries/uk.m3u')).toBe(`#EXTM3U x-tvg-url=""
|
||||
#EXTINF:-1 tvg-id="BBCNews.uk" tvg-country="UK" tvg-language="English" tvg-logo="https://i.imgur.com/eNPIQ9f.png" group-title="News",BBC News HD (720p) [Not 24/7]
|
||||
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
|
||||
`)
|
||||
})
|
||||
|
||||
it('can generate languages', () => {
|
||||
expect(content('.gh-pages/languages/rus.m3u'))
|
||||
.toBe(`#EXTM3U x-tvg-url="https://iptv-org.github.io/epg/guides/ru/tv.yandex.ru.epg.xml"
|
||||
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-country="RU" tvg-language="Russian" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
|
||||
http://46.46.143.222:1935/live/mp4:ldpr.stream/playlist.m3u8
|
||||
`)
|
||||
|
||||
expect(content('.gh-pages/languages/eng.m3u')).toBe(`#EXTM3U x-tvg-url=""
|
||||
#EXTINF:-1 tvg-id="BBCNews.uk" tvg-country="UK" tvg-language="English" tvg-logo="https://i.imgur.com/eNPIQ9f.png" group-title="News",BBC News HD (720p) [Not 24/7]
|
||||
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
|
||||
`)
|
||||
})
|
||||
|
||||
it('can generate regions', () => {
|
||||
expect(content('.gh-pages/regions/asia.m3u'))
|
||||
.toBe(`#EXTM3U x-tvg-url="https://iptv-org.github.io/epg/guides/ru/tv.yandex.ru.epg.xml"
|
||||
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-country="RU" tvg-language="Russian" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
|
||||
http://46.46.143.222:1935/live/mp4:ldpr.stream/playlist.m3u8
|
||||
`)
|
||||
|
||||
expect(content('.gh-pages/regions/cis.m3u'))
|
||||
.toBe(`#EXTM3U x-tvg-url="https://iptv-org.github.io/epg/guides/ru/tv.yandex.ru.epg.xml"
|
||||
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-country="RU" tvg-language="Russian" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
|
||||
http://46.46.143.222:1935/live/mp4:ldpr.stream/playlist.m3u8
|
||||
`)
|
||||
|
||||
expect(content('.gh-pages/regions/emea.m3u'))
|
||||
.toBe(`#EXTM3U x-tvg-url="https://iptv-org.github.io/epg/guides/ru/tv.yandex.ru.epg.xml"
|
||||
#EXTINF:-1 tvg-id="BBCNews.uk" tvg-country="UK" tvg-language="English" tvg-logo="https://i.imgur.com/eNPIQ9f.png" group-title="News",BBC News HD (720p) [Not 24/7]
|
||||
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
|
||||
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-country="RU" tvg-language="Russian" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
|
||||
http://46.46.143.222:1935/live/mp4:ldpr.stream/playlist.m3u8
|
||||
`)
|
||||
|
||||
expect(content('.gh-pages/regions/eur.m3u'))
|
||||
.toBe(`#EXTM3U x-tvg-url="https://iptv-org.github.io/epg/guides/ru/tv.yandex.ru.epg.xml"
|
||||
#EXTINF:-1 tvg-id="BBCNews.uk" tvg-country="UK" tvg-language="English" tvg-logo="https://i.imgur.com/eNPIQ9f.png" group-title="News",BBC News HD (720p) [Not 24/7]
|
||||
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
|
||||
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-country="RU" tvg-language="Russian" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
|
||||
http://46.46.143.222:1935/live/mp4:ldpr.stream/playlist.m3u8
|
||||
`)
|
||||
})
|
||||
|
||||
it('can generate channels.json', () => {
|
||||
expect(content('.gh-pages/channels.json')).toBe(
|
||||
`[{"name":"BBC News HD","logo":"https://i.imgur.com/eNPIQ9f.png","url":"http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8","categories":[{"name":"News","slug":"news"}],"countries":[{"name":"United Kingdom","code":"UK"}],"languages":[{"name":"English","code":"eng"}],"tvg":{"id":"BBCNews.uk","name":"BBC News HD","url":""}},{"name":"ЛДПР ТВ","logo":"https://iptvx.one/icn/ldpr-tv.png","url":"http://46.46.143.222:1935/live/mp4:ldpr.stream/playlist.m3u8","categories":[{"name":"General","slug":"general"}],"countries":[{"name":"Russia","code":"RU"}],"languages":[{"name":"Russian","code":"rus"}],"tvg":{"id":"LDPRTV.ru","name":"ЛДПР ТВ","url":"https://iptv-org.github.io/epg/guides/ru/tv.yandex.ru.epg.xml"}}]`
|
||||
)
|
||||
})
|
||||
|
||||
it('can generate index.category.m3u', () => {
|
||||
expect(content('.gh-pages/index.category.m3u'))
|
||||
.toBe(`#EXTM3U x-tvg-url="https://iptv-org.github.io/epg/guides/ru/tv.yandex.ru.epg.xml"
|
||||
#EXTINF:-1 tvg-id="BBCNews.uk" tvg-country="UK" tvg-language="English" tvg-logo="https://i.imgur.com/eNPIQ9f.png" group-title="News",BBC News HD (720p) [Not 24/7]
|
||||
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
|
||||
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-country="RU" tvg-language="Russian" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
|
||||
http://46.46.143.222:1935/live/mp4:ldpr.stream/playlist.m3u8
|
||||
`)
|
||||
})
|
||||
|
||||
it('can generate index.country.m3u', () => {
|
||||
expect(content('.gh-pages/index.country.m3u'))
|
||||
.toBe(`#EXTM3U x-tvg-url="https://iptv-org.github.io/epg/guides/ru/tv.yandex.ru.epg.xml"
|
||||
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-country="RU" tvg-language="Russian" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Russia",ЛДПР ТВ (1080p)
|
||||
http://46.46.143.222:1935/live/mp4:ldpr.stream/playlist.m3u8
|
||||
#EXTINF:-1 tvg-id="BBCNews.uk" tvg-country="UK" tvg-language="English" tvg-logo="https://i.imgur.com/eNPIQ9f.png" group-title="United Kingdom",BBC News HD (720p) [Not 24/7]
|
||||
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
|
||||
`)
|
||||
})
|
||||
|
||||
it('can generate index.language.m3u', () => {
|
||||
expect(content('.gh-pages/index.language.m3u'))
|
||||
.toBe(`#EXTM3U x-tvg-url="https://iptv-org.github.io/epg/guides/ru/tv.yandex.ru.epg.xml"
|
||||
#EXTINF:-1 tvg-id="BBCNews.uk" tvg-country="UK" tvg-language="English" tvg-logo="https://i.imgur.com/eNPIQ9f.png" group-title="English",BBC News HD (720p) [Not 24/7]
|
||||
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
|
||||
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-country="RU" tvg-language="Russian" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Russian",ЛДПР ТВ (1080p)
|
||||
http://46.46.143.222:1935/live/mp4:ldpr.stream/playlist.m3u8
|
||||
`)
|
||||
})
|
||||
|
||||
it('can generate index.region.m3u', () => {
|
||||
expect(content('.gh-pages/index.region.m3u'))
|
||||
.toBe(`#EXTM3U x-tvg-url="https://iptv-org.github.io/epg/guides/ru/tv.yandex.ru.epg.xml"
|
||||
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-country="RU" tvg-language="Russian" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Asia",ЛДПР ТВ (1080p)
|
||||
http://46.46.143.222:1935/live/mp4:ldpr.stream/playlist.m3u8
|
||||
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-country="RU" tvg-language="Russian" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Commonwealth of Independent States",ЛДПР ТВ (1080p)
|
||||
http://46.46.143.222:1935/live/mp4:ldpr.stream/playlist.m3u8
|
||||
#EXTINF:-1 tvg-id="BBCNews.uk" tvg-country="UK" tvg-language="English" tvg-logo="https://i.imgur.com/eNPIQ9f.png" group-title="Europe",BBC News HD (720p) [Not 24/7]
|
||||
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
|
||||
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-country="RU" tvg-language="Russian" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Europe",ЛДПР ТВ (1080p)
|
||||
http://46.46.143.222:1935/live/mp4:ldpr.stream/playlist.m3u8
|
||||
#EXTINF:-1 tvg-id="BBCNews.uk" tvg-country="UK" tvg-language="English" tvg-logo="https://i.imgur.com/eNPIQ9f.png" group-title="Europe, the Middle East and Africa",BBC News HD (720p) [Not 24/7]
|
||||
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
|
||||
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-country="RU" tvg-language="Russian" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Europe, the Middle East and Africa",ЛДПР ТВ (1080p)
|
||||
http://46.46.143.222:1935/live/mp4:ldpr.stream/playlist.m3u8
|
||||
`)
|
||||
})
|
||||
|
||||
it('can generate index.m3u', () => {
|
||||
expect(content('.gh-pages/index.m3u'))
|
||||
.toBe(`#EXTM3U x-tvg-url="https://iptv-org.github.io/epg/guides/ru/tv.yandex.ru.epg.xml"
|
||||
#EXTINF:-1 tvg-id="BBCNews.uk" tvg-country="UK" tvg-language="English" tvg-logo="https://i.imgur.com/eNPIQ9f.png" group-title="News",BBC News HD (720p) [Not 24/7]
|
||||
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
|
||||
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-country="RU" tvg-language="Russian" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
|
||||
http://46.46.143.222:1935/live/mp4:ldpr.stream/playlist.m3u8
|
||||
`)
|
||||
})
|
||||
|
||||
it('can generate index.nsfw.m3u', () => {
|
||||
expect(content('.gh-pages/index.nsfw.m3u'))
|
||||
.toBe(`#EXTM3U x-tvg-url="https://iptv-org.github.io/epg/guides/ru/tv.yandex.ru.epg.xml"
|
||||
#EXTINF:-1 tvg-id="BBCNews.uk" tvg-country="UK" tvg-language="English" tvg-logo="https://i.imgur.com/eNPIQ9f.png" group-title="News",BBC News HD (720p) [Not 24/7]
|
||||
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
|
||||
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-country="RU" tvg-language="Russian" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
|
||||
http://46.46.143.222:1935/live/mp4:ldpr.stream/playlist.m3u8
|
||||
`)
|
||||
})
|
||||
|
||||
it('can generate logs categories', () => {
|
||||
expect(content('logs/generate-playlists/categories.log'))
|
||||
.toBe(`{"name":"General","slug":"general","count":1}
|
||||
{"name":"News","slug":"news","count":1}
|
||||
{"name":"Other","slug":"other","count":0}
|
||||
`)
|
||||
})
|
||||
|
||||
it('can generate logs countries', () => {
|
||||
expect(content('logs/generate-playlists/countries.log'))
|
||||
.toBe(`{"name":"Andorra","code":"AD","count":0}
|
||||
{"name":"Russia","code":"RU","count":1}
|
||||
{"name":"United Kingdom","code":"UK","count":1}
|
||||
{"name":"International","code":"INT","count":0}
|
||||
{"name":"Undefined","code":"UNDEFINED","count":0}
|
||||
`)
|
||||
})
|
||||
|
||||
it('can generate logs languages', () => {
|
||||
expect(content('logs/generate-playlists/languages.log'))
|
||||
.toBe(`{"name":"Catalan","code":"cat","count":0}
|
||||
{"name":"English","code":"eng","count":1}
|
||||
{"name":"Russian","code":"rus","count":1}
|
||||
{"name":"Undefined","code":"undefined","count":0}
|
||||
`)
|
||||
})
|
||||
|
||||
it('can generate logs regions', () => {
|
||||
expect(content('logs/generate-playlists/regions.log'))
|
||||
.toBe(`{"name":"Asia","code":"ASIA","count":1}
|
||||
{"name":"Commonwealth of Independent States","code":"CIS","count":1}
|
||||
{"name":"Europe","code":"EUR","count":2}
|
||||
{"name":"Europe, the Middle East and Africa","code":"EMEA","count":2}
|
||||
{"name":"Undefined","code":"UNDEFINED","count":0}
|
||||
`)
|
||||
})
|
|
@ -0,0 +1,97 @@
|
|||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { execSync } = require('child_process')
|
||||
|
||||
beforeEach(() => {
|
||||
fs.rmdirSync('tests/__data__/temp', { recursive: true })
|
||||
fs.mkdirSync('tests/__data__/temp')
|
||||
fs.copyFileSync('tests/__data__/input/test.db', 'tests/__data__/temp/test.db')
|
||||
})
|
||||
|
||||
it('can update database', () => {
|
||||
const result = execSync(
|
||||
'DB_FILEPATH=tests/__data__/temp/test.db LOGS_PATH=tests/__data__/input/logs EPG_CODES_FILEPATH=tests/__data__/input/codes.json node scripts/commands/update-database.js',
|
||||
{ encoding: 'utf8' }
|
||||
)
|
||||
const database = fs.readFileSync('tests/__data__/temp/test.db', { encoding: 'utf8' })
|
||||
const lines = database.split('\n')
|
||||
expect(JSON.parse(lines[0])).toMatchObject({
|
||||
name: 'ЛДПР ТВ',
|
||||
id: 'LDPRTV.ru',
|
||||
filepath: 'tests/__data__/output/channels/ru.m3u',
|
||||
src_country: { name: 'Russia', code: 'RU', lang: 'rus' },
|
||||
tvg_country: 'RU',
|
||||
countries: [{ name: 'Russia', code: 'RU', lang: 'rus' }],
|
||||
regions: [
|
||||
{ name: 'Asia', code: 'ASIA' },
|
||||
{ name: 'Commonwealth of Independent States', code: 'CIS' },
|
||||
{ name: 'Europe, the Middle East and Africa', code: 'EMEA' },
|
||||
{ name: 'Europe', code: 'EUR' }
|
||||
],
|
||||
languages: [{ name: 'Russian', code: 'rus' }],
|
||||
categories: [{ name: 'General', slug: 'general', nsfw: false }],
|
||||
tvg_url: '',
|
||||
guides: ['https://iptv-org.github.io/epg/guides/ru/tv.yandex.ru.epg.xml'],
|
||||
logo: 'https://iptvx.one/icn/ldpr-tv.png',
|
||||
resolution: { height: 1080, width: 1920 },
|
||||
status: { label: '', code: 'online', level: 1 },
|
||||
url: 'http://46.46.143.222:1935/live/mp4:ldpr.stream/playlist.m3u8',
|
||||
http: { referrer: '', 'user-agent': '' },
|
||||
is_nsfw: false,
|
||||
is_broken: false,
|
||||
updated: true,
|
||||
cluster_id: 1,
|
||||
_id: '2ST8btby3mmsgPF0'
|
||||
})
|
||||
expect(JSON.parse(lines[1])).toMatchObject({
|
||||
name: 'BBC News HD',
|
||||
id: 'BBCNews.uk',
|
||||
filepath: 'tests/__data__/output/channels/uk.m3u',
|
||||
src_country: { name: 'United Kingdom', code: 'UK', lang: 'eng' },
|
||||
tvg_country: 'UK',
|
||||
countries: [{ name: 'United Kingdom', code: 'UK', lang: 'eng' }],
|
||||
regions: [
|
||||
{ name: 'Europe, the Middle East and Africa', code: 'EMEA' },
|
||||
{ name: 'Europe', code: 'EUR' }
|
||||
],
|
||||
languages: [{ name: 'English', code: 'eng' }],
|
||||
categories: [{ name: 'News', slug: 'news', nsfw: false }],
|
||||
tvg_url: '',
|
||||
guides: [],
|
||||
logo: 'https://i.imgur.com/eNPIQ9f.png',
|
||||
resolution: { height: 720, width: null },
|
||||
status: { label: 'Not 24/7', code: 'not_247', level: 3 },
|
||||
url: 'http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8',
|
||||
http: { referrer: '', 'user-agent': '' },
|
||||
is_nsfw: false,
|
||||
is_broken: false,
|
||||
updated: false,
|
||||
cluster_id: 3,
|
||||
_id: '3TbieV1ptnZVCIdn'
|
||||
})
|
||||
expect(JSON.parse(lines[2])).toMatchObject({
|
||||
name: 'ATV',
|
||||
id: 'AndorraTV.ad',
|
||||
filepath: 'tests/__data__/output/channels/ad.m3u',
|
||||
src_country: { name: 'Andorra', code: 'AD', lang: 'cat' },
|
||||
tvg_country: 'AD',
|
||||
countries: [{ name: 'Andorra', code: 'AD', lang: 'cat' }],
|
||||
regions: [
|
||||
{ name: 'Europe, the Middle East and Africa', code: 'EMEA' },
|
||||
{ name: 'Europe', code: 'EUR' }
|
||||
],
|
||||
languages: [{ name: 'Catalan', code: 'cat' }],
|
||||
categories: [{ name: 'General', slug: 'general', nsfw: false }],
|
||||
tvg_url: '',
|
||||
guides: ['https://iptv-org.github.io/epg/guides/ad/andorradifusio.ad.epg.xml'],
|
||||
logo: 'https://i.imgur.com/kJCjeQ4.png',
|
||||
resolution: { height: 720, width: null },
|
||||
status: { label: 'Timeout', code: 'timeout', level: 4 },
|
||||
url: 'https://iptv-all.lanesh4d0w.repl.co/andorra/atv',
|
||||
http: { referrer: '', 'user-agent': '' },
|
||||
is_nsfw: false,
|
||||
is_broken: true,
|
||||
updated: true,
|
||||
cluster_id: 1
|
||||
})
|
||||
})
|
|
@ -0,0 +1,37 @@
|
|||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { execSync } = require('child_process')
|
||||
|
||||
beforeEach(() => {
|
||||
fs.copyFileSync('tests/__data__/input/test.db', 'tests/__data__/temp/test.db')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmdirSync('tests/__data__/temp', { recursive: true })
|
||||
fs.mkdirSync('tests/__data__/temp')
|
||||
})
|
||||
|
||||
it('can update playlist', () => {
|
||||
const result = execSync(
|
||||
'DB_FILEPATH=tests/__data__/temp/test.db node scripts/commands/update-playlists.js',
|
||||
{ encoding: 'utf8' }
|
||||
)
|
||||
|
||||
const adPlaylist = fs.readFileSync('tests/__data__/output/channels/ad.m3u', {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
|
||||
expect(adPlaylist).toBe(`#EXTM3U
|
||||
#EXTINF:-1 tvg-id="AndorraTV.ad" tvg-country="AD" tvg-language="Catalan" tvg-logo="https://i.imgur.com/kJCjeQ4.png" group-title="General",ATV (720p) [Offline]
|
||||
https://iptv-all.lanesh4d0w.repl.co/andorra/atv
|
||||
`)
|
||||
|
||||
const ruPlaylist = fs.readFileSync('tests/__data__/output/channels/ru.m3u', {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
|
||||
expect(ruPlaylist).toBe(`#EXTM3U
|
||||
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-country="RU" tvg-language="Russian" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
|
||||
http://46.46.143.222:1935/live/mp4:ldpr.stream/playlist.m3u8
|
||||
`)
|
||||
})
|
|
@ -0,0 +1,23 @@
|
|||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { execSync } = require('child_process')
|
||||
|
||||
beforeEach(() => {
|
||||
fs.rmdirSync(path.resolve('tests/__data__/output'), { recursive: true })
|
||||
})
|
||||
|
||||
it('can update readme.md', () => {
|
||||
const result = execSync(
|
||||
'LOGS_PATH=tests/__data__/input/logs node scripts/commands/update-readme.js --config=tests/__data__/input/readme.json',
|
||||
{ encoding: 'utf8' }
|
||||
)
|
||||
|
||||
const readme = fs.readFileSync(path.resolve('tests/__data__/output/readme.md'), {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
const expected = fs.readFileSync(path.resolve('tests/__data__/input/readme.md'), {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
|
||||
expect(readme).toBe(expected)
|
||||
})
|
|
@ -0,0 +1,16 @@
|
|||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { execSync } = require('child_process')
|
||||
|
||||
it('can validate channels name', () => {
|
||||
try {
|
||||
execSync('node scripts/commands/validate.js --input-dir=tests/__data__/input/channels', {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
} catch (err) {
|
||||
expect(err.status).toBe(1)
|
||||
expect(err.stdout).toBe(
|
||||
`tests/__data__/input/channels/us_blocked.m3u:2 'Fox Sports' is on the blocklist due to claims of copyright holders (https://github.com/github/dmca/blob/master/2020/09/2020-09-16-dfl.md)\n\n`
|
||||
)
|
||||
}
|
||||
})
|
Loading…
Reference in New Issue