Merge branch 'master' into vtv-patch

This commit is contained in:
Aleksandr Statciuk 2021-12-13 23:56:19 +03:00 committed by GitHub
commit e0f776b4b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
97 changed files with 4605 additions and 8158 deletions

View File

@ -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

View File

@ -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

View File

@ -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 }}

7
.gitignore vendored
View File

@ -1 +1,6 @@
node_modules
node_modules
database
.artifacts
.secrets
.actrc
.DS_Store

3
.readme/.gitignore vendored
View File

@ -1,3 +1,4 @@
_categories.md
_countries.md
_languages.md
_languages.md
_regions.md

View File

@ -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 |

View File

@ -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. |

View File

@ -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",

View File

@ -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.

5405
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

2
scripts/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
logs/
channels.db

View File

@ -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()

View File

@ -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()

View File

@ -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
}

View File

@ -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()

View File

@ -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')
}

View File

@ -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 []
}

View File

@ -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()

View File

@ -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))
}
}

View File

@ -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()

19
scripts/core/checker.js Normal file
View File

@ -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

61
scripts/core/db.js Normal file
View File

@ -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

67
scripts/core/file.js Normal file
View File

@ -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

114
scripts/core/generator.js Normal file
View 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

10
scripts/core/index.js Normal file
View File

@ -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')

42
scripts/core/logger.js Normal file
View File

@ -0,0 +1,42 @@
const { createLogger, format, transports, addColors } = require('winston')
const { combine, timestamp, printf } = format
const consoleFormat = ({ level, message, timestamp }) => {
if (typeof message === 'object') return JSON.stringify(message)
return message
}
const config = {
levels: {
error: 0,
warn: 1,
info: 2,
failed: 3,
success: 4,
http: 5,
verbose: 6,
debug: 7,
silly: 8
},
colors: {
info: 'white',
success: 'green',
failed: 'red'
}
}
const t = [
new transports.Console({
format: format.combine(format.printf(consoleFormat))
})
]
const logger = createLogger({
transports: t,
levels: config.levels,
level: 'verbose'
})
addColors(config.colors)
module.exports = logger

39
scripts/core/markdown.js Normal file
View File

@ -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

31
scripts/core/parser.js Normal file
View File

@ -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

49
scripts/core/playlist.js Normal file
View File

@ -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

56
scripts/core/store.js Normal file
View File

@ -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
}
}
}
}

29
scripts/core/timer.js Normal file
View File

@ -0,0 +1,29 @@
const { performance } = require('perf_hooks')
const dayjs = require('dayjs')
const duration = require('dayjs/plugin/duration')
const relativeTime = require('dayjs/plugin/relativeTime')
dayjs.extend(relativeTime)
dayjs.extend(duration)
const timer = {}
let t0 = 0
timer.start = function () {
t0 = performance.now()
}
timer.format = function (f) {
let t1 = performance.now()
return dayjs.duration(t1 - t0).format(f)
}
timer.humanize = function (suffix = true) {
let t1 = performance.now()
return dayjs.duration(t1 - t0).humanize(suffix)
}
module.exports = timer

View File

@ -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)
})

1
scripts/data/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
codes.json

View File

@ -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$"
}
]

632
scripts/data/blocklist.json Normal file
View File

@ -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"
}
]

View File

@ -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

View File

@ -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",

View File

@ -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
}
}

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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
}
}
}
}

View File

@ -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())
}
}
}

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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

View 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

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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 ''
}

View File

@ -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')

View File

@ -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
}

View File

@ -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(';') : ''
}

View File

@ -0,0 +1,3 @@
module.exports = function () {
return this.id || ''
}

View File

@ -0,0 +1,3 @@
module.exports = function () {
return Array.isArray(this.languages) ? this.languages.map(i => i.name).join(';') : ''
}

View File

@ -0,0 +1,3 @@
module.exports = function () {
return this.logo || ''
}

View File

@ -0,0 +1,3 @@
module.exports = function () {
return this.guides.length ? this.guides[0] : ''
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -0,0 +1,3 @@
module.exports = function ({ tvg_url, guides = [] }) {
return tvg_url ? [tvg_url] : guides
}

View File

@ -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')

View File

@ -0,0 +1,7 @@
module.exports = function ({ is_broken = false, status }) {
if (status) {
return status.level > 3 ? true : false
}
return is_broken
}

View File

@ -0,0 +1,3 @@
module.exports = function ({ categories }) {
return Array.isArray(categories) ? categories.filter(c => c.nsfw).length > 0 : false
}

View File

@ -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
}

View File

@ -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(' ')
}

View File

@ -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')
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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, '+')
}

View File

@ -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}&nbsp;` : ''
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()

2
tests/__data__/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
output/
temp/

View File

@ -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

View File

@ -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

View File

@ -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"}]

View File

@ -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"}}]}

View File

@ -0,0 +1,3 @@
{"name":"General","slug":"general","count":1}
{"name":"News","slug":"news","count":1}
{"name":"Other","slug":"other","count":0}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -0,0 +1,4 @@
{
"build" : "tests/__data__/output/readme.md",
"files" : ["./.readme/template.md"]
}

View File

@ -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).

View File

@ -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"}

View File

@ -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: []
})
})

View File

@ -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"}`
)
})

View File

@ -0,0 +1,45 @@
const fs = require('fs')
const path = require('path')
const { execSync } = require('child_process')
beforeEach(() => {
fs.rmdirSync('tests/__data__/output', { recursive: true })
fs.mkdirSync('tests/__data__/output')
})
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
})
})

View File

@ -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')
})

View File

@ -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}
`)
})

View File

@ -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
})
})

View File

@ -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
`)
})

View File

@ -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)
})

View File

@ -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`
)
}
})