mirror of
https://github.com/iptv-org/epg
synced 2026-05-09 02:47:00 -04:00
Merge branch 'master' into pr/2821
This commit is contained in:
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Enforce the usage of CRLF in GitHub Actions per ESLint configuration.
|
||||||
|
* text eol=crlf
|
||||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -5,11 +5,4 @@
|
|||||||
/guide.xml.gz
|
/guide.xml.gz
|
||||||
|
|
||||||
# macOS
|
# macOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# If Yarn is used (yarn.lock)
|
|
||||||
/.yarn/*
|
|
||||||
.yarnrc
|
|
||||||
.yarnrc.yml
|
|
||||||
|
|
||||||
.idea
|
|
||||||
474
README.md
474
README.md
@@ -1,237 +1,237 @@
|
|||||||
# EPG [](https://github.com/iptv-org/epg/actions/workflows/update.yml)
|
# EPG [](https://github.com/iptv-org/epg/actions/workflows/update.yml)
|
||||||
|
|
||||||
Tools for downloading the EPG (Electronic Program Guide) for thousands of TV channels from hundreds of sources.
|
Tools for downloading the EPG (Electronic Program Guide) for thousands of TV channels from hundreds of sources.
|
||||||
|
|
||||||
## Table of contents
|
## Table of contents
|
||||||
|
|
||||||
- ✨ [Installation](#installation)
|
- ✨ [Installation](#installation)
|
||||||
- 🚀 [Usage](#usage)
|
- 🚀 [Usage](#usage)
|
||||||
- 💫 [Update](#update)
|
- 💫 [Update](#update)
|
||||||
- 🐋 [Docker](#docker)
|
- 🐋 [Docker](#docker)
|
||||||
- 📺 [Playlists](#playlists)
|
- 📺 [Playlists](#playlists)
|
||||||
- 🗄 [Database](#database)
|
- 🗄 [Database](#database)
|
||||||
- 👨💻 [API](#api)
|
- 👨💻 [API](#api)
|
||||||
- 📚 [Resources](#resources)
|
- 📚 [Resources](#resources)
|
||||||
- 💬 [Discussions](#discussions)
|
- 💬 [Discussions](#discussions)
|
||||||
- 🛠 [Contribution](#contribution)
|
- 🛠 [Contribution](#contribution)
|
||||||
- 📄 [License](#license)
|
- 📄 [License](#license)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
First, you need to install [Node.js](https://nodejs.org/en) on your computer. You will also need to install [Git](https://git-scm.com/downloads) to follow these instructions.
|
First, you need to install [Node.js](https://nodejs.org/en) on your computer. You will also need to install [Git](https://git-scm.com/downloads) to follow these instructions.
|
||||||
|
|
||||||
After that open the [Console](https://en.wikipedia.org/wiki/Windows_Console) (or [Terminal](<https://en.wikipedia.org/wiki/Terminal_(macOS)>) if you have macOS) and type the following command:
|
After that open the [Console](https://en.wikipedia.org/wiki/Windows_Console) (or [Terminal](<https://en.wikipedia.org/wiki/Terminal_(macOS)>) if you have macOS) and type the following command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone --depth 1 -b master https://github.com/iptv-org/epg.git
|
git clone --depth 1 -b master https://github.com/iptv-org/epg.git
|
||||||
```
|
```
|
||||||
|
|
||||||
Then navigate to the downloaded `epg` folder:
|
Then navigate to the downloaded `epg` folder:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd epg
|
cd epg
|
||||||
```
|
```
|
||||||
|
|
||||||
And install all the dependencies:
|
And install all the dependencies:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
To start the download of the guide, select one of the supported sites from [SITES.md](SITES.md) file and paste its name into the command below:
|
To start the download of the guide, select one of the supported sites from [SITES.md](SITES.md) file and paste its name into the command below:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run grab --- --site=example.com
|
npm run grab --- --site=example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
Then run it and wait for the guide to finish downloading. When finished, a new `guide.xml` file will appear in the current directory.
|
Then run it and wait for the guide to finish downloading. When finished, a new `guide.xml` file will appear in the current directory.
|
||||||
|
|
||||||
You can also customize the behavior of the script using this options:
|
You can also customize the behavior of the script using this options:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
Usage: npm run grab --- [options]
|
Usage: npm run grab --- [options]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-s, --site <name> Name of the site to parse
|
-s, --site <name> Name of the site to parse
|
||||||
-c, --channels <path> Path to *.channels.xml file (required if the "--site" attribute is
|
-c, --channels <path> Path to *.channels.xml file (required if the "--site" attribute is
|
||||||
not specified)
|
not specified)
|
||||||
-o, --output <path> Path to output file (default: "guide.xml")
|
-o, --output <path> Path to output file (default: "guide.xml")
|
||||||
-l, --lang <codes> Allows you to restrict downloading to channels in specified languages only (example: "en,id")
|
-l, --lang <codes> Allows you to restrict downloading to channels in specified languages only (example: "en,id")
|
||||||
-t, --timeout <milliseconds> Timeout for each request in milliseconds (default: 0)
|
-t, --timeout <milliseconds> Timeout for each request in milliseconds (default: 0)
|
||||||
-d, --delay <milliseconds> Delay between request in milliseconds (default: 0)
|
-d, --delay <milliseconds> Delay between request in milliseconds (default: 0)
|
||||||
-x, --proxy <url> Use the specified proxy (example: "socks5://username:password@127.0.0.1:1234")
|
-x, --proxy <url> Use the specified proxy (example: "socks5://username:password@127.0.0.1:1234")
|
||||||
--days <days> Number of days for which the program will be loaded (defaults to the value from the site config)
|
--days <days> Number of days for which the program will be loaded (defaults to the value from the site config)
|
||||||
--maxConnections <number> Number of concurrent requests (default: 1)
|
--maxConnections <number> Number of concurrent requests (default: 1)
|
||||||
--gzip Specifies whether or not to create a compressed version of the guide (default: false)
|
--gzip Specifies whether or not to create a compressed version of the guide (default: false)
|
||||||
--curl Display each request as CURL (default: false)
|
--curl Display each request as CURL (default: false)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Parallel downloading
|
### Parallel downloading
|
||||||
|
|
||||||
By default, the guide for each channel is downloaded one by one, but you can change this behavior by increasing the number of simultaneous requests using the `--maxConnections` attribute:
|
By default, the guide for each channel is downloaded one by one, but you can change this behavior by increasing the number of simultaneous requests using the `--maxConnections` attribute:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run grab --- --site=example.com --maxConnections=10
|
npm run grab --- --site=example.com --maxConnections=10
|
||||||
```
|
```
|
||||||
|
|
||||||
But be aware that under heavy load, some sites may start return an error or completely block your access.
|
But be aware that under heavy load, some sites may start return an error or completely block your access.
|
||||||
|
|
||||||
### Use custom channel list
|
### Use custom channel list
|
||||||
|
|
||||||
Create an XML file and copy the descriptions of all the channels you need from the [/sites](sites) into it:
|
Create an XML file and copy the descriptions of all the channels you need from the [/sites](sites) into it:
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<channels>
|
<channels>
|
||||||
<channel site="arirang.com" lang="en" xmltv_id="ArirangTV.kr" site_id="CH_K">Arirang TV</channel>
|
<channel site="arirang.com" lang="en" xmltv_id="ArirangTV.kr" site_id="CH_K">Arirang TV</channel>
|
||||||
...
|
...
|
||||||
</channels>
|
</channels>
|
||||||
```
|
```
|
||||||
|
|
||||||
And then specify the path to that file via the `--channels` attribute:
|
And then specify the path to that file via the `--channels` attribute:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run grab --- --channels=path/to/custom.channels.xml
|
npm run grab --- --channels=path/to/custom.channels.xml
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run on schedule
|
### Run on schedule
|
||||||
|
|
||||||
If you want to download guides on a schedule, you can use [cron](https://en.wikipedia.org/wiki/Cron) or any other task scheduler. Currently, we use a tool called `chronos` for this purpose.
|
If you want to download guides on a schedule, you can use [cron](https://en.wikipedia.org/wiki/Cron) or any other task scheduler. Currently, we use a tool called `chronos` for this purpose.
|
||||||
|
|
||||||
To start it, you only need to specify the necessary `grab` command and [cron expression](https://crontab.guru/):
|
To start it, you only need to specify the necessary `grab` command and [cron expression](https://crontab.guru/):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npx chronos --execute="npm run grab --- --site=example.com" --pattern="0 0,12 * * *" --log
|
npx chronos --execute="npm run grab --- --site=example.com" --pattern="0 0,12 * * *" --log
|
||||||
```
|
```
|
||||||
|
|
||||||
For more info go to [chronos](https://github.com/freearhey/chronos) documentation.
|
For more info go to [chronos](https://github.com/freearhey/chronos) documentation.
|
||||||
|
|
||||||
### Access the guide by URL
|
### Access the guide by URL
|
||||||
|
|
||||||
You can make the guide available via URL by running your own server. The easiest way to do this is to run this command:
|
You can make the guide available via URL by running your own server. The easiest way to do this is to run this command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npx serve
|
npx serve
|
||||||
```
|
```
|
||||||
|
|
||||||
After that, the guide will be available at the link:
|
After that, the guide will be available at the link:
|
||||||
|
|
||||||
```
|
```
|
||||||
http://localhost:3000/guide.xml
|
http://localhost:3000/guide.xml
|
||||||
```
|
```
|
||||||
|
|
||||||
In addition it will be available to other devices on the same local network at the address:
|
In addition it will be available to other devices on the same local network at the address:
|
||||||
|
|
||||||
```
|
```
|
||||||
http://<your_local_ip_address>:3000/guide.xml
|
http://<your_local_ip_address>:3000/guide.xml
|
||||||
```
|
```
|
||||||
|
|
||||||
For more info go to [serve](https://github.com/vercel/serve) documentation.
|
For more info go to [serve](https://github.com/vercel/serve) documentation.
|
||||||
|
|
||||||
## Update
|
## Update
|
||||||
|
|
||||||
If you have downloaded the repository code according to the instructions above, then to update it will be enough to run the command:
|
If you have downloaded the repository code according to the instructions above, then to update it will be enough to run the command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git pull
|
git pull
|
||||||
```
|
```
|
||||||
|
|
||||||
And then update all the dependencies:
|
And then update all the dependencies:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
### Build an image
|
### Build an image
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker build -t iptv-org/epg --no-cache .
|
docker build -t iptv-org/epg --no-cache .
|
||||||
```
|
```
|
||||||
|
|
||||||
### Create and run container
|
### Create and run container
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker run -p 3000:3000 -v /path/to/channels.xml:/epg/channels.xml iptv-org/epg
|
docker run -p 3000:3000 -v /path/to/channels.xml:/epg/channels.xml iptv-org/epg
|
||||||
```
|
```
|
||||||
|
|
||||||
By default, the guide will be downloaded every day at 00:00 UTC and saved to the `/epg/public/guide.xml` file inside the container.
|
By default, the guide will be downloaded every day at 00:00 UTC and saved to the `/epg/public/guide.xml` file inside the container.
|
||||||
|
|
||||||
From the outside, it will be available at this link:
|
From the outside, it will be available at this link:
|
||||||
|
|
||||||
```
|
```
|
||||||
http://localhost:3000/guide.xml
|
http://localhost:3000/guide.xml
|
||||||
```
|
```
|
||||||
|
|
||||||
or
|
or
|
||||||
|
|
||||||
```
|
```
|
||||||
http://<your_local_ip_address>:3000/guide.xml
|
http://<your_local_ip_address>:3000/guide.xml
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment Variables
|
### Environment Variables
|
||||||
|
|
||||||
To fine-tune the execution, you can pass environment variables to the container as follows:
|
To fine-tune the execution, you can pass environment variables to the container as follows:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker run \
|
docker run \
|
||||||
-p 5000:3000 \
|
-p 5000:3000 \
|
||||||
-v /path/to/channels.xml:/epg/channels.xml \
|
-v /path/to/channels.xml:/epg/channels.xml \
|
||||||
-e CRON_SCHEDULE="0 0,12 * * *" \
|
-e CRON_SCHEDULE="0 0,12 * * *" \
|
||||||
-e MAX_CONNECTIONS=10 \
|
-e MAX_CONNECTIONS=10 \
|
||||||
-e GZIP=true \
|
-e GZIP=true \
|
||||||
-e CURL=true \
|
-e CURL=true \
|
||||||
-e PROXY="socks5://127.0.0.1:1234" \
|
-e PROXY="socks5://127.0.0.1:1234" \
|
||||||
-e DAYS=14 \
|
-e DAYS=14 \
|
||||||
-e TIMEOUT=5 \
|
-e TIMEOUT=5 \
|
||||||
-e DELAY=2 \
|
-e DELAY=2 \
|
||||||
iptv-org/epg
|
iptv-org/epg
|
||||||
```
|
```
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
| --------------- | ------------------------------------------------------------------------------------------------------------------ |
|
| --------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||||
| CRON_SCHEDULE | A [cron expression](https://crontab.guru/) describing the schedule of the guide loadings (default: "0 0 \* \* \*") |
|
| CRON_SCHEDULE | A [cron expression](https://crontab.guru/) describing the schedule of the guide loadings (default: "0 0 \* \* \*") |
|
||||||
| MAX_CONNECTIONS | Limit on the number of concurrent requests (default: 1) |
|
| MAX_CONNECTIONS | Limit on the number of concurrent requests (default: 1) |
|
||||||
| GZIP | Boolean value indicating whether to create a compressed version of the guide (default: false) |
|
| GZIP | Boolean value indicating whether to create a compressed version of the guide (default: false) |
|
||||||
| CURL | Display each request as CURL (default: false) |
|
| CURL | Display each request as CURL (default: false) |
|
||||||
| PROXY | Use the specified proxy |
|
| PROXY | Use the specified proxy |
|
||||||
| DAYS | Number of days for which the guide will be loaded (defaults to the value from the site config) |
|
| DAYS | Number of days for which the guide will be loaded (defaults to the value from the site config) |
|
||||||
| TIMEOUT | Timeout for each request in milliseconds (default: 0) |
|
| TIMEOUT | Timeout for each request in milliseconds (default: 0) |
|
||||||
| DELAY | Delay between request in milliseconds (default: 0) |
|
| DELAY | Delay between request in milliseconds (default: 0) |
|
||||||
|
|
||||||
## Database
|
## Database
|
||||||
|
|
||||||
All channel data is taken from the [iptv-org/database](https://github.com/iptv-org/database) repository. If you find any errors please open a new [issue](https://github.com/iptv-org/database/issues) there.
|
All channel data is taken from the [iptv-org/database](https://github.com/iptv-org/database) repository. If you find any errors please open a new [issue](https://github.com/iptv-org/database/issues) there.
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
The API documentation can be found in the [iptv-org/api](https://github.com/iptv-org/api) repository.
|
The API documentation can be found in the [iptv-org/api](https://github.com/iptv-org/api) repository.
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
Links to other useful IPTV-related resources can be found in the [iptv-org/awesome-iptv](https://github.com/iptv-org/awesome-iptv) repository.
|
Links to other useful IPTV-related resources can be found in the [iptv-org/awesome-iptv](https://github.com/iptv-org/awesome-iptv) repository.
|
||||||
|
|
||||||
## Discussions
|
## Discussions
|
||||||
|
|
||||||
If you have a question or an idea, you can post it in the [Discussions](https://github.com/orgs/iptv-org/discussions) tab.
|
If you have a question or an idea, you can post it in the [Discussions](https://github.com/orgs/iptv-org/discussions) tab.
|
||||||
|
|
||||||
## Contribution
|
## Contribution
|
||||||
|
|
||||||
Please make sure to read the [Contributing Guide](https://github.com/iptv-org/epg/blob/master/CONTRIBUTING.md) before sending [issue](https://github.com/iptv-org/epg/issues) or a [pull request](https://github.com/iptv-org/epg/pulls).
|
Please make sure to read the [Contributing Guide](https://github.com/iptv-org/epg/blob/master/CONTRIBUTING.md) before sending [issue](https://github.com/iptv-org/epg/issues) or a [pull request](https://github.com/iptv-org/epg/pulls).
|
||||||
|
|
||||||
And thank you to everyone who has already contributed!
|
And thank you to everyone who has already contributed!
|
||||||
|
|
||||||
### Backers
|
### Backers
|
||||||
|
|
||||||
<a href="https://opencollective.com/iptv-org"><img src="https://opencollective.com/iptv-org/backers.svg?width=890" /></a>
|
<a href="https://opencollective.com/iptv-org"><img src="https://opencollective.com/iptv-org/backers.svg?width=890" /></a>
|
||||||
|
|
||||||
### Contributors
|
### Contributors
|
||||||
|
|
||||||
<a href="https://github.com/iptv-org/epg/graphs/contributors"><img src="https://opencollective.com/iptv-org/contributors.svg?width=890" /></a>
|
<a href="https://github.com/iptv-org/epg/graphs/contributors"><img src="https://opencollective.com/iptv-org/contributors.svg?width=890" /></a>
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
|||||||
484
SITES.md
484
SITES.md
@@ -1,242 +1,242 @@
|
|||||||
# Sites
|
# Sites
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th align="left">Site</th><th align="left" colspan="2">Channels<br>(total / with xmltv-id)</th><th align="left">Status</th><th align="left">Notes</th></tr>
|
<tr><th align="left">Site</th><th align="left" colspan="2">Channels<br>(total / with xmltv-id)</th><th align="left">Status</th><th align="left">Notes</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td><a href="sites/9tv.co.il">9tv.co.il</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/9tv.co.il">9tv.co.il</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/abc.net.au">abc.net.au</a></td><td align="right">548</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/abc.net.au">abc.net.au</a></td><td align="right">548</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/allente.dk">allente.dk</a></td><td align="right">74</td><td align="right">43</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/allente.dk">allente.dk</a></td><td align="right">74</td><td align="right">43</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/allente.fi">allente.fi</a></td><td align="right">71</td><td align="right">24</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/allente.fi">allente.fi</a></td><td align="right">71</td><td align="right">24</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/allente.no">allente.no</a></td><td align="right">84</td><td align="right">52</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/allente.no">allente.no</a></td><td align="right">84</td><td align="right">52</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/allente.se">allente.se</a></td><td align="right">92</td><td align="right">91</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/allente.se">allente.se</a></td><td align="right">92</td><td align="right">91</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/andorradifusio.ad">andorradifusio.ad</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/andorradifusio.ad">andorradifusio.ad</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/anteltv.com.uy">anteltv.com.uy</a></td><td align="right">53</td><td align="right">47</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/anteltv.com.uy">anteltv.com.uy</a></td><td align="right">53</td><td align="right">47</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/antennaeurope.gr">antennaeurope.gr</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/antennaeurope.gr">antennaeurope.gr</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/antennapacific.gr">antennapacific.gr</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/antennapacific.gr">antennapacific.gr</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/antennasatellite.gr">antennasatellite.gr</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/antennasatellite.gr">antennasatellite.gr</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/arianaafgtv.com">arianaafgtv.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/arianaafgtv.com">arianaafgtv.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/arianatelevision.com">arianatelevision.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/arianatelevision.com">arianatelevision.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/arirang.com">arirang.com</a></td><td align="right">3</td><td align="right">3</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/arirang.com">arirang.com</a></td><td align="right">3</td><td align="right">3</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/artonline.tv">artonline.tv</a></td><td align="right">5</td><td align="right">5</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/artonline.tv">artonline.tv</a></td><td align="right">5</td><td align="right">5</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/awilime.com">awilime.com</a></td><td align="right">111</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/awilime.com">awilime.com</a></td><td align="right">111</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/bein.com">bein.com</a></td><td align="right">160</td><td align="right">160</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/bein.com">bein.com</a></td><td align="right">160</td><td align="right">160</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/beinsports.com">beinsports.com</a></td><td align="right">104</td><td align="right">81</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/beinsports.com">beinsports.com</a></td><td align="right">104</td><td align="right">81</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/berrymedia.co.kr">berrymedia.co.kr</a></td><td align="right">5</td><td align="right">5</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/berrymedia.co.kr">berrymedia.co.kr</a></td><td align="right">5</td><td align="right">5</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/cableplus.com.uy">cableplus.com.uy</a></td><td align="right">171</td><td align="right">47</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/cableplus.com.uy">cableplus.com.uy</a></td><td align="right">171</td><td align="right">47</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/canalplus.com">canalplus.com</a></td><td align="right">11720</td><td align="right">212</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/canalplus.com">canalplus.com</a></td><td align="right">11720</td><td align="right">212</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/cgates.lt">cgates.lt</a></td><td align="right">102</td><td align="right">61</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/cgates.lt">cgates.lt</a></td><td align="right">102</td><td align="right">61</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/chada.ma">chada.ma</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/chada.ma">chada.ma</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/chaines-tv.orange.fr">chaines-tv.orange.fr</a></td><td align="right">295</td><td align="right">146</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/chaines-tv.orange.fr">chaines-tv.orange.fr</a></td><td align="right">295</td><td align="right">146</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/clickthecity.com">clickthecity.com</a></td><td align="right">32</td><td align="right">30</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/clickthecity.com">clickthecity.com</a></td><td align="right">32</td><td align="right">30</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/content.astro.com.my">content.astro.com.my</a></td><td align="right">157</td><td align="right">112</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/content.astro.com.my">content.astro.com.my</a></td><td align="right">157</td><td align="right">112</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/cosmotetv.gr">cosmotetv.gr</a></td><td align="right">108</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/cosmotetv.gr">cosmotetv.gr</a></td><td align="right">108</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/ctc.ru">ctc.ru</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/ctc.ru">ctc.ru</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/cubmu.com">cubmu.com</a></td><td align="right">174</td><td align="right">122</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/cubmu.com">cubmu.com</a></td><td align="right">174</td><td align="right">122</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/cyta.com.cy">cyta.com.cy</a></td><td align="right">116</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/cyta.com.cy">cyta.com.cy</a></td><td align="right">116</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/dens.tv">dens.tv</a></td><td align="right">67</td><td align="right">64</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/dens.tv">dens.tv</a></td><td align="right">67</td><td align="right">64</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/derana.lk">derana.lk</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/derana.lk">derana.lk</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/digea.gr">digea.gr</a></td><td align="right">92</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/digea.gr">digea.gr</a></td><td align="right">92</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/digiturk.com.tr">digiturk.com.tr</a></td><td align="right">108</td><td align="right">107</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/digiturk.com.tr">digiturk.com.tr</a></td><td align="right">108</td><td align="right">107</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/directv.com">directv.com</a></td><td align="right">1043</td><td align="right">696</td><td align="center">🔴</td><td>https://github.com/iptv-org/epg/issues/2284</td></tr>
|
<tr><td><a href="sites/directv.com">directv.com</a></td><td align="right">1043</td><td align="right">696</td><td align="center">🔴</td><td>https://github.com/iptv-org/epg/issues/2284</td></tr>
|
||||||
<tr><td><a href="sites/directv.com.ar">directv.com.ar</a></td><td align="right">412</td><td align="right">229</td><td align="center">🔴</td><td>https://github.com/iptv-org/epg/issues/2339</td></tr>
|
<tr><td><a href="sites/directv.com.ar">directv.com.ar</a></td><td align="right">412</td><td align="right">229</td><td align="center">🔴</td><td>https://github.com/iptv-org/epg/issues/2339</td></tr>
|
||||||
<tr><td><a href="sites/directv.com.uy">directv.com.uy</a></td><td align="right">143</td><td align="right">142</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/directv.com.uy">directv.com.uy</a></td><td align="right">143</td><td align="right">142</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/dishtv.in">dishtv.in</a></td><td align="right">448</td><td align="right">89</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/dishtv.in">dishtv.in</a></td><td align="right">448</td><td align="right">89</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/dna.fi">dna.fi</a></td><td align="right">122</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/dna.fi">dna.fi</a></td><td align="right">122</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/dsmart.com.tr">dsmart.com.tr</a></td><td align="right">104</td><td align="right">90</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/dsmart.com.tr">dsmart.com.tr</a></td><td align="right">104</td><td align="right">90</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/dstv.com">dstv.com</a></td><td align="right">6983</td><td align="right">181</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/dstv.com">dstv.com</a></td><td align="right">6983</td><td align="right">181</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/dtv8.net">dtv8.net</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/dtv8.net">dtv8.net</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/elcinema.com">elcinema.com</a></td><td align="right">262</td><td align="right">226</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/elcinema.com">elcinema.com</a></td><td align="right">262</td><td align="right">226</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/ena.skylifetv.co.kr">ena.skylifetv.co.kr</a></td><td align="right">6</td><td align="right">6</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/ena.skylifetv.co.kr">ena.skylifetv.co.kr</a></td><td align="right">6</td><td align="right">6</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/energeek.cl">energeek.cl</a></td><td align="right">6</td><td align="right">2</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/energeek.cl">energeek.cl</a></td><td align="right">6</td><td align="right">2</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/entertainment.ie">entertainment.ie</a></td><td align="right">109</td><td align="right">95</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/entertainment.ie">entertainment.ie</a></td><td align="right">109</td><td align="right">95</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/epg.112114.xyz">epg.112114.xyz</a></td><td align="right">930</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/epg.112114.xyz">epg.112114.xyz</a></td><td align="right">930</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/epg.iptvx.one">epg.iptvx.one</a></td><td align="right">2862</td><td align="right">747</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/epg.iptvx.one">epg.iptvx.one</a></td><td align="right">2862</td><td align="right">747</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/epg.telemach.ba">epg.telemach.ba</a></td><td align="right">259</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/epg.telemach.ba">epg.telemach.ba</a></td><td align="right">259</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/epg.telemach.me">epg.telemach.me</a></td><td align="right">216</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/epg.telemach.me">epg.telemach.me</a></td><td align="right">216</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/epgmaster.com">epgmaster.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/epgmaster.com">epgmaster.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/epgshare01.online">epgshare01.online</a></td><td align="right">20971</td><td align="right">17</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/epgshare01.online">epgshare01.online</a></td><td align="right">20971</td><td align="right">17</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/firstmedia.com">firstmedia.com</a></td><td align="right">116</td><td align="right">101</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/firstmedia.com">firstmedia.com</a></td><td align="right">116</td><td align="right">101</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/foxsports.com.au">foxsports.com.au</a></td><td align="right">7</td><td align="right">7</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/foxsports.com.au">foxsports.com.au</a></td><td align="right">7</td><td align="right">7</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/foxtel.com.au">foxtel.com.au</a></td><td align="right">99</td><td align="right">61</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/foxtel.com.au">foxtel.com.au</a></td><td align="right">99</td><td align="right">61</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/freetv.tv">freetv.tv</a></td><td align="right">7</td><td align="right">7</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/freetv.tv">freetv.tv</a></td><td align="right">7</td><td align="right">7</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/freeview.co.uk">freeview.co.uk</a></td><td align="right">171</td><td align="right">100</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/freeview.co.uk">freeview.co.uk</a></td><td align="right">171</td><td align="right">100</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/frikanalen.no">frikanalen.no</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/frikanalen.no">frikanalen.no</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/galamtv.kz">galamtv.kz</a></td><td align="right">27</td><td align="right">22</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/galamtv.kz">galamtv.kz</a></td><td align="right">27</td><td align="right">22</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/gatotv.com">gatotv.com</a></td><td align="right">475</td><td align="right">362</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/gatotv.com">gatotv.com</a></td><td align="right">475</td><td align="right">362</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/getafteritmedia.com">getafteritmedia.com</a></td><td align="right">5</td><td align="right">5</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/getafteritmedia.com">getafteritmedia.com</a></td><td align="right">5</td><td align="right">5</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/gigatv.3bbtv.co.th">gigatv.3bbtv.co.th</a></td><td align="right">79</td><td align="right">38</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/gigatv.3bbtv.co.th">gigatv.3bbtv.co.th</a></td><td align="right">79</td><td align="right">38</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/guiadetv.com">guiadetv.com</a></td><td align="right">124</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/guiadetv.com">guiadetv.com</a></td><td align="right">124</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/guida.tv">guida.tv</a></td><td align="right">88</td><td align="right">88</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/guida.tv">guida.tv</a></td><td align="right">88</td><td align="right">88</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/guidatv.sky.it">guidatv.sky.it</a></td><td align="right">168</td><td align="right">153</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/guidatv.sky.it">guidatv.sky.it</a></td><td align="right">168</td><td align="right">153</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/guidetnt.com">guidetnt.com</a></td><td align="right">69</td><td align="right">69</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/guidetnt.com">guidetnt.com</a></td><td align="right">69</td><td align="right">69</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/horizon.tv">horizon.tv</a></td><td align="right">184</td><td align="right">172</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/horizon.tv">horizon.tv</a></td><td align="right">184</td><td align="right">172</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/hoy.tv">hoy.tv</a></td><td align="right">3</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/hoy.tv">hoy.tv</a></td><td align="right">3</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/i.mjh.nz">i.mjh.nz</a></td><td align="right">6458</td><td align="right">1489</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/i.mjh.nz">i.mjh.nz</a></td><td align="right">6458</td><td align="right">1489</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/i24news.tv">i24news.tv</a></td><td align="right">4</td><td align="right">3</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/i24news.tv">i24news.tv</a></td><td align="right">4</td><td align="right">3</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/iltalehti.fi">iltalehti.fi</a></td><td align="right">142</td><td align="right">44</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/iltalehti.fi">iltalehti.fi</a></td><td align="right">142</td><td align="right">44</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/indihometv.com">indihometv.com</a></td><td align="right">130</td><td align="right">124</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/indihometv.com">indihometv.com</a></td><td align="right">130</td><td align="right">124</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/ionplustv.com">ionplustv.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/ionplustv.com">ionplustv.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/ipko.tv">ipko.tv</a></td><td align="right">194</td><td align="right">152</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/ipko.tv">ipko.tv</a></td><td align="right">194</td><td align="right">152</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/jiotv.com">jiotv.com</a></td><td align="right">1094</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/jiotv.com">jiotv.com</a></td><td align="right">1094</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/kan.org.il">kan.org.il</a></td><td align="right">3</td><td align="right">3</td><td align="center">🔴</td><td>https://github.com/iptv-org/epg/issues/2273</td></tr>
|
<tr><td><a href="sites/kan.org.il">kan.org.il</a></td><td align="right">3</td><td align="right">3</td><td align="center">🔴</td><td>https://github.com/iptv-org/epg/issues/2273</td></tr>
|
||||||
<tr><td><a href="sites/knr.gl">knr.gl</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/knr.gl">knr.gl</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/kvf.fo">kvf.fo</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/kvf.fo">kvf.fo</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/m.tv.sms.cz">m.tv.sms.cz</a></td><td align="right">1027</td><td align="right">450</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/m.tv.sms.cz">m.tv.sms.cz</a></td><td align="right">1027</td><td align="right">450</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/m.tving.com">m.tving.com</a></td><td align="right">30</td><td align="right">26</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/m.tving.com">m.tving.com</a></td><td align="right">30</td><td align="right">26</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/magticom.ge">magticom.ge</a></td><td align="right">240</td><td align="right">110</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/magticom.ge">magticom.ge</a></td><td align="right">240</td><td align="right">110</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/mako.co.il">mako.co.il</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/mako.co.il">mako.co.il</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/makrodigitaltelevision.com">makrodigitaltelevision.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/makrodigitaltelevision.com">makrodigitaltelevision.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/maxtvgo.mk">maxtvgo.mk</a></td><td align="right">110</td><td align="right">48</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/maxtvgo.mk">maxtvgo.mk</a></td><td align="right">110</td><td align="right">48</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/mediagenie.co.kr">mediagenie.co.kr</a></td><td align="right">5</td><td align="right">4</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/mediagenie.co.kr">mediagenie.co.kr</a></td><td align="right">5</td><td align="right">4</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/mediaklikk.hu">mediaklikk.hu</a></td><td align="right">8</td><td align="right">8</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/mediaklikk.hu">mediaklikk.hu</a></td><td align="right">8</td><td align="right">8</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/mediasetinfinity.mediaset.it">mediasetinfinity.mediaset.it</a></td><td align="right">13</td><td align="right">13</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/mediasetinfinity.mediaset.it">mediasetinfinity.mediaset.it</a></td><td align="right">13</td><td align="right">13</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/melita.com">melita.com</a></td><td align="right">127</td><td align="right">111</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/melita.com">melita.com</a></td><td align="right">127</td><td align="right">111</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/meo.pt">meo.pt</a></td><td align="right">216</td><td align="right">192</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/meo.pt">meo.pt</a></td><td align="right">216</td><td align="right">192</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/meuguia.tv">meuguia.tv</a></td><td align="right">102</td><td align="right">97</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/meuguia.tv">meuguia.tv</a></td><td align="right">102</td><td align="right">97</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/mewatch.sg">mewatch.sg</a></td><td align="right">25</td><td align="right">24</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/mewatch.sg">mewatch.sg</a></td><td align="right">25</td><td align="right">24</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/mi.tv">mi.tv</a></td><td align="right">2084</td><td align="right">620</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/mi.tv">mi.tv</a></td><td align="right">2084</td><td align="right">620</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/mncvision.id">mncvision.id</a></td><td align="right">276</td><td align="right">223</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/mncvision.id">mncvision.id</a></td><td align="right">276</td><td align="right">223</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/moji.id">moji.id</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/moji.id">moji.id</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/mojmaxtv.hrvatskitelekom.hr">mojmaxtv.hrvatskitelekom.hr</a></td><td align="right">243</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/mojmaxtv.hrvatskitelekom.hr">mojmaxtv.hrvatskitelekom.hr</a></td><td align="right">243</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/mon-programme-tv.be">mon-programme-tv.be</a></td><td align="right">111</td><td align="right">95</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/mon-programme-tv.be">mon-programme-tv.be</a></td><td align="right">111</td><td align="right">95</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/movistarplus.es">movistarplus.es</a></td><td align="right">178</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/movistarplus.es">movistarplus.es</a></td><td align="right">178</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/mtel.ba">mtel.ba</a></td><td align="right">501</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/mtel.ba">mtel.ba</a></td><td align="right">501</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/mts.rs">mts.rs</a></td><td align="right">457</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/mts.rs">mts.rs</a></td><td align="right">457</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/mujtvprogram.cz">mujtvprogram.cz</a></td><td align="right">216</td><td align="right">202</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/mujtvprogram.cz">mujtvprogram.cz</a></td><td align="right">216</td><td align="right">202</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/musor.tv">musor.tv</a></td><td align="right">181</td><td align="right">145</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/musor.tv">musor.tv</a></td><td align="right">181</td><td align="right">145</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/mysky.com.ph">mysky.com.ph</a></td><td align="right">115</td><td align="right">43</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/mysky.com.ph">mysky.com.ph</a></td><td align="right">115</td><td align="right">43</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/mytelly.co.uk">mytelly.co.uk</a></td><td align="right">488</td><td align="right">401</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/mytelly.co.uk">mytelly.co.uk</a></td><td align="right">488</td><td align="right">401</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/mytvsuper.com">mytvsuper.com</a></td><td align="right">108</td><td align="right">99</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/mytvsuper.com">mytvsuper.com</a></td><td align="right">108</td><td align="right">99</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/neo.io">neo.io</a></td><td align="right">337</td><td align="right">241</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/neo.io">neo.io</a></td><td align="right">337</td><td align="right">241</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/nhkworldpremium.com">nhkworldpremium.com</a></td><td align="right">2</td><td align="right">2</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/nhkworldpremium.com">nhkworldpremium.com</a></td><td align="right">2</td><td align="right">2</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/nhl.com">nhl.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/nhl.com">nhl.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/nostv.pt">nostv.pt</a></td><td align="right">168</td><td align="right">155</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/nostv.pt">nostv.pt</a></td><td align="right">168</td><td align="right">155</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/novacyprus.com">novacyprus.com</a></td><td align="right">29</td><td align="right">24</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/novacyprus.com">novacyprus.com</a></td><td align="right">29</td><td align="right">24</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/novasports.gr">novasports.gr</a></td><td align="right">16</td><td align="right">16</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/novasports.gr">novasports.gr</a></td><td align="right">16</td><td align="right">16</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/nowplayer.now.com">nowplayer.now.com</a></td><td align="right">288</td><td align="right">229</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/nowplayer.now.com">nowplayer.now.com</a></td><td align="right">288</td><td align="right">229</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/nuevosiglo.com.uy">nuevosiglo.com.uy</a></td><td align="right">173</td><td align="right">47</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/nuevosiglo.com.uy">nuevosiglo.com.uy</a></td><td align="right">173</td><td align="right">47</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/nzxmltv.com">nzxmltv.com</a></td><td align="right">532</td><td align="right">118</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/nzxmltv.com">nzxmltv.com</a></td><td align="right">532</td><td align="right">118</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/ontvtonight.com">ontvtonight.com</a></td><td align="right">5177</td><td align="right">532</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/ontvtonight.com">ontvtonight.com</a></td><td align="right">5177</td><td align="right">532</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/opto.sic.pt">opto.sic.pt</a></td><td align="right">4</td><td align="right">4</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/opto.sic.pt">opto.sic.pt</a></td><td align="right">4</td><td align="right">4</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/orangetv.orange.es">orangetv.orange.es</a></td><td align="right">168</td><td align="right">165</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/orangetv.orange.es">orangetv.orange.es</a></td><td align="right">168</td><td align="right">165</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/osn.com">osn.com</a></td><td align="right">118</td><td align="right">98</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/osn.com">osn.com</a></td><td align="right">118</td><td align="right">98</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/pbsguam.org">pbsguam.org</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/pbsguam.org">pbsguam.org</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/pickx.be">pickx.be</a></td><td align="right">404</td><td align="right">391</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/pickx.be">pickx.be</a></td><td align="right">404</td><td align="right">391</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/player.ee.co.uk">player.ee.co.uk</a></td><td align="right">241</td><td align="right">206</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/player.ee.co.uk">player.ee.co.uk</a></td><td align="right">241</td><td align="right">206</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/playtv.unifi.com.my">playtv.unifi.com.my</a></td><td align="right">66</td><td align="right">61</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/playtv.unifi.com.my">playtv.unifi.com.my</a></td><td align="right">66</td><td align="right">61</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/plex.tv">plex.tv</a></td><td align="right">170</td><td align="right">119</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/plex.tv">plex.tv</a></td><td align="right">170</td><td align="right">119</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/pluto.tv">pluto.tv</a></td><td align="right">3302</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/pluto.tv">pluto.tv</a></td><td align="right">3302</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/programacion-tv.elpais.com">programacion-tv.elpais.com</a></td><td align="right">195</td><td align="right">104</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/programacion-tv.elpais.com">programacion-tv.elpais.com</a></td><td align="right">195</td><td align="right">104</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/programacion.tcc.com.uy">programacion.tcc.com.uy</a></td><td align="right">149</td><td align="right">56</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/programacion.tcc.com.uy">programacion.tcc.com.uy</a></td><td align="right">149</td><td align="right">56</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/programetv.ro">programetv.ro</a></td><td align="right">331</td><td align="right">224</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/programetv.ro">programetv.ro</a></td><td align="right">331</td><td align="right">224</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/programme-tv.net">programme-tv.net</a></td><td align="right">295</td><td align="right">197</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/programme-tv.net">programme-tv.net</a></td><td align="right">295</td><td align="right">197</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/programme-tv.vini.pf">programme-tv.vini.pf</a></td><td align="right">58</td><td align="right">2</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/programme-tv.vini.pf">programme-tv.vini.pf</a></td><td align="right">58</td><td align="right">2</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/programme.tvb.com">programme.tvb.com</a></td><td align="right">8</td><td align="right">6</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/programme.tvb.com">programme.tvb.com</a></td><td align="right">8</td><td align="right">6</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/programtv.onet.pl">programtv.onet.pl</a></td><td align="right">590</td><td align="right">362</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/programtv.onet.pl">programtv.onet.pl</a></td><td align="right">590</td><td align="right">362</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/raiplay.it">raiplay.it</a></td><td align="right">17</td><td align="right">13</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/raiplay.it">raiplay.it</a></td><td align="right">17</td><td align="right">13</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/reportv.com.ar">reportv.com.ar</a></td><td align="right">163</td><td align="right">97</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/reportv.com.ar">reportv.com.ar</a></td><td align="right">163</td><td align="right">97</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/rikstv.no">rikstv.no</a></td><td align="right">80</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/rikstv.no">rikstv.no</a></td><td align="right">80</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/rotana.net">rotana.net</a></td><td align="right">32</td><td align="right">28</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/rotana.net">rotana.net</a></td><td align="right">32</td><td align="right">28</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/rtb.gov.bn">rtb.gov.bn</a></td><td align="right">3</td><td align="right">3</td><td align="center">🔴</td><td>https://github.com/iptv-org/epg/issues/2257</td></tr>
|
<tr><td><a href="sites/rtb.gov.bn">rtb.gov.bn</a></td><td align="right">3</td><td align="right">3</td><td align="center">🔴</td><td>https://github.com/iptv-org/epg/issues/2257</td></tr>
|
||||||
<tr><td><a href="sites/rthk.hk">rthk.hk</a></td><td align="right">8</td><td align="right">8</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/rthk.hk">rthk.hk</a></td><td align="right">8</td><td align="right">8</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/rtmklik.rtm.gov.my">rtmklik.rtm.gov.my</a></td><td align="right">8</td><td align="right">6</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/rtmklik.rtm.gov.my">rtmklik.rtm.gov.my</a></td><td align="right">8</td><td align="right">6</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/rtp.pt">rtp.pt</a></td><td align="right">10</td><td align="right">10</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/rtp.pt">rtp.pt</a></td><td align="right">10</td><td align="right">10</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/ruv.is">ruv.is</a></td><td align="right">2</td><td align="right">2</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/ruv.is">ruv.is</a></td><td align="right">2</td><td align="right">2</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/s.mxtv.jp">s.mxtv.jp</a></td><td align="right">2</td><td align="right">2</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/s.mxtv.jp">s.mxtv.jp</a></td><td align="right">2</td><td align="right">2</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/sat.tv">sat.tv</a></td><td align="right">30308</td><td align="right">249</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/sat.tv">sat.tv</a></td><td align="right">30308</td><td align="right">249</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/shahid.mbc.net">shahid.mbc.net</a></td><td align="right">231</td><td align="right">165</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/shahid.mbc.net">shahid.mbc.net</a></td><td align="right">231</td><td align="right">165</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/siba.com.co">siba.com.co</a></td><td align="right">98</td><td align="right">96</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/siba.com.co">siba.com.co</a></td><td align="right">98</td><td align="right">96</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/singtel.com">singtel.com</a></td><td align="right">155</td><td align="right">113</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/singtel.com">singtel.com</a></td><td align="right">155</td><td align="right">113</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/sjonvarp.is">sjonvarp.is</a></td><td align="right">13</td><td align="right">13</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/sjonvarp.is">sjonvarp.is</a></td><td align="right">13</td><td align="right">13</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/sky.co.nz">sky.co.nz</a></td><td align="right">111</td><td align="right">93</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/sky.co.nz">sky.co.nz</a></td><td align="right">111</td><td align="right">93</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/sky.com">sky.com</a></td><td align="right">559</td><td align="right">458</td><td align="center">🟡</td><td>https://github.com/iptv-org/epg/issues/2763</td></tr>
|
<tr><td><a href="sites/sky.com">sky.com</a></td><td align="right">559</td><td align="right">458</td><td align="center">🟡</td><td>https://github.com/iptv-org/epg/issues/2763</td></tr>
|
||||||
<tr><td><a href="sites/sky.de">sky.de</a></td><td align="right">75</td><td align="right">75</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/sky.de">sky.de</a></td><td align="right">75</td><td align="right">75</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/skylife.co.kr">skylife.co.kr</a></td><td align="right">251</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/skylife.co.kr">skylife.co.kr</a></td><td align="right">251</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/skyperfectv.co.jp">skyperfectv.co.jp</a></td><td align="right">137</td><td align="right">130</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/skyperfectv.co.jp">skyperfectv.co.jp</a></td><td align="right">137</td><td align="right">130</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/snrt.ma">snrt.ma</a></td><td align="right">11</td><td align="right">7</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/snrt.ma">snrt.ma</a></td><td align="right">11</td><td align="right">7</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/sporttv.pt">sporttv.pt</a></td><td align="right">9</td><td align="right">8</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/sporttv.pt">sporttv.pt</a></td><td align="right">9</td><td align="right">8</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/starhubtvplus.com">starhubtvplus.com</a></td><td align="right">232</td><td align="right">208</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/starhubtvplus.com">starhubtvplus.com</a></td><td align="right">232</td><td align="right">208</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/startimestv.com">startimestv.com</a></td><td align="right">77</td><td align="right">58</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/startimestv.com">startimestv.com</a></td><td align="right">77</td><td align="right">58</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/stod2.is">stod2.is</a></td><td align="right">12</td><td align="right">8</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/stod2.is">stod2.is</a></td><td align="right">12</td><td align="right">8</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/streamingtvguides.com">streamingtvguides.com</a></td><td align="right">3066</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/streamingtvguides.com">streamingtvguides.com</a></td><td align="right">3066</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/superguidatv.it">superguidatv.it</a></td><td align="right">204</td><td align="right">163</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/superguidatv.it">superguidatv.it</a></td><td align="right">204</td><td align="right">163</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/taiwanplus.com">taiwanplus.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/taiwanplus.com">taiwanplus.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tapdmv.com">tapdmv.com</a></td><td align="right">39</td><td align="right">7</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tapdmv.com">tapdmv.com</a></td><td align="right">39</td><td align="right">7</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tataplay.com">tataplay.com</a></td><td align="right">785</td><td align="right">401</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tataplay.com">tataplay.com</a></td><td align="right">785</td><td align="right">401</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/telebilbao.es">telebilbao.es</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/telebilbao.es">telebilbao.es</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/teleboy.ch">teleboy.ch</a></td><td align="right">325</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/teleboy.ch">teleboy.ch</a></td><td align="right">325</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/telenet.tv">telenet.tv</a></td><td align="right">260</td><td align="right">91</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/telenet.tv">telenet.tv</a></td><td align="right">260</td><td align="right">91</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/teliatv.ee">teliatv.ee</a></td><td align="right">342</td><td align="right">233</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/teliatv.ee">teliatv.ee</a></td><td align="right">342</td><td align="right">233</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/telkussa.fi">telkussa.fi</a></td><td align="right">66</td><td align="right">32</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/telkussa.fi">telkussa.fi</a></td><td align="right">66</td><td align="right">32</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/telsu.fi">telsu.fi</a></td><td align="right">17</td><td align="right">15</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/telsu.fi">telsu.fi</a></td><td align="right">17</td><td align="right">15</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/thesportplus.com">thesportplus.com</a></td><td align="right">3</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/thesportplus.com">thesportplus.com</a></td><td align="right">3</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tivie.id">tivie.id</a></td><td align="right">45</td><td align="right">44</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tivie.id">tivie.id</a></td><td align="right">45</td><td align="right">44</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tivu.tv">tivu.tv</a></td><td align="right">69</td><td align="right">66</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tivu.tv">tivu.tv</a></td><td align="right">69</td><td align="right">66</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/toonamiaftermath.com">toonamiaftermath.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/toonamiaftermath.com">toonamiaftermath.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/turksatkablo.com.tr">turksatkablo.com.tr</a></td><td align="right">177</td><td align="right">118</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/turksatkablo.com.tr">turksatkablo.com.tr</a></td><td align="right">177</td><td align="right">118</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tv-programme.telecablesat.fr">tv-programme.telecablesat.fr</a></td><td align="right">268</td><td align="right">250</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tv-programme.telecablesat.fr">tv-programme.telecablesat.fr</a></td><td align="right">268</td><td align="right">250</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tv-spored.siol.net">tv-spored.siol.net</a></td><td align="right">312</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tv-spored.siol.net">tv-spored.siol.net</a></td><td align="right">312</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tv.blue.ch">tv.blue.ch</a></td><td align="right">1030</td><td align="right">565</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tv.blue.ch">tv.blue.ch</a></td><td align="right">1030</td><td align="right">565</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tv.cctv.com">tv.cctv.com</a></td><td align="right">94</td><td align="right">88</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tv.cctv.com">tv.cctv.com</a></td><td align="right">94</td><td align="right">88</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tv.dir.bg">tv.dir.bg</a></td><td align="right">111</td><td align="right">93</td><td align="center">🔴</td><td>https://github.com/iptv-org/epg/issues/2779</td></tr>
|
<tr><td><a href="sites/tv.dir.bg">tv.dir.bg</a></td><td align="right">111</td><td align="right">93</td><td align="center">🔴</td><td>https://github.com/iptv-org/epg/issues/2779</td></tr>
|
||||||
<tr><td><a href="sites/tv.lv">tv.lv</a></td><td align="right">137</td><td align="right">49</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tv.lv">tv.lv</a></td><td align="right">137</td><td align="right">49</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tv.magenta.at">tv.magenta.at</a></td><td align="right">307</td><td align="right">228</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tv.magenta.at">tv.magenta.at</a></td><td align="right">307</td><td align="right">228</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tv.mail.ru">tv.mail.ru</a></td><td align="right">664</td><td align="right">643</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tv.mail.ru">tv.mail.ru</a></td><td align="right">664</td><td align="right">643</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tv.movistar.com.pe">tv.movistar.com.pe</a></td><td align="right">282</td><td align="right">40</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tv.movistar.com.pe">tv.movistar.com.pe</a></td><td align="right">282</td><td align="right">40</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tv.nu">tv.nu</a></td><td align="right">199</td><td align="right">180</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tv.nu">tv.nu</a></td><td align="right">199</td><td align="right">180</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tv.post.lu">tv.post.lu</a></td><td align="right">332</td><td align="right">242</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tv.post.lu">tv.post.lu</a></td><td align="right">332</td><td align="right">242</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tv.sfr.fr">tv.sfr.fr</a></td><td align="right">489</td><td align="right">456</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tv.sfr.fr">tv.sfr.fr</a></td><td align="right">489</td><td align="right">456</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tv.trueid.net">tv.trueid.net</a></td><td align="right">266</td><td align="right">74</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tv.trueid.net">tv.trueid.net</a></td><td align="right">266</td><td align="right">74</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tv.yandex.ru">tv.yandex.ru</a></td><td align="right">97</td><td align="right">67</td><td align="center">🔴</td><td>https://github.com/iptv-org/epg/issues/2803</td></tr>
|
<tr><td><a href="sites/tv.yandex.ru">tv.yandex.ru</a></td><td align="right">97</td><td align="right">67</td><td align="center">🔴</td><td>https://github.com/iptv-org/epg/issues/2803</td></tr>
|
||||||
<tr><td><a href="sites/tv24.co.uk">tv24.co.uk</a></td><td align="right">1072</td><td align="right">39</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tv24.co.uk">tv24.co.uk</a></td><td align="right">1072</td><td align="right">39</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tv24.se">tv24.se</a></td><td align="right">326</td><td align="right">157</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tv24.se">tv24.se</a></td><td align="right">326</td><td align="right">157</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tv2go.t-2.net">tv2go.t-2.net</a></td><td align="right">335</td><td align="right">254</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tv2go.t-2.net">tv2go.t-2.net</a></td><td align="right">335</td><td align="right">254</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tvarenasport.com">tvarenasport.com</a></td><td align="right">14</td><td align="right">12</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tvarenasport.com">tvarenasport.com</a></td><td align="right">14</td><td align="right">12</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tvarenasport.hr">tvarenasport.hr</a></td><td align="right">10</td><td align="right">10</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tvarenasport.hr">tvarenasport.hr</a></td><td align="right">10</td><td align="right">10</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tvcesoir.fr">tvcesoir.fr</a></td><td align="right">135</td><td align="right">133</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tvcesoir.fr">tvcesoir.fr</a></td><td align="right">135</td><td align="right">133</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tvcubana.icrt.cu">tvcubana.icrt.cu</a></td><td align="right">10</td><td align="right">10</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tvcubana.icrt.cu">tvcubana.icrt.cu</a></td><td align="right">10</td><td align="right">10</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tvgids.nl">tvgids.nl</a></td><td align="right">115</td><td align="right">90</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tvgids.nl">tvgids.nl</a></td><td align="right">115</td><td align="right">90</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tvguide.com">tvguide.com</a></td><td align="right">153</td><td align="right">149</td><td align="center">🟡</td><td>https://github.com/iptv-org/epg/issues/2644</td></tr>
|
<tr><td><a href="sites/tvguide.com">tvguide.com</a></td><td align="right">153</td><td align="right">149</td><td align="center">🟡</td><td>https://github.com/iptv-org/epg/issues/2644</td></tr>
|
||||||
<tr><td><a href="sites/tvguide.myjcom.jp">tvguide.myjcom.jp</a></td><td align="right">145</td><td align="right">140</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tvguide.myjcom.jp">tvguide.myjcom.jp</a></td><td align="right">145</td><td align="right">140</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tvhebdo.com">tvhebdo.com</a></td><td align="right">317</td><td align="right">215</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tvhebdo.com">tvhebdo.com</a></td><td align="right">317</td><td align="right">215</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tvheute.at">tvheute.at</a></td><td align="right">53</td><td align="right">53</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tvheute.at">tvheute.at</a></td><td align="right">53</td><td align="right">53</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tvi.iol.pt">tvi.iol.pt</a></td><td align="right">6</td><td align="right">6</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tvi.iol.pt">tvi.iol.pt</a></td><td align="right">6</td><td align="right">6</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tvim.tv">tvim.tv</a></td><td align="right">25</td><td align="right">19</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tvim.tv">tvim.tv</a></td><td align="right">25</td><td align="right">19</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tvinsider.com">tvinsider.com</a></td><td align="right">374</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tvinsider.com">tvinsider.com</a></td><td align="right">374</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tvireland.ie">tvireland.ie</a></td><td align="right">334</td><td align="right">304</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tvireland.ie">tvireland.ie</a></td><td align="right">334</td><td align="right">304</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tvkaista.org">tvkaista.org</a></td><td align="right">149</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tvkaista.org">tvkaista.org</a></td><td align="right">149</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tvmi.mt">tvmi.mt</a></td><td align="right">3</td><td align="right">3</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tvmi.mt">tvmi.mt</a></td><td align="right">3</td><td align="right">3</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tvmusor.hu">tvmusor.hu</a></td><td align="right">99</td><td align="right">67</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tvmusor.hu">tvmusor.hu</a></td><td align="right">99</td><td align="right">67</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tvmustra.hu">tvmustra.hu</a></td><td align="right">188</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tvmustra.hu">tvmustra.hu</a></td><td align="right">188</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tvpassport.com">tvpassport.com</a></td><td align="right">19287</td><td align="right">2509</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tvpassport.com">tvpassport.com</a></td><td align="right">19287</td><td align="right">2509</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tvplus.com.tr">tvplus.com.tr</a></td><td align="right">143</td><td align="right">134</td><td align="center">🟢</td><td>https://github.com/iptv-org/epg/issues/2816</td></tr>
|
<tr><td><a href="sites/tvplus.com.tr">tvplus.com.tr</a></td><td align="right">150</td><td align="right">144</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tvprofil.com">tvprofil.com</a></td><td align="right">5836</td><td align="right">455</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tvprofil.com">tvprofil.com</a></td><td align="right">5836</td><td align="right">455</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/tvtv.us">tvtv.us</a></td><td align="right">2299</td><td align="right">2255</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/tvtv.us">tvtv.us</a></td><td align="right">2299</td><td align="right">2255</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/v3.myafn.dodmedia.osd.mil">v3.myafn.dodmedia.osd.mil</a></td><td align="right">8</td><td align="right">8</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/v3.myafn.dodmedia.osd.mil">v3.myafn.dodmedia.osd.mil</a></td><td align="right">8</td><td align="right">8</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/vidio.com">vidio.com</a></td><td align="right">57</td><td align="right">52</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/vidio.com">vidio.com</a></td><td align="right">57</td><td align="right">52</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/virginmediatelevision.ie">virginmediatelevision.ie</a></td><td align="right">5</td><td align="right">5</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/virginmediatelevision.ie">virginmediatelevision.ie</a></td><td align="right">5</td><td align="right">5</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/virgintvgo.virginmedia.com">virgintvgo.virginmedia.com</a></td><td align="right">238</td><td align="right">195</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/virgintvgo.virginmedia.com">virgintvgo.virginmedia.com</a></td><td align="right">238</td><td align="right">195</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/visionplus.id">visionplus.id</a></td><td align="right">250</td><td align="right">226</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/visionplus.id">visionplus.id</a></td><td align="right">250</td><td align="right">226</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/vivoplay.com.br">vivoplay.com.br</a></td><td align="right">389</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/vivoplay.com.br">vivoplay.com.br</a></td><td align="right">389</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/vtm.be">vtm.be</a></td><td align="right">7</td><td align="right">6</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/vtm.be">vtm.be</a></td><td align="right">7</td><td align="right">6</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/walesi.com.fj">walesi.com.fj</a></td><td align="right">9</td><td align="right">8</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/walesi.com.fj">walesi.com.fj</a></td><td align="right">9</td><td align="right">8</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/watch.sportsnet.ca">watch.sportsnet.ca</a></td><td align="right">8</td><td align="right">8</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/watch.sportsnet.ca">watch.sportsnet.ca</a></td><td align="right">8</td><td align="right">8</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/watchyour.tv">watchyour.tv</a></td><td align="right">40</td><td align="right">24</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/watchyour.tv">watchyour.tv</a></td><td align="right">40</td><td align="right">24</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/wavve.com">wavve.com</a></td><td align="right">77</td><td align="right">76</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/wavve.com">wavve.com</a></td><td align="right">77</td><td align="right">76</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/web.magentatv.de">web.magentatv.de</a></td><td align="right">348</td><td align="right">247</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/web.magentatv.de">web.magentatv.de</a></td><td align="right">348</td><td align="right">247</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/webtv.delta.nl">webtv.delta.nl</a></td><td align="right">247</td><td align="right">218</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/webtv.delta.nl">webtv.delta.nl</a></td><td align="right">247</td><td align="right">218</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/winplay.co">winplay.co</a></td><td align="right">2</td><td align="right">2</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/winplay.co">winplay.co</a></td><td align="right">2</td><td align="right">2</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/worldfishingnetwork.com">worldfishingnetwork.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/worldfishingnetwork.com">worldfishingnetwork.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/www3.nhk.or.jp">www3.nhk.or.jp</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/www3.nhk.or.jp">www3.nhk.or.jp</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/xem.kplus.vn">xem.kplus.vn</a></td><td align="right">77</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/xem.kplus.vn">xem.kplus.vn</a></td><td align="right">77</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/xumo.tv">xumo.tv</a></td><td align="right">350</td><td align="right">33</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/xumo.tv">xumo.tv</a></td><td align="right">350</td><td align="right">33</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/yes.co.il">yes.co.il</a></td><td align="right">174</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/yes.co.il">yes.co.il</a></td><td align="right">174</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/zap.co.ao">zap.co.ao</a></td><td align="right">114</td><td align="right">64</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/zap.co.ao">zap.co.ao</a></td><td align="right">114</td><td align="right">64</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/zap2it.com">zap2it.com</a></td><td align="right">595</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/zap2it.com">zap2it.com</a></td><td align="right">595</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/ziggogo.tv">ziggogo.tv</a></td><td align="right">152</td><td align="right">130</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/ziggogo.tv">ziggogo.tv</a></td><td align="right">152</td><td align="right">130</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/znbc.co.zm">znbc.co.zm</a></td><td align="right">4</td><td align="right">4</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/znbc.co.zm">znbc.co.zm</a></td><td align="right">4</td><td align="right">4</td><td align="center">🟢</td><td></td></tr>
|
||||||
<tr><td><a href="sites/zuragt.mn">zuragt.mn</a></td><td align="right">36</td><td align="right">25</td><td align="center">🟢</td><td></td></tr>
|
<tr><td><a href="sites/zuragt.mn">zuragt.mn</a></td><td align="right">36</td><td align="right">25</td><td align="center">🟢</td><td></td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1,55 +1,57 @@
|
|||||||
import typescriptEslint from '@typescript-eslint/eslint-plugin'
|
import typescriptEslint from '@typescript-eslint/eslint-plugin'
|
||||||
import globals from 'globals'
|
import stylistic from '@stylistic/eslint-plugin'
|
||||||
import tsParser from '@typescript-eslint/parser'
|
import globals from 'globals'
|
||||||
import path from 'node:path'
|
import tsParser from '@typescript-eslint/parser'
|
||||||
import { fileURLToPath } from 'node:url'
|
import path from 'node:path'
|
||||||
import js from '@eslint/js'
|
import { fileURLToPath } from 'node:url'
|
||||||
import { FlatCompat } from '@eslint/eslintrc'
|
import js from '@eslint/js'
|
||||||
|
import { FlatCompat } from '@eslint/eslintrc'
|
||||||
const __filename = fileURLToPath(import.meta.url)
|
|
||||||
const __dirname = path.dirname(__filename)
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
const compat = new FlatCompat({
|
const __dirname = path.dirname(__filename)
|
||||||
baseDirectory: __dirname,
|
const compat = new FlatCompat({
|
||||||
recommendedConfig: js.configs.recommended,
|
baseDirectory: __dirname,
|
||||||
allConfig: js.configs.all
|
recommendedConfig: js.configs.recommended,
|
||||||
})
|
allConfig: js.configs.all
|
||||||
|
})
|
||||||
export default [
|
|
||||||
...compat.extends('eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'),
|
export default [
|
||||||
{
|
...compat.extends('eslint:recommended', 'plugin:@typescript-eslint/strict', 'plugin:@typescript-eslint/stylistic', 'prettier'),
|
||||||
plugins: {
|
{
|
||||||
'@typescript-eslint': typescriptEslint
|
plugins: {
|
||||||
},
|
'@typescript-eslint': typescriptEslint,
|
||||||
|
'@stylistic': stylistic
|
||||||
languageOptions: {
|
},
|
||||||
globals: {
|
|
||||||
...globals.node,
|
languageOptions: {
|
||||||
...globals.jest
|
globals: {
|
||||||
},
|
...globals.node,
|
||||||
|
...globals.jest
|
||||||
parser: tsParser,
|
},
|
||||||
ecmaVersion: 'latest',
|
|
||||||
sourceType: 'module'
|
parser: tsParser,
|
||||||
},
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module'
|
||||||
rules: {
|
},
|
||||||
'@typescript-eslint/no-require-imports': 'off',
|
|
||||||
'@typescript-eslint/no-var-requires': 'off',
|
rules: {
|
||||||
'no-case-declarations': 'off',
|
'@typescript-eslint/no-require-imports': 'off',
|
||||||
'linebreak-style': ['error', 'windows'],
|
'@typescript-eslint/no-var-requires': 'off',
|
||||||
|
'no-case-declarations': 'off',
|
||||||
quotes: [
|
'@stylistic/linebreak-style': ['error', 'windows'],
|
||||||
'error',
|
|
||||||
'single',
|
quotes: [
|
||||||
{
|
'error',
|
||||||
avoidEscape: true
|
'single',
|
||||||
}
|
{
|
||||||
],
|
avoidEscape: true
|
||||||
|
}
|
||||||
semi: ['error', 'never']
|
],
|
||||||
}
|
|
||||||
},
|
semi: ['error', 'never']
|
||||||
{
|
}
|
||||||
ignores: ['tests/__data__/']
|
},
|
||||||
}
|
{
|
||||||
]
|
ignores: ['tests/__data__/']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|||||||
3968
package-lock.json
generated
3968
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@@ -38,7 +38,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alex_neo/jest-expect-message": "^1.0.5",
|
"@alex_neo/jest-expect-message": "^1.0.5",
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@eslint/js": "^9.30.0",
|
"@eslint/js": "^9.32.0",
|
||||||
"@freearhey/chronos": "^0.0.1",
|
"@freearhey/chronos": "^0.0.1",
|
||||||
"@freearhey/core": "^0.10.2",
|
"@freearhey/core": "^0.10.2",
|
||||||
"@freearhey/search-js": "^0.1.2",
|
"@freearhey/search-js": "^0.1.2",
|
||||||
@@ -46,33 +46,37 @@
|
|||||||
"@octokit/core": "^7.0.3",
|
"@octokit/core": "^7.0.3",
|
||||||
"@octokit/plugin-paginate-rest": "^13.1.1",
|
"@octokit/plugin-paginate-rest": "^13.1.1",
|
||||||
"@octokit/plugin-rest-endpoint-methods": "^16.0.0",
|
"@octokit/plugin-rest-endpoint-methods": "^16.0.0",
|
||||||
"@swc/core": "^1.13.0",
|
"@stylistic/eslint-plugin": "^5.2.2",
|
||||||
|
"@swc/core": "^1.13.2",
|
||||||
"@swc/jest": "^0.2.39",
|
"@swc/jest": "^0.2.39",
|
||||||
"@types/cli-progress": "^3.11.6",
|
"@types/cli-progress": "^3.11.6",
|
||||||
"@types/fs-extra": "^11.0.4",
|
"@types/fs-extra": "^11.0.4",
|
||||||
"@types/inquirer": "^9.0.8",
|
"@types/inquirer": "^9.0.8",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/langs": "^2.0.5",
|
"@types/langs": "^2.0.5",
|
||||||
"@types/lodash": "^4.17.19",
|
"@types/lodash.orderby": "^4.6.9",
|
||||||
"@types/node": "^24.0.14",
|
"@types/lodash.sortby": "^4.7.9",
|
||||||
|
"@types/lodash.startcase": "^4.4.9",
|
||||||
|
"@types/lodash.uniqby": "^4.7.9",
|
||||||
|
"@types/node": "^24.1.0",
|
||||||
"@types/node-cleanup": "^2.1.5",
|
"@types/node-cleanup": "^2.1.5",
|
||||||
"@types/numeral": "^2.0.5",
|
"@types/numeral": "^2.0.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.35.0",
|
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
||||||
"@typescript-eslint/parser": "^8.35.0",
|
"@typescript-eslint/parser": "^8.38.0",
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.11.0",
|
||||||
"axios-cookiejar-support": "^6.0.4",
|
"axios-cookiejar-support": "^6.0.4",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"cheerio": "^1.1.0",
|
"cheerio": "^1.1.2",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
"commander": "^14.0.0",
|
"commander": "^14.0.0",
|
||||||
"consola": "^3.4.2",
|
"consola": "^3.4.2",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^10.0.0",
|
||||||
"csv-parser": "^3.2.0",
|
"csv-parser": "^3.2.0",
|
||||||
"cwait": "^1.1.2",
|
"cwait": "^1.1.2",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"epg-grabber": "^0.41.0",
|
"epg-grabber": "^0.41.0",
|
||||||
"epg-parser": "^0.3.1",
|
"epg-parser": "^0.3.1",
|
||||||
"eslint": "^9.30.0",
|
"eslint": "^9.32.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"form-data": "^4.0.4",
|
"form-data": "^4.0.4",
|
||||||
"fs-extra": "^11.3.0",
|
"fs-extra": "^11.3.0",
|
||||||
@@ -80,12 +84,15 @@
|
|||||||
"globals": "^16.3.0",
|
"globals": "^16.3.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
"inquirer": "^12.7.0",
|
"inquirer": "^12.8.2",
|
||||||
"jest": "^30.0.3",
|
"jest": "^30.0.5",
|
||||||
"jest-offline": "^1.0.1",
|
"jest-offline": "^1.0.1",
|
||||||
"langs": "^2.0.0",
|
"langs": "^2.0.0",
|
||||||
"libxml2-wasm": "^0.5.0",
|
"libxml2-wasm": "^0.5.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash.orderby": "^4.6.0",
|
||||||
|
"lodash.sortby": "^4.7.0",
|
||||||
|
"lodash.startcase": "^4.4.0",
|
||||||
|
"lodash.uniqby": "^4.7.0",
|
||||||
"luxon": "^3.7.1",
|
"luxon": "^3.7.1",
|
||||||
"mockdate": "^3.0.5",
|
"mockdate": "^3.0.5",
|
||||||
"nedb-promises": "^6.2.3",
|
"nedb-promises": "^6.2.3",
|
||||||
@@ -110,6 +117,7 @@
|
|||||||
"tsx": "^4.20.3",
|
"tsx": "^4.20.3",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"unzipit": "^1.4.3",
|
"unzipit": "^1.4.3",
|
||||||
|
"uuid": "^11.1.0",
|
||||||
"wildcard-match": "^5.1.4"
|
"wildcard-match": "^5.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,41 @@
|
|||||||
import { Logger, Collection, Storage } from '@freearhey/core'
|
import { Logger, Collection, Storage } from '@freearhey/core'
|
||||||
import { SITES_DIR, API_DIR } from '../../constants'
|
import { SITES_DIR, API_DIR } from '../../constants'
|
||||||
import { GuideChannel } from '../../models'
|
import { GuideChannel } from '../../models'
|
||||||
import { ChannelsParser } from '../../core'
|
import { ChannelsParser } from '../../core'
|
||||||
import epgGrabber from 'epg-grabber'
|
import epgGrabber from 'epg-grabber'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const logger = new Logger()
|
const logger = new Logger()
|
||||||
|
|
||||||
logger.start('staring...')
|
logger.start('staring...')
|
||||||
|
|
||||||
logger.info('loading channels...')
|
logger.info('loading channels...')
|
||||||
const sitesStorage = new Storage(SITES_DIR)
|
const sitesStorage = new Storage(SITES_DIR)
|
||||||
const parser = new ChannelsParser({
|
const parser = new ChannelsParser({
|
||||||
storage: sitesStorage
|
storage: sitesStorage
|
||||||
})
|
})
|
||||||
|
|
||||||
const files: string[] = await sitesStorage.list('**/*.channels.xml')
|
const files: string[] = await sitesStorage.list('**/*.channels.xml')
|
||||||
|
|
||||||
const channels = new Collection()
|
const channels = new Collection()
|
||||||
for (const filepath of files) {
|
for (const filepath of files) {
|
||||||
const channelList = await parser.parse(filepath)
|
const channelList = await parser.parse(filepath)
|
||||||
|
|
||||||
channelList.channels.forEach((data: epgGrabber.Channel) => {
|
channelList.channels.forEach((data: epgGrabber.Channel) => {
|
||||||
channels.add(new GuideChannel(data))
|
channels.add(new GuideChannel(data))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`found ${channels.count()} channel(s)`)
|
logger.info(`found ${channels.count()} channel(s)`)
|
||||||
|
|
||||||
const output = channels.map((channel: GuideChannel) => channel.toJSON())
|
const output = channels.map((channel: GuideChannel) => channel.toJSON())
|
||||||
|
|
||||||
const apiStorage = new Storage(API_DIR)
|
const apiStorage = new Storage(API_DIR)
|
||||||
const outputFilename = 'guides.json'
|
const outputFilename = 'guides.json'
|
||||||
await apiStorage.save('guides.json', output.toJSON())
|
await apiStorage.save('guides.json', output.toJSON())
|
||||||
|
|
||||||
logger.info(`saved to "${path.join(API_DIR, outputFilename)}"`)
|
logger.info(`saved to "${path.join(API_DIR, outputFilename)}"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
import { DATA_DIR } from '../../constants'
|
import { DATA_DIR } from '../../constants'
|
||||||
import { Storage } from '@freearhey/core'
|
import { Storage } from '@freearhey/core'
|
||||||
import { DataLoader } from '../../core'
|
import { DataLoader } from '../../core'
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const storage = new Storage(DATA_DIR)
|
const storage = new Storage(DATA_DIR)
|
||||||
const loader = new DataLoader({ storage })
|
const loader = new DataLoader({ storage })
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
loader.download('blocklist.json'),
|
loader.download('blocklist.json'),
|
||||||
loader.download('categories.json'),
|
loader.download('categories.json'),
|
||||||
loader.download('channels.json'),
|
loader.download('channels.json'),
|
||||||
loader.download('countries.json'),
|
loader.download('countries.json'),
|
||||||
loader.download('languages.json'),
|
loader.download('languages.json'),
|
||||||
loader.download('regions.json'),
|
loader.download('regions.json'),
|
||||||
loader.download('subdivisions.json'),
|
loader.download('subdivisions.json'),
|
||||||
loader.download('feeds.json'),
|
loader.download('feeds.json'),
|
||||||
loader.download('timezones.json'),
|
loader.download('timezones.json'),
|
||||||
loader.download('guides.json'),
|
loader.download('guides.json'),
|
||||||
loader.download('streams.json'),
|
loader.download('streams.json'),
|
||||||
loader.download('logos.json')
|
loader.download('logos.json')
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,216 +1,216 @@
|
|||||||
import { Storage, Collection, Logger, Dictionary } from '@freearhey/core'
|
import { Storage, Collection, Logger, Dictionary } from '@freearhey/core'
|
||||||
import type { DataProcessorData } from '../../types/dataProcessor'
|
import type { DataProcessorData } from '../../types/dataProcessor'
|
||||||
import type { DataLoaderData } from '../../types/dataLoader'
|
import type { DataLoaderData } from '../../types/dataLoader'
|
||||||
import { ChannelSearchableData } from '../../types/channel'
|
import { ChannelSearchableData } from '../../types/channel'
|
||||||
import { Channel, ChannelList, Feed } from '../../models'
|
import { Channel, ChannelList, Feed } from '../../models'
|
||||||
import { DataProcessor, DataLoader } from '../../core'
|
import { DataProcessor, DataLoader } from '../../core'
|
||||||
import { select, input } from '@inquirer/prompts'
|
import { select, input } from '@inquirer/prompts'
|
||||||
import { ChannelsParser } from '../../core'
|
import { ChannelsParser } from '../../core'
|
||||||
import { DATA_DIR } from '../../constants'
|
import { DATA_DIR } from '../../constants'
|
||||||
import nodeCleanup from 'node-cleanup'
|
import nodeCleanup from 'node-cleanup'
|
||||||
import sjs from '@freearhey/search-js'
|
import sjs from '@freearhey/search-js'
|
||||||
import epgGrabber from 'epg-grabber'
|
import epgGrabber from 'epg-grabber'
|
||||||
import { Command } from 'commander'
|
import { Command } from 'commander'
|
||||||
import readline from 'readline'
|
import readline from 'readline'
|
||||||
|
|
||||||
type ChoiceValue = { type: string; value?: Feed | Channel }
|
interface ChoiceValue { type: string; value?: Feed | Channel }
|
||||||
type Choice = { name: string; short?: string; value: ChoiceValue; default?: boolean }
|
interface Choice { name: string; short?: string; value: ChoiceValue; default?: boolean }
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
readline
|
readline
|
||||||
.createInterface({
|
.createInterface({
|
||||||
input: process.stdin,
|
input: process.stdin,
|
||||||
output: process.stdout
|
output: process.stdout
|
||||||
})
|
})
|
||||||
.on('SIGINT', function () {
|
.on('SIGINT', function () {
|
||||||
process.emit('SIGINT')
|
process.emit('SIGINT')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const program = new Command()
|
const program = new Command()
|
||||||
|
|
||||||
program.argument('<filepath>', 'Path to *.channels.xml file to edit').parse(process.argv)
|
program.argument('<filepath>', 'Path to *.channels.xml file to edit').parse(process.argv)
|
||||||
|
|
||||||
const filepath = program.args[0]
|
const filepath = program.args[0]
|
||||||
const logger = new Logger()
|
const logger = new Logger()
|
||||||
const storage = new Storage()
|
const storage = new Storage()
|
||||||
let channelList = new ChannelList({ channels: [] })
|
let channelList = new ChannelList({ channels: [] })
|
||||||
|
|
||||||
main(filepath)
|
main(filepath)
|
||||||
nodeCleanup(() => {
|
nodeCleanup(() => {
|
||||||
save(filepath, channelList)
|
save(filepath, channelList)
|
||||||
})
|
})
|
||||||
|
|
||||||
export default async function main(filepath: string) {
|
export default async function main(filepath: string) {
|
||||||
if (!(await storage.exists(filepath))) {
|
if (!(await storage.exists(filepath))) {
|
||||||
throw new Error(`File "${filepath}" does not exists`)
|
throw new Error(`File "${filepath}" does not exists`)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('loading data from api...')
|
logger.info('loading data from api...')
|
||||||
const processor = new DataProcessor()
|
const processor = new DataProcessor()
|
||||||
const dataStorage = new Storage(DATA_DIR)
|
const dataStorage = new Storage(DATA_DIR)
|
||||||
const loader = new DataLoader({ storage: dataStorage })
|
const loader = new DataLoader({ storage: dataStorage })
|
||||||
const data: DataLoaderData = await loader.load()
|
const data: DataLoaderData = await loader.load()
|
||||||
const { channels, channelsKeyById, feedsGroupedByChannelId }: DataProcessorData =
|
const { channels, channelsKeyById, feedsGroupedByChannelId }: DataProcessorData =
|
||||||
processor.process(data)
|
processor.process(data)
|
||||||
|
|
||||||
logger.info('loading channels...')
|
logger.info('loading channels...')
|
||||||
const parser = new ChannelsParser({ storage })
|
const parser = new ChannelsParser({ storage })
|
||||||
channelList = await parser.parse(filepath)
|
channelList = await parser.parse(filepath)
|
||||||
const parsedChannelsWithoutId = channelList.channels.filter(
|
const parsedChannelsWithoutId = channelList.channels.filter(
|
||||||
(channel: epgGrabber.Channel) => !channel.xmltv_id
|
(channel: epgGrabber.Channel) => !channel.xmltv_id
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`found ${channelList.channels.count()} channels (including ${parsedChannelsWithoutId.count()} without ID)`
|
`found ${channelList.channels.count()} channels (including ${parsedChannelsWithoutId.count()} without ID)`
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info('creating search index...')
|
logger.info('creating search index...')
|
||||||
const items = channels.map((channel: Channel) => channel.getSearchable()).all()
|
const items = channels.map((channel: Channel) => channel.getSearchable()).all()
|
||||||
const searchIndex = sjs.createIndex(items, {
|
const searchIndex = sjs.createIndex(items, {
|
||||||
searchable: ['name', 'altNames', 'guideNames', 'streamNames', 'feedFullNames']
|
searchable: ['name', 'altNames', 'guideNames', 'streamNames', 'feedFullNames']
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.info('starting...\n')
|
logger.info('starting...\n')
|
||||||
|
|
||||||
for (const channel of parsedChannelsWithoutId.all()) {
|
for (const channel of parsedChannelsWithoutId.all()) {
|
||||||
try {
|
try {
|
||||||
channel.xmltv_id = await selectChannel(
|
channel.xmltv_id = await selectChannel(
|
||||||
channel,
|
channel,
|
||||||
searchIndex,
|
searchIndex,
|
||||||
feedsGroupedByChannelId,
|
feedsGroupedByChannelId,
|
||||||
channelsKeyById
|
channelsKeyById
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.info(err.message)
|
logger.info(err.message)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedChannelsWithoutId.forEach((channel: epgGrabber.Channel) => {
|
parsedChannelsWithoutId.forEach((channel: epgGrabber.Channel) => {
|
||||||
if (channel.xmltv_id === '-') {
|
if (channel.xmltv_id === '-') {
|
||||||
channel.xmltv_id = ''
|
channel.xmltv_id = ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectChannel(
|
async function selectChannel(
|
||||||
channel: epgGrabber.Channel,
|
channel: epgGrabber.Channel,
|
||||||
searchIndex,
|
searchIndex,
|
||||||
feedsGroupedByChannelId: Dictionary,
|
feedsGroupedByChannelId: Dictionary,
|
||||||
channelsKeyById: Dictionary
|
channelsKeyById: Dictionary
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const query = escapeRegex(channel.name)
|
const query = escapeRegex(channel.name)
|
||||||
const similarChannels = searchIndex
|
const similarChannels = searchIndex
|
||||||
.search(query)
|
.search(query)
|
||||||
.map((item: ChannelSearchableData) => channelsKeyById.get(item.id))
|
.map((item: ChannelSearchableData) => channelsKeyById.get(item.id))
|
||||||
|
|
||||||
const selected: ChoiceValue = await select({
|
const selected: ChoiceValue = await select({
|
||||||
message: `Select channel ID for "${channel.name}" (${channel.site_id}):`,
|
message: `Select channel ID for "${channel.name}" (${channel.site_id}):`,
|
||||||
choices: getChannelChoises(new Collection(similarChannels)),
|
choices: getChannelChoises(new Collection(similarChannels)),
|
||||||
pageSize: 10
|
pageSize: 10
|
||||||
})
|
})
|
||||||
|
|
||||||
switch (selected.type) {
|
switch (selected.type) {
|
||||||
case 'skip':
|
case 'skip':
|
||||||
return '-'
|
return '-'
|
||||||
case 'type': {
|
case 'type': {
|
||||||
const typedChannelId = await input({ message: ' Channel ID:' })
|
const typedChannelId = await input({ message: ' Channel ID:' })
|
||||||
if (!typedChannelId) return ''
|
if (!typedChannelId) return ''
|
||||||
const selectedFeedId = await selectFeed(typedChannelId, feedsGroupedByChannelId)
|
const selectedFeedId = await selectFeed(typedChannelId, feedsGroupedByChannelId)
|
||||||
if (selectedFeedId === '-') return typedChannelId
|
if (selectedFeedId === '-') return typedChannelId
|
||||||
return [typedChannelId, selectedFeedId].join('@')
|
return [typedChannelId, selectedFeedId].join('@')
|
||||||
}
|
}
|
||||||
case 'channel': {
|
case 'channel': {
|
||||||
const selectedChannel = selected.value
|
const selectedChannel = selected.value
|
||||||
if (!selectedChannel) return ''
|
if (!selectedChannel) return ''
|
||||||
const selectedFeedId = await selectFeed(selectedChannel.id || '', feedsGroupedByChannelId)
|
const selectedFeedId = await selectFeed(selectedChannel.id || '', feedsGroupedByChannelId)
|
||||||
if (selectedFeedId === '-') return selectedChannel.id || ''
|
if (selectedFeedId === '-') return selectedChannel.id || ''
|
||||||
return [selectedChannel.id, selectedFeedId].join('@')
|
return [selectedChannel.id, selectedFeedId].join('@')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectFeed(channelId: string, feedsGroupedByChannelId: Dictionary): Promise<string> {
|
async function selectFeed(channelId: string, feedsGroupedByChannelId: Dictionary): Promise<string> {
|
||||||
const channelFeeds = feedsGroupedByChannelId.has(channelId)
|
const channelFeeds = feedsGroupedByChannelId.has(channelId)
|
||||||
? new Collection(feedsGroupedByChannelId.get(channelId))
|
? new Collection(feedsGroupedByChannelId.get(channelId))
|
||||||
: new Collection()
|
: new Collection()
|
||||||
const choices = getFeedChoises(channelFeeds)
|
const choices = getFeedChoises(channelFeeds)
|
||||||
|
|
||||||
const selected: ChoiceValue = await select({
|
const selected: ChoiceValue = await select({
|
||||||
message: `Select feed ID for "${channelId}":`,
|
message: `Select feed ID for "${channelId}":`,
|
||||||
choices,
|
choices,
|
||||||
pageSize: 10
|
pageSize: 10
|
||||||
})
|
})
|
||||||
|
|
||||||
switch (selected.type) {
|
switch (selected.type) {
|
||||||
case 'skip':
|
case 'skip':
|
||||||
return '-'
|
return '-'
|
||||||
case 'type':
|
case 'type':
|
||||||
return await input({ message: ' Feed ID:', default: 'SD' })
|
return await input({ message: ' Feed ID:', default: 'SD' })
|
||||||
case 'feed':
|
case 'feed':
|
||||||
const selectedFeed = selected.value
|
const selectedFeed = selected.value
|
||||||
if (!selectedFeed) return ''
|
if (!selectedFeed) return ''
|
||||||
return selectedFeed.id || ''
|
return selectedFeed.id || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function getChannelChoises(channels: Collection): Choice[] {
|
function getChannelChoises(channels: Collection): Choice[] {
|
||||||
const choises: Choice[] = []
|
const choises: Choice[] = []
|
||||||
|
|
||||||
channels.forEach((channel: Channel) => {
|
channels.forEach((channel: Channel) => {
|
||||||
const names = new Collection([channel.name, ...channel.getAltNames().all()]).uniq().join(', ')
|
const names = new Collection([channel.name, ...channel.getAltNames().all()]).uniq().join(', ')
|
||||||
|
|
||||||
choises.push({
|
choises.push({
|
||||||
value: {
|
value: {
|
||||||
type: 'channel',
|
type: 'channel',
|
||||||
value: channel
|
value: channel
|
||||||
},
|
},
|
||||||
name: `${channel.id} (${names})`,
|
name: `${channel.id} (${names})`,
|
||||||
short: `${channel.id}`
|
short: `${channel.id}`
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
choises.push({ name: 'Type...', value: { type: 'type' } })
|
choises.push({ name: 'Type...', value: { type: 'type' } })
|
||||||
choises.push({ name: 'Skip', value: { type: 'skip' } })
|
choises.push({ name: 'Skip', value: { type: 'skip' } })
|
||||||
|
|
||||||
return choises
|
return choises
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFeedChoises(feeds: Collection): Choice[] {
|
function getFeedChoises(feeds: Collection): Choice[] {
|
||||||
const choises: Choice[] = []
|
const choises: Choice[] = []
|
||||||
|
|
||||||
feeds.forEach((feed: Feed) => {
|
feeds.forEach((feed: Feed) => {
|
||||||
let name = `${feed.id} (${feed.name})`
|
let name = `${feed.id} (${feed.name})`
|
||||||
if (feed.isMain) name += ' [main]'
|
if (feed.isMain) name += ' [main]'
|
||||||
|
|
||||||
choises.push({
|
choises.push({
|
||||||
value: {
|
value: {
|
||||||
type: 'feed',
|
type: 'feed',
|
||||||
value: feed
|
value: feed
|
||||||
},
|
},
|
||||||
default: feed.isMain,
|
default: feed.isMain,
|
||||||
name,
|
name,
|
||||||
short: feed.id
|
short: feed.id
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
choises.push({ name: 'Type...', value: { type: 'type' } })
|
choises.push({ name: 'Type...', value: { type: 'type' } })
|
||||||
choises.push({ name: 'Skip', value: { type: 'skip' } })
|
choises.push({ name: 'Skip', value: { type: 'skip' } })
|
||||||
|
|
||||||
return choises
|
return choises
|
||||||
}
|
}
|
||||||
|
|
||||||
function save(filepath: string, channelList: ChannelList) {
|
function save(filepath: string, channelList: ChannelList) {
|
||||||
if (!storage.existsSync(filepath)) return
|
if (!storage.existsSync(filepath)) return
|
||||||
storage.saveSync(filepath, channelList.toString())
|
storage.saveSync(filepath, channelList.toString())
|
||||||
logger.info(`\nFile '${filepath}' successfully saved`)
|
logger.info(`\nFile '${filepath}' successfully saved`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeRegex(string: string) {
|
function escapeRegex(string: string) {
|
||||||
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&')
|
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,109 +1,109 @@
|
|||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
import { program } from 'commander'
|
import { program } from 'commander'
|
||||||
import { Storage, File } from '@freearhey/core'
|
import { Storage, File } from '@freearhey/core'
|
||||||
import { XmlDocument, XsdValidator, XmlValidateError, ErrorDetail } from 'libxml2-wasm'
|
import { XmlDocument, XsdValidator, XmlValidateError, ErrorDetail } from 'libxml2-wasm'
|
||||||
|
|
||||||
const xsd = `<?xml version="1.0" encoding="UTF-8"?>
|
const xsd = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
|
||||||
<xs:element name="channels">
|
<xs:element name="channels">
|
||||||
<xs:complexType>
|
<xs:complexType>
|
||||||
<xs:sequence>
|
<xs:sequence>
|
||||||
<xs:element minOccurs="0" maxOccurs="unbounded" ref="channel"/>
|
<xs:element minOccurs="0" maxOccurs="unbounded" ref="channel"/>
|
||||||
</xs:sequence>
|
</xs:sequence>
|
||||||
</xs:complexType>
|
</xs:complexType>
|
||||||
</xs:element>
|
</xs:element>
|
||||||
<xs:element name="channel">
|
<xs:element name="channel">
|
||||||
<xs:complexType mixed="true">
|
<xs:complexType mixed="true">
|
||||||
<xs:attribute use="required" ref="site"/>
|
<xs:attribute use="required" ref="site"/>
|
||||||
<xs:attribute use="required" ref="lang"/>
|
<xs:attribute use="required" ref="lang"/>
|
||||||
<xs:attribute use="required" ref="site_id"/>
|
<xs:attribute use="required" ref="site_id"/>
|
||||||
<xs:attribute name="xmltv_id" use="required" type="xs:string"/>
|
<xs:attribute name="xmltv_id" use="required" type="xs:string"/>
|
||||||
<xs:attribute name="logo" type="xs:string"/>
|
<xs:attribute name="logo" type="xs:string"/>
|
||||||
<xs:attribute name="lcn" type="xs:string"/>
|
<xs:attribute name="lcn" type="xs:string"/>
|
||||||
</xs:complexType>
|
</xs:complexType>
|
||||||
</xs:element>
|
</xs:element>
|
||||||
<xs:attribute name="site">
|
<xs:attribute name="site">
|
||||||
<xs:simpleType>
|
<xs:simpleType>
|
||||||
<xs:restriction base="xs:string">
|
<xs:restriction base="xs:string">
|
||||||
<xs:minLength value="1"/>
|
<xs:minLength value="1"/>
|
||||||
</xs:restriction>
|
</xs:restriction>
|
||||||
</xs:simpleType>
|
</xs:simpleType>
|
||||||
</xs:attribute>
|
</xs:attribute>
|
||||||
<xs:attribute name="site_id">
|
<xs:attribute name="site_id">
|
||||||
<xs:simpleType>
|
<xs:simpleType>
|
||||||
<xs:restriction base="xs:string">
|
<xs:restriction base="xs:string">
|
||||||
<xs:minLength value="1"/>
|
<xs:minLength value="1"/>
|
||||||
</xs:restriction>
|
</xs:restriction>
|
||||||
</xs:simpleType>
|
</xs:simpleType>
|
||||||
</xs:attribute>
|
</xs:attribute>
|
||||||
<xs:attribute name="lang">
|
<xs:attribute name="lang">
|
||||||
<xs:simpleType>
|
<xs:simpleType>
|
||||||
<xs:restriction base="xs:string">
|
<xs:restriction base="xs:string">
|
||||||
<xs:minLength value="1"/>
|
<xs:minLength value="1"/>
|
||||||
</xs:restriction>
|
</xs:restriction>
|
||||||
</xs:simpleType>
|
</xs:simpleType>
|
||||||
</xs:attribute>
|
</xs:attribute>
|
||||||
</xs:schema>`
|
</xs:schema>`
|
||||||
|
|
||||||
program.argument('[filepath...]', 'Path to *.channels.xml files to check').parse(process.argv)
|
program.argument('[filepath...]', 'Path to *.channels.xml files to check').parse(process.argv)
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const storage = new Storage()
|
const storage = new Storage()
|
||||||
|
|
||||||
let errors: ErrorDetail[] = []
|
let errors: ErrorDetail[] = []
|
||||||
|
|
||||||
const files = program.args.length ? program.args : await storage.list('sites/**/*.channels.xml')
|
const files = program.args.length ? program.args : await storage.list('sites/**/*.channels.xml')
|
||||||
for (const filepath of files) {
|
for (const filepath of files) {
|
||||||
const file = new File(filepath)
|
const file = new File(filepath)
|
||||||
if (file.extension() !== 'xml') continue
|
if (file.extension() !== 'xml') continue
|
||||||
|
|
||||||
const xml = await storage.load(filepath)
|
const xml = await storage.load(filepath)
|
||||||
|
|
||||||
let localErrors: ErrorDetail[] = []
|
let localErrors: ErrorDetail[] = []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const schema = XmlDocument.fromString(xsd)
|
const schema = XmlDocument.fromString(xsd)
|
||||||
const validator = XsdValidator.fromDoc(schema)
|
const validator = XsdValidator.fromDoc(schema)
|
||||||
const doc = XmlDocument.fromString(xml)
|
const doc = XmlDocument.fromString(xml)
|
||||||
|
|
||||||
validator.validate(doc)
|
validator.validate(doc)
|
||||||
|
|
||||||
schema.dispose()
|
schema.dispose()
|
||||||
validator.dispose()
|
validator.dispose()
|
||||||
doc.dispose()
|
doc.dispose()
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
const error = _error as XmlValidateError
|
const error = _error as XmlValidateError
|
||||||
|
|
||||||
localErrors = localErrors.concat(error.details)
|
localErrors = localErrors.concat(error.details)
|
||||||
}
|
}
|
||||||
|
|
||||||
xml.split('\n').forEach((line: string, lineIndex: number) => {
|
xml.split('\n').forEach((line: string, lineIndex: number) => {
|
||||||
const found = line.match(/='/)
|
const found = line.match(/='/)
|
||||||
if (found) {
|
if (found) {
|
||||||
const colIndex = found.index || 0
|
const colIndex = found.index || 0
|
||||||
localErrors.push({
|
localErrors.push({
|
||||||
line: lineIndex + 1,
|
line: lineIndex + 1,
|
||||||
col: colIndex + 1,
|
col: colIndex + 1,
|
||||||
message: 'Single quotes cannot be used in attributes'
|
message: 'Single quotes cannot be used in attributes'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (localErrors.length) {
|
if (localErrors.length) {
|
||||||
console.log(`\n${chalk.underline(filepath)}`)
|
console.log(`\n${chalk.underline(filepath)}`)
|
||||||
localErrors.forEach((error: ErrorDetail) => {
|
localErrors.forEach((error: ErrorDetail) => {
|
||||||
const position = `${error.line}:${error.col}`
|
const position = `${error.line}:${error.col}`
|
||||||
console.log(` ${chalk.gray(position.padEnd(4, ' '))} ${error.message.trim()}`)
|
console.log(` ${chalk.gray(position.padEnd(4, ' '))} ${error.message.trim()}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
errors = errors.concat(localErrors)
|
errors = errors.concat(localErrors)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errors.length) {
|
if (errors.length) {
|
||||||
console.log(chalk.red(`\n${errors.length} error(s)`))
|
console.log(chalk.red(`\n${errors.length} error(s)`))
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,88 +1,86 @@
|
|||||||
import { Logger, File, Storage } from '@freearhey/core'
|
import { Logger, File, Storage } from '@freearhey/core'
|
||||||
import { ChannelsParser } from '../../core'
|
import { ChannelsParser } from '../../core'
|
||||||
import { ChannelList } from '../../models'
|
import { ChannelList } from '../../models'
|
||||||
import { pathToFileURL } from 'node:url'
|
import { pathToFileURL } from 'node:url'
|
||||||
import epgGrabber from 'epg-grabber'
|
import epgGrabber from 'epg-grabber'
|
||||||
import { Command } from 'commander'
|
import { Command } from 'commander'
|
||||||
|
|
||||||
const program = new Command()
|
const program = new Command()
|
||||||
program
|
program
|
||||||
.requiredOption('-c, --config <config>', 'Config file')
|
.requiredOption('-c, --config <config>', 'Config file')
|
||||||
.option('-s, --set [args...]', 'Set custom arguments')
|
.option('-s, --set [args...]', 'Set custom arguments')
|
||||||
.option('-o, --output <output>', 'Output file')
|
.option('-o, --output <output>', 'Output file')
|
||||||
.parse(process.argv)
|
.parse(process.argv)
|
||||||
|
|
||||||
type ParseOptions = {
|
interface ParseOptions {
|
||||||
config: string
|
config: string
|
||||||
set?: string
|
set?: string
|
||||||
output?: string
|
output?: string
|
||||||
clean?: boolean
|
clean?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const options: ParseOptions = program.opts()
|
const options: ParseOptions = program.opts()
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
function isPromise(promise: object[] | Promise<object[]>) {
|
function isPromise(promise: object[] | Promise<object[]>) {
|
||||||
return (
|
return (
|
||||||
!!promise &&
|
!!promise &&
|
||||||
typeof promise === 'object' &&
|
typeof promise === 'object' &&
|
||||||
typeof (promise as Promise<object[]>).then === 'function'
|
typeof (promise as Promise<object[]>).then === 'function'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const storage = new Storage()
|
const storage = new Storage()
|
||||||
const logger = new Logger()
|
const logger = new Logger()
|
||||||
const parser = new ChannelsParser({ storage })
|
const parser = new ChannelsParser({ storage })
|
||||||
const file = new File(options.config)
|
const file = new File(options.config)
|
||||||
const dir = file.dirname()
|
const dir = file.dirname()
|
||||||
const config = (await import(pathToFileURL(options.config).toString())).default
|
const config = (await import(pathToFileURL(options.config).toString())).default
|
||||||
const outputFilepath = options.output || `${dir}/${config.site}.channels.xml`
|
const outputFilepath = options.output || `${dir}/${config.site}.channels.xml`
|
||||||
|
|
||||||
let channelList = new ChannelList({ channels: [] })
|
let channelList = new ChannelList({ channels: [] })
|
||||||
if (await storage.exists(outputFilepath)) {
|
if (await storage.exists(outputFilepath)) {
|
||||||
channelList = await parser.parse(outputFilepath)
|
channelList = await parser.parse(outputFilepath)
|
||||||
}
|
}
|
||||||
|
|
||||||
const args: {
|
const args: Record<string, string> = {}
|
||||||
[key: string]: string
|
|
||||||
} = {}
|
if (Array.isArray(options.set)) {
|
||||||
|
options.set.forEach((arg: string) => {
|
||||||
if (Array.isArray(options.set)) {
|
const [key, value] = arg.split(':')
|
||||||
options.set.forEach((arg: string) => {
|
args[key] = value
|
||||||
const [key, value] = arg.split(':')
|
})
|
||||||
args[key] = value
|
}
|
||||||
})
|
|
||||||
}
|
let parsedChannels = config.channels(args)
|
||||||
|
if (isPromise(parsedChannels)) {
|
||||||
let parsedChannels = config.channels(args)
|
parsedChannels = await parsedChannels
|
||||||
if (isPromise(parsedChannels)) {
|
}
|
||||||
parsedChannels = await parsedChannels
|
parsedChannels = parsedChannels.map((channel: epgGrabber.Channel) => {
|
||||||
}
|
channel.site = config.site
|
||||||
parsedChannels = parsedChannels.map((channel: epgGrabber.Channel) => {
|
|
||||||
channel.site = config.site
|
return channel
|
||||||
|
})
|
||||||
return channel
|
|
||||||
})
|
const newChannelList = new ChannelList({ channels: [] })
|
||||||
|
parsedChannels.forEach((channel: epgGrabber.Channel) => {
|
||||||
const newChannelList = new ChannelList({ channels: [] })
|
if (!channel.site_id) return
|
||||||
parsedChannels.forEach((channel: epgGrabber.Channel) => {
|
|
||||||
if (!channel.site_id) return
|
const found: epgGrabber.Channel | undefined = channelList.get(channel.site_id)
|
||||||
|
|
||||||
const found: epgGrabber.Channel | undefined = channelList.get(channel.site_id)
|
if (found) {
|
||||||
|
channel.xmltv_id = found.xmltv_id
|
||||||
if (found) {
|
channel.lang = found.lang
|
||||||
channel.xmltv_id = found.xmltv_id
|
}
|
||||||
channel.lang = found.lang
|
|
||||||
}
|
newChannelList.add(channel)
|
||||||
|
})
|
||||||
newChannelList.add(channel)
|
|
||||||
})
|
newChannelList.sort()
|
||||||
|
|
||||||
newChannelList.sort()
|
await storage.save(outputFilepath, newChannelList.toString())
|
||||||
|
|
||||||
await storage.save(outputFilepath, newChannelList.toString())
|
logger.info(`File '${outputFilepath}' successfully saved`)
|
||||||
|
}
|
||||||
logger.info(`File '${outputFilepath}' successfully saved`)
|
|
||||||
}
|
main()
|
||||||
|
|
||||||
main()
|
|
||||||
|
|||||||
@@ -1,100 +1,100 @@
|
|||||||
import { ChannelsParser, DataLoader, DataProcessor } from '../../core'
|
import { ChannelsParser, DataLoader, DataProcessor } from '../../core'
|
||||||
import { DataProcessorData } from '../../types/dataProcessor'
|
import { DataProcessorData } from '../../types/dataProcessor'
|
||||||
import { Storage, Dictionary, File } from '@freearhey/core'
|
import { Storage, Dictionary, File } from '@freearhey/core'
|
||||||
import { DataLoaderData } from '../../types/dataLoader'
|
import { DataLoaderData } from '../../types/dataLoader'
|
||||||
import { ChannelList } from '../../models'
|
import { ChannelList } from '../../models'
|
||||||
import { DATA_DIR } from '../../constants'
|
import { DATA_DIR } from '../../constants'
|
||||||
import epgGrabber from 'epg-grabber'
|
import epgGrabber from 'epg-grabber'
|
||||||
import { program } from 'commander'
|
import { program } from 'commander'
|
||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
import langs from 'langs'
|
import langs from 'langs'
|
||||||
|
|
||||||
program.argument('[filepath]', 'Path to *.channels.xml files to validate').parse(process.argv)
|
program.argument('[filepath...]', 'Path to *.channels.xml files to validate').parse(process.argv)
|
||||||
|
|
||||||
type ValidationError = {
|
interface ValidationError {
|
||||||
type: 'duplicate' | 'wrong_channel_id' | 'wrong_feed_id' | 'wrong_lang'
|
type: 'duplicate' | 'wrong_channel_id' | 'wrong_feed_id' | 'wrong_lang'
|
||||||
name: string
|
name: string
|
||||||
lang?: string
|
lang?: string
|
||||||
xmltv_id?: string
|
xmltv_id?: string
|
||||||
site_id?: string
|
site_id?: string
|
||||||
logo?: string
|
logo?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const processor = new DataProcessor()
|
const processor = new DataProcessor()
|
||||||
const dataStorage = new Storage(DATA_DIR)
|
const dataStorage = new Storage(DATA_DIR)
|
||||||
const loader = new DataLoader({ storage: dataStorage })
|
const loader = new DataLoader({ storage: dataStorage })
|
||||||
const data: DataLoaderData = await loader.load()
|
const data: DataLoaderData = await loader.load()
|
||||||
const { channelsKeyById, feedsKeyByStreamId }: DataProcessorData = processor.process(data)
|
const { channelsKeyById, feedsKeyByStreamId }: DataProcessorData = processor.process(data)
|
||||||
const parser = new ChannelsParser({
|
const parser = new ChannelsParser({
|
||||||
storage: new Storage()
|
storage: new Storage()
|
||||||
})
|
})
|
||||||
|
|
||||||
let totalFiles = 0
|
let totalFiles = 0
|
||||||
let totalErrors = 0
|
let totalErrors = 0
|
||||||
let totalWarnings = 0
|
let totalWarnings = 0
|
||||||
|
|
||||||
const storage = new Storage()
|
const storage = new Storage()
|
||||||
const files = program.args.length ? program.args : await storage.list('sites/**/*.channels.xml')
|
const files = program.args.length ? program.args : await storage.list('sites/**/*.channels.xml')
|
||||||
for (const filepath of files) {
|
for (const filepath of files) {
|
||||||
const file = new File(filepath)
|
const file = new File(filepath)
|
||||||
if (file.extension() !== 'xml') continue
|
if (file.extension() !== 'xml') continue
|
||||||
|
|
||||||
const channelList: ChannelList = await parser.parse(filepath)
|
const channelList: ChannelList = await parser.parse(filepath)
|
||||||
|
|
||||||
const bufferBySiteId = new Dictionary()
|
const bufferBySiteId = new Dictionary()
|
||||||
const errors: ValidationError[] = []
|
const errors: ValidationError[] = []
|
||||||
channelList.channels.forEach((channel: epgGrabber.Channel) => {
|
channelList.channels.forEach((channel: epgGrabber.Channel) => {
|
||||||
const bufferId: string = channel.site_id
|
const bufferId: string = channel.site_id
|
||||||
if (bufferBySiteId.missing(bufferId)) {
|
if (bufferBySiteId.missing(bufferId)) {
|
||||||
bufferBySiteId.set(bufferId, true)
|
bufferBySiteId.set(bufferId, true)
|
||||||
} else {
|
} else {
|
||||||
errors.push({ type: 'duplicate', ...channel })
|
errors.push({ type: 'duplicate', ...channel })
|
||||||
totalErrors++
|
totalErrors++
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!langs.where('1', channel.lang ?? '')) {
|
if (!langs.where('1', channel.lang ?? '')) {
|
||||||
errors.push({ type: 'wrong_lang', ...channel })
|
errors.push({ type: 'wrong_lang', ...channel })
|
||||||
totalErrors++
|
totalErrors++
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!channel.xmltv_id) return
|
if (!channel.xmltv_id) return
|
||||||
const [channelId, feedId] = channel.xmltv_id.split('@')
|
const [channelId, feedId] = channel.xmltv_id.split('@')
|
||||||
|
|
||||||
const foundChannel = channelsKeyById.get(channelId)
|
const foundChannel = channelsKeyById.get(channelId)
|
||||||
if (!foundChannel) {
|
if (!foundChannel) {
|
||||||
errors.push({ type: 'wrong_channel_id', ...channel })
|
errors.push({ type: 'wrong_channel_id', ...channel })
|
||||||
totalWarnings++
|
totalWarnings++
|
||||||
}
|
}
|
||||||
|
|
||||||
if (feedId) {
|
if (feedId) {
|
||||||
const foundFeed = feedsKeyByStreamId.get(channel.xmltv_id)
|
const foundFeed = feedsKeyByStreamId.get(channel.xmltv_id)
|
||||||
if (!foundFeed) {
|
if (!foundFeed) {
|
||||||
errors.push({ type: 'wrong_feed_id', ...channel })
|
errors.push({ type: 'wrong_feed_id', ...channel })
|
||||||
totalWarnings++
|
totalWarnings++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (errors.length) {
|
if (errors.length) {
|
||||||
console.log(chalk.underline(filepath))
|
console.log(chalk.underline(filepath))
|
||||||
console.table(errors, ['type', 'lang', 'xmltv_id', 'site_id', 'name'])
|
console.table(errors, ['type', 'lang', 'xmltv_id', 'site_id', 'name'])
|
||||||
console.log()
|
console.log()
|
||||||
totalFiles++
|
totalFiles++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalProblems = totalWarnings + totalErrors
|
const totalProblems = totalWarnings + totalErrors
|
||||||
if (totalProblems > 0) {
|
if (totalProblems > 0) {
|
||||||
console.log(
|
console.log(
|
||||||
chalk.red(
|
chalk.red(
|
||||||
`${totalProblems} problems (${totalErrors} errors, ${totalWarnings} warnings) in ${totalFiles} file(s)`
|
`${totalProblems} problems (${totalErrors} errors, ${totalWarnings} warnings) in ${totalFiles} file(s)`
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if (totalErrors > 0) {
|
if (totalErrors > 0) {
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,133 +1,133 @@
|
|||||||
import { Logger, Timer, Storage, Collection } from '@freearhey/core'
|
import { Logger, Timer, Storage, Collection } from '@freearhey/core'
|
||||||
import { QueueCreator, Job, ChannelsParser } from '../../core'
|
import { QueueCreator, Job, ChannelsParser } from '../../core'
|
||||||
import { Option, program } from 'commander'
|
import { Option, program } from 'commander'
|
||||||
import { SITES_DIR } from '../../constants'
|
import { SITES_DIR } from '../../constants'
|
||||||
import { Channel } from 'epg-grabber'
|
import { Channel } from 'epg-grabber'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { ChannelList } from '../../models'
|
import { ChannelList } from '../../models'
|
||||||
|
|
||||||
program
|
program
|
||||||
.addOption(new Option('-s, --site <name>', 'Name of the site to parse'))
|
.addOption(new Option('-s, --site <name>', 'Name of the site to parse'))
|
||||||
.addOption(
|
.addOption(
|
||||||
new Option(
|
new Option(
|
||||||
'-c, --channels <path>',
|
'-c, --channels <path>',
|
||||||
'Path to *.channels.xml file (required if the "--site" attribute is not specified)'
|
'Path to *.channels.xml file (required if the "--site" attribute is not specified)'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.addOption(new Option('-o, --output <path>', 'Path to output file').default('guide.xml'))
|
.addOption(new Option('-o, --output <path>', 'Path to output file').default('guide.xml'))
|
||||||
.addOption(new Option('-l, --lang <codes>', 'Filter channels by languages (ISO 639-1 codes)'))
|
.addOption(new Option('-l, --lang <codes>', 'Filter channels by languages (ISO 639-1 codes)'))
|
||||||
.addOption(
|
.addOption(
|
||||||
new Option('-t, --timeout <milliseconds>', 'Override the default timeout for each request').env(
|
new Option('-t, --timeout <milliseconds>', 'Override the default timeout for each request').env(
|
||||||
'TIMEOUT'
|
'TIMEOUT'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.addOption(
|
.addOption(
|
||||||
new Option('-d, --delay <milliseconds>', 'Override the default delay between request').env(
|
new Option('-d, --delay <milliseconds>', 'Override the default delay between request').env(
|
||||||
'DELAY'
|
'DELAY'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.addOption(new Option('-x, --proxy <url>', 'Use the specified proxy').env('PROXY'))
|
.addOption(new Option('-x, --proxy <url>', 'Use the specified proxy').env('PROXY'))
|
||||||
.addOption(
|
.addOption(
|
||||||
new Option(
|
new Option(
|
||||||
'--days <days>',
|
'--days <days>',
|
||||||
'Override the number of days for which the program will be loaded (defaults to the value from the site config)'
|
'Override the number of days for which the program will be loaded (defaults to the value from the site config)'
|
||||||
)
|
)
|
||||||
.argParser(value => parseInt(value))
|
.argParser(value => parseInt(value))
|
||||||
.env('DAYS')
|
.env('DAYS')
|
||||||
)
|
)
|
||||||
.addOption(
|
.addOption(
|
||||||
new Option('--maxConnections <number>', 'Limit on the number of concurrent requests')
|
new Option('--maxConnections <number>', 'Limit on the number of concurrent requests')
|
||||||
.default(1)
|
.default(1)
|
||||||
.env('MAX_CONNECTIONS')
|
.env('MAX_CONNECTIONS')
|
||||||
)
|
)
|
||||||
.addOption(
|
.addOption(
|
||||||
new Option('--gzip', 'Create a compressed version of the guide as well')
|
new Option('--gzip', 'Create a compressed version of the guide as well')
|
||||||
.default(false)
|
.default(false)
|
||||||
.env('GZIP')
|
.env('GZIP')
|
||||||
)
|
)
|
||||||
.addOption(new Option('--curl', 'Display each request as CURL').default(false).env('CURL'))
|
.addOption(new Option('--curl', 'Display each request as CURL').default(false).env('CURL'))
|
||||||
.parse()
|
.parse()
|
||||||
|
|
||||||
export type GrabOptions = {
|
export interface GrabOptions {
|
||||||
site?: string
|
site?: string
|
||||||
channels?: string
|
channels?: string
|
||||||
output: string
|
output: string
|
||||||
gzip: boolean
|
gzip: boolean
|
||||||
curl: boolean
|
curl: boolean
|
||||||
maxConnections: number
|
maxConnections: number
|
||||||
timeout?: string
|
timeout?: string
|
||||||
delay?: string
|
delay?: string
|
||||||
lang?: string
|
lang?: string
|
||||||
days?: number
|
days?: number
|
||||||
proxy?: string
|
proxy?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const options: GrabOptions = program.opts()
|
const options: GrabOptions = program.opts()
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
if (!options.site && !options.channels)
|
if (!options.site && !options.channels)
|
||||||
throw new Error('One of the arguments must be presented: `--site` or `--channels`')
|
throw new Error('One of the arguments must be presented: `--site` or `--channels`')
|
||||||
|
|
||||||
const logger = new Logger()
|
const logger = new Logger()
|
||||||
|
|
||||||
logger.start('starting...')
|
logger.start('starting...')
|
||||||
|
|
||||||
logger.info('config:')
|
logger.info('config:')
|
||||||
logger.tree(options)
|
logger.tree(options)
|
||||||
|
|
||||||
logger.info('loading channels...')
|
logger.info('loading channels...')
|
||||||
const storage = new Storage()
|
const storage = new Storage()
|
||||||
const parser = new ChannelsParser({ storage })
|
const parser = new ChannelsParser({ storage })
|
||||||
|
|
||||||
let files: string[] = []
|
let files: string[] = []
|
||||||
if (options.site) {
|
if (options.site) {
|
||||||
let pattern = path.join(SITES_DIR, options.site, '*.channels.xml')
|
let pattern = path.join(SITES_DIR, options.site, '*.channels.xml')
|
||||||
pattern = pattern.replace(/\\/g, '/')
|
pattern = pattern.replace(/\\/g, '/')
|
||||||
files = await storage.list(pattern)
|
files = await storage.list(pattern)
|
||||||
} else if (options.channels) {
|
} else if (options.channels) {
|
||||||
files = await storage.list(options.channels)
|
files = await storage.list(options.channels)
|
||||||
}
|
}
|
||||||
|
|
||||||
let channels = new Collection()
|
let channels = new Collection()
|
||||||
for (const filepath of files) {
|
for (const filepath of files) {
|
||||||
const channelList: ChannelList = await parser.parse(filepath)
|
const channelList: ChannelList = await parser.parse(filepath)
|
||||||
|
|
||||||
channels = channels.concat(channelList.channels)
|
channels = channels.concat(channelList.channels)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.lang) {
|
if (options.lang) {
|
||||||
channels = channels.filter((channel: Channel) => {
|
channels = channels.filter((channel: Channel) => {
|
||||||
if (!options.lang || !channel.lang) return true
|
if (!options.lang || !channel.lang) return true
|
||||||
|
|
||||||
return options.lang.includes(channel.lang)
|
return options.lang.includes(channel.lang)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(` found ${channels.count()} channel(s)`)
|
logger.info(` found ${channels.count()} channel(s)`)
|
||||||
|
|
||||||
logger.info('run:')
|
logger.info('run:')
|
||||||
runJob({ logger, channels })
|
runJob({ logger, channels })
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|
||||||
async function runJob({ logger, channels }: { logger: Logger; channels: Collection }) {
|
async function runJob({ logger, channels }: { logger: Logger; channels: Collection }) {
|
||||||
const timer = new Timer()
|
const timer = new Timer()
|
||||||
timer.start()
|
timer.start()
|
||||||
|
|
||||||
const queueCreator = new QueueCreator({
|
const queueCreator = new QueueCreator({
|
||||||
channels,
|
channels,
|
||||||
logger,
|
logger,
|
||||||
options
|
options
|
||||||
})
|
})
|
||||||
const queue = await queueCreator.create()
|
const queue = await queueCreator.create()
|
||||||
const job = new Job({
|
const job = new Job({
|
||||||
queue,
|
queue,
|
||||||
logger,
|
logger,
|
||||||
options
|
options
|
||||||
})
|
})
|
||||||
|
|
||||||
await job.run()
|
await job.run()
|
||||||
|
|
||||||
logger.success(` done in ${timer.format('HH[h] mm[m] ss[s]')}`)
|
logger.success(` done in ${timer.format('HH[h] mm[m] ss[s]')}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,45 @@
|
|||||||
import { Logger, Storage } from '@freearhey/core'
|
import { Logger, Storage } from '@freearhey/core'
|
||||||
import { SITES_DIR } from '../../constants'
|
import { SITES_DIR } from '../../constants'
|
||||||
import { pathToFileURL } from 'node:url'
|
import { pathToFileURL } from 'node:url'
|
||||||
import { program } from 'commander'
|
import { program } from 'commander'
|
||||||
import fs from 'fs-extra'
|
import fs from 'fs-extra'
|
||||||
|
|
||||||
program.argument('<site>', 'Domain name of the site').parse(process.argv)
|
program.argument('<site>', 'Domain name of the site').parse(process.argv)
|
||||||
|
|
||||||
const domain = program.args[0]
|
const domain = program.args[0]
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const storage = new Storage(SITES_DIR)
|
const storage = new Storage(SITES_DIR)
|
||||||
const logger = new Logger()
|
const logger = new Logger()
|
||||||
|
|
||||||
logger.info(`Initializing "${domain}"...\r\n`)
|
logger.info(`Initializing "${domain}"...\r\n`)
|
||||||
|
|
||||||
const dir = domain
|
const dir = domain
|
||||||
if (await storage.exists(dir)) {
|
if (await storage.exists(dir)) {
|
||||||
throw new Error(`Folder "${dir}" already exists`)
|
throw new Error(`Folder "${dir}" already exists`)
|
||||||
}
|
}
|
||||||
|
|
||||||
await storage.createDir(dir)
|
await storage.createDir(dir)
|
||||||
|
|
||||||
logger.info(`Creating "${dir}/${domain}.test.js"...`)
|
logger.info(`Creating "${dir}/${domain}.test.js"...`)
|
||||||
const testTemplate = fs.readFileSync(pathToFileURL('scripts/templates/_test.js'), {
|
const testTemplate = fs.readFileSync(pathToFileURL('scripts/templates/_test.js'), {
|
||||||
encoding: 'utf8'
|
encoding: 'utf8'
|
||||||
})
|
})
|
||||||
await storage.save(`${dir}/${domain}.test.js`, testTemplate.replace(/<DOMAIN>/g, domain))
|
await storage.save(`${dir}/${domain}.test.js`, testTemplate.replace(/<DOMAIN>/g, domain))
|
||||||
|
|
||||||
logger.info(`Creating "${dir}/${domain}.config.js"...`)
|
logger.info(`Creating "${dir}/${domain}.config.js"...`)
|
||||||
const configTemplate = fs.readFileSync(pathToFileURL('scripts/templates/_config.js'), {
|
const configTemplate = fs.readFileSync(pathToFileURL('scripts/templates/_config.js'), {
|
||||||
encoding: 'utf8'
|
encoding: 'utf8'
|
||||||
})
|
})
|
||||||
await storage.save(`${dir}/${domain}.config.js`, configTemplate.replace(/<DOMAIN>/g, domain))
|
await storage.save(`${dir}/${domain}.config.js`, configTemplate.replace(/<DOMAIN>/g, domain))
|
||||||
|
|
||||||
logger.info(`Creating "${dir}/readme.md"...`)
|
logger.info(`Creating "${dir}/readme.md"...`)
|
||||||
const readmeTemplate = fs.readFileSync(pathToFileURL('scripts/templates/_readme.md'), {
|
const readmeTemplate = fs.readFileSync(pathToFileURL('scripts/templates/_readme.md'), {
|
||||||
encoding: 'utf8'
|
encoding: 'utf8'
|
||||||
})
|
})
|
||||||
await storage.save(`${dir}/readme.md`, readmeTemplate.replace(/<DOMAIN>/g, domain))
|
await storage.save(`${dir}/readme.md`, readmeTemplate.replace(/<DOMAIN>/g, domain))
|
||||||
|
|
||||||
logger.info('\r\nDone')
|
logger.info('\r\nDone')
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,76 +1,76 @@
|
|||||||
import { IssueLoader, HTMLTable, ChannelsParser } from '../../core'
|
import { IssueLoader, HTMLTable, ChannelsParser } from '../../core'
|
||||||
import { Logger, Storage, Collection } from '@freearhey/core'
|
import { Logger, Storage, Collection } from '@freearhey/core'
|
||||||
import { ChannelList, Issue, Site } from '../../models'
|
import { ChannelList, Issue, Site } from '../../models'
|
||||||
import { SITES_DIR, ROOT_DIR } from '../../constants'
|
import { SITES_DIR, ROOT_DIR } from '../../constants'
|
||||||
import { Channel } from 'epg-grabber'
|
import { Channel } from 'epg-grabber'
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const logger = new Logger({ level: -999 })
|
const logger = new Logger({ level: -999 })
|
||||||
const issueLoader = new IssueLoader()
|
const issueLoader = new IssueLoader()
|
||||||
const sitesStorage = new Storage(SITES_DIR)
|
const sitesStorage = new Storage(SITES_DIR)
|
||||||
const sites = new Collection()
|
const sites = new Collection()
|
||||||
|
|
||||||
logger.info('loading channels...')
|
logger.info('loading channels...')
|
||||||
const channelsParser = new ChannelsParser({
|
const channelsParser = new ChannelsParser({
|
||||||
storage: sitesStorage
|
storage: sitesStorage
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.info('loading list of sites')
|
logger.info('loading list of sites')
|
||||||
const folders = await sitesStorage.list('*/')
|
const folders = await sitesStorage.list('*/')
|
||||||
|
|
||||||
logger.info('loading issues...')
|
logger.info('loading issues...')
|
||||||
const issues = await issueLoader.load()
|
const issues = await issueLoader.load()
|
||||||
|
|
||||||
logger.info('putting the data together...')
|
logger.info('putting the data together...')
|
||||||
const brokenGuideReports = issues.filter(issue =>
|
const brokenGuideReports = issues.filter(issue =>
|
||||||
issue.labels.find((label: string) => label === 'broken guide')
|
issue.labels.find((label: string) => label === 'broken guide')
|
||||||
)
|
)
|
||||||
for (const domain of folders) {
|
for (const domain of folders) {
|
||||||
const filteredIssues = brokenGuideReports.filter(
|
const filteredIssues = brokenGuideReports.filter(
|
||||||
(issue: Issue) => domain === issue.data.get('site')
|
(issue: Issue) => domain === issue.data.get('site')
|
||||||
)
|
)
|
||||||
|
|
||||||
const site = new Site({
|
const site = new Site({
|
||||||
domain,
|
domain,
|
||||||
issues: filteredIssues
|
issues: filteredIssues
|
||||||
})
|
})
|
||||||
|
|
||||||
const files = await sitesStorage.list(`${domain}/*.channels.xml`)
|
const files = await sitesStorage.list(`${domain}/*.channels.xml`)
|
||||||
for (const filepath of files) {
|
for (const filepath of files) {
|
||||||
const channelList: ChannelList = await channelsParser.parse(filepath)
|
const channelList: ChannelList = await channelsParser.parse(filepath)
|
||||||
|
|
||||||
site.totalChannels += channelList.channels.count()
|
site.totalChannels += channelList.channels.count()
|
||||||
site.markedChannels += channelList.channels
|
site.markedChannels += channelList.channels
|
||||||
.filter((channel: Channel) => channel.xmltv_id)
|
.filter((channel: Channel) => channel.xmltv_id)
|
||||||
.count()
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
sites.add(site)
|
sites.add(site)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('creating sites table...')
|
logger.info('creating sites table...')
|
||||||
const tableData = new Collection()
|
const tableData = new Collection()
|
||||||
sites.forEach((site: Site) => {
|
sites.forEach((site: Site) => {
|
||||||
tableData.add([
|
tableData.add([
|
||||||
{ value: `<a href="sites/${site.domain}">${site.domain}</a>` },
|
{ value: `<a href="sites/${site.domain}">${site.domain}</a>` },
|
||||||
{ value: site.totalChannels, align: 'right' },
|
{ value: site.totalChannels, align: 'right' },
|
||||||
{ value: site.markedChannels, align: 'right' },
|
{ value: site.markedChannels, align: 'right' },
|
||||||
{ value: site.getStatus().emoji, align: 'center' },
|
{ value: site.getStatus().emoji, align: 'center' },
|
||||||
{ value: site.getIssues().all().join(', ') }
|
{ value: site.getIssues().all().join(', ') }
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.info('updating sites.md...')
|
logger.info('updating sites.md...')
|
||||||
const table = new HTMLTable(tableData.all(), [
|
const table = new HTMLTable(tableData.all(), [
|
||||||
{ name: 'Site', align: 'left' },
|
{ name: 'Site', align: 'left' },
|
||||||
{ name: 'Channels<br>(total / with xmltv-id)', colspan: 2, align: 'left' },
|
{ name: 'Channels<br>(total / with xmltv-id)', colspan: 2, align: 'left' },
|
||||||
{ name: 'Status', align: 'left' },
|
{ name: 'Status', align: 'left' },
|
||||||
{ name: 'Notes', align: 'left' }
|
{ name: 'Notes', align: 'left' }
|
||||||
])
|
])
|
||||||
const rootStorage = new Storage(ROOT_DIR)
|
const rootStorage = new Storage(ROOT_DIR)
|
||||||
const sitesTemplate = await new Storage().load('scripts/templates/_sites.md')
|
const sitesTemplate = await new Storage().load('scripts/templates/_sites.md')
|
||||||
const sitesContent = sitesTemplate.replace('_TABLE_', table.toString())
|
const sitesContent = sitesTemplate.replace('_TABLE_', table.toString())
|
||||||
await rootStorage.save('SITES.md', sitesContent)
|
await rootStorage.save('SITES.md', sitesContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
export const ROOT_DIR = process.env.ROOT_DIR || '.'
|
export const ROOT_DIR = process.env.ROOT_DIR || '.'
|
||||||
export const SITES_DIR = process.env.SITES_DIR || './sites'
|
export const SITES_DIR = process.env.SITES_DIR || './sites'
|
||||||
export const GUIDES_DIR = process.env.GUIDES_DIR || './guides'
|
export const GUIDES_DIR = process.env.GUIDES_DIR || './guides'
|
||||||
export const DATA_DIR = process.env.DATA_DIR || './temp/data'
|
export const DATA_DIR = process.env.DATA_DIR || './temp/data'
|
||||||
export const API_DIR = process.env.API_DIR || '.api'
|
export const API_DIR = process.env.API_DIR || '.api'
|
||||||
export const DOT_SITES_DIR = process.env.DOT_SITES_DIR || './.sites'
|
export const DOT_SITES_DIR = process.env.DOT_SITES_DIR || './.sites'
|
||||||
export const TESTING = process.env.NODE_ENV === 'test' ? true : false
|
export const TESTING = process.env.NODE_ENV === 'test' ? true : false
|
||||||
export const OWNER = 'iptv-org'
|
export const OWNER = 'iptv-org'
|
||||||
export const REPO = 'epg'
|
export const REPO = 'epg'
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import axios, { AxiosInstance, AxiosResponse, AxiosRequestConfig } from 'axios'
|
import axios, { AxiosInstance, AxiosResponse, AxiosRequestConfig } from 'axios'
|
||||||
|
|
||||||
export class ApiClient {
|
export class ApiClient {
|
||||||
instance: AxiosInstance
|
instance: AxiosInstance
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.instance = axios.create({
|
this.instance = axios.create({
|
||||||
baseURL: 'https://iptv-org.github.io/api',
|
baseURL: 'https://iptv-org.github.io/api',
|
||||||
responseType: 'stream'
|
responseType: 'stream'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
get(url: string, options: AxiosRequestConfig): Promise<AxiosResponse> {
|
get(url: string, options: AxiosRequestConfig): Promise<AxiosResponse> {
|
||||||
return this.instance.get(url, options)
|
return this.instance.get(url, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import { parseChannels } from 'epg-grabber'
|
import { parseChannels } from 'epg-grabber'
|
||||||
import { Storage } from '@freearhey/core'
|
import { Storage } from '@freearhey/core'
|
||||||
import { ChannelList } from '../models'
|
import { ChannelList } from '../models'
|
||||||
|
|
||||||
type ChannelsParserProps = {
|
interface ChannelsParserProps {
|
||||||
storage: Storage
|
storage: Storage
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ChannelsParser {
|
export class ChannelsParser {
|
||||||
storage: Storage
|
storage: Storage
|
||||||
|
|
||||||
constructor({ storage }: ChannelsParserProps) {
|
constructor({ storage }: ChannelsParserProps) {
|
||||||
this.storage = storage
|
this.storage = storage
|
||||||
}
|
}
|
||||||
|
|
||||||
async parse(filepath: string): Promise<ChannelList> {
|
async parse(filepath: string): Promise<ChannelList> {
|
||||||
const content = await this.storage.load(filepath)
|
const content = await this.storage.load(filepath)
|
||||||
const parsed = parseChannels(content)
|
const parsed = parseChannels(content)
|
||||||
|
|
||||||
return new ChannelList({ channels: parsed })
|
return new ChannelList({ channels: parsed })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,32 @@
|
|||||||
import { SiteConfig } from 'epg-grabber'
|
import { SiteConfig } from 'epg-grabber'
|
||||||
import _ from 'lodash'
|
import { pathToFileURL } from 'url'
|
||||||
import { pathToFileURL } from 'url'
|
|
||||||
|
export class ConfigLoader {
|
||||||
export class ConfigLoader {
|
async load(filepath: string): Promise<SiteConfig> {
|
||||||
async load(filepath: string): Promise<SiteConfig> {
|
const fileUrl = pathToFileURL(filepath).toString()
|
||||||
const fileUrl = pathToFileURL(filepath).toString()
|
const config = (await import(fileUrl)).default
|
||||||
const config = (await import(fileUrl)).default
|
const defaultConfig = {
|
||||||
const defaultConfig = {
|
days: 1,
|
||||||
days: 1,
|
delay: 0,
|
||||||
delay: 0,
|
output: 'guide.xml',
|
||||||
output: 'guide.xml',
|
request: {
|
||||||
request: {
|
method: 'GET',
|
||||||
method: 'GET',
|
maxContentLength: 5242880,
|
||||||
maxContentLength: 5242880,
|
timeout: 30000,
|
||||||
timeout: 30000,
|
withCredentials: true,
|
||||||
withCredentials: true,
|
jar: null,
|
||||||
jar: null,
|
responseType: 'arraybuffer',
|
||||||
responseType: 'arraybuffer',
|
cache: false,
|
||||||
cache: false,
|
headers: null,
|
||||||
headers: null,
|
data: null
|
||||||
data: null
|
},
|
||||||
},
|
maxConnections: 1,
|
||||||
maxConnections: 1,
|
site: undefined,
|
||||||
site: undefined,
|
url: undefined,
|
||||||
url: undefined,
|
parser: undefined,
|
||||||
parser: undefined,
|
channels: undefined
|
||||||
channels: undefined
|
}
|
||||||
}
|
|
||||||
|
return { ...defaultConfig, ...config } as SiteConfig
|
||||||
return _.merge(defaultConfig, config)
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,103 +1,103 @@
|
|||||||
import type { DataLoaderProps, DataLoaderData } from '../types/dataLoader'
|
import type { DataLoaderProps, DataLoaderData } from '../types/dataLoader'
|
||||||
import cliProgress, { MultiBar } from 'cli-progress'
|
import cliProgress, { MultiBar } from 'cli-progress'
|
||||||
import { Storage } from '@freearhey/core'
|
import { Storage } from '@freearhey/core'
|
||||||
import { ApiClient } from './apiClient'
|
import { ApiClient } from './apiClient'
|
||||||
import numeral from 'numeral'
|
import numeral from 'numeral'
|
||||||
|
|
||||||
export class DataLoader {
|
export class DataLoader {
|
||||||
client: ApiClient
|
client: ApiClient
|
||||||
storage: Storage
|
storage: Storage
|
||||||
progressBar: MultiBar
|
progressBar: MultiBar
|
||||||
|
|
||||||
constructor(props: DataLoaderProps) {
|
constructor(props: DataLoaderProps) {
|
||||||
this.client = new ApiClient()
|
this.client = new ApiClient()
|
||||||
this.storage = props.storage
|
this.storage = props.storage
|
||||||
this.progressBar = new cliProgress.MultiBar({
|
this.progressBar = new cliProgress.MultiBar({
|
||||||
stopOnComplete: true,
|
stopOnComplete: true,
|
||||||
hideCursor: true,
|
hideCursor: true,
|
||||||
forceRedraw: true,
|
forceRedraw: true,
|
||||||
barsize: 36,
|
barsize: 36,
|
||||||
format(options, params, payload) {
|
format(options, params, payload) {
|
||||||
const filename = payload.filename.padEnd(18, ' ')
|
const filename = payload.filename.padEnd(18, ' ')
|
||||||
const barsize = options.barsize || 40
|
const barsize = options.barsize || 40
|
||||||
const percent = (params.progress * 100).toFixed(2)
|
const percent = (params.progress * 100).toFixed(2)
|
||||||
const speed = payload.speed ? numeral(payload.speed).format('0.0 b') + '/s' : 'N/A'
|
const speed = payload.speed ? numeral(payload.speed).format('0.0 b') + '/s' : 'N/A'
|
||||||
const total = numeral(params.total).format('0.0 b')
|
const total = numeral(params.total).format('0.0 b')
|
||||||
const completeSize = Math.round(params.progress * barsize)
|
const completeSize = Math.round(params.progress * barsize)
|
||||||
const incompleteSize = barsize - completeSize
|
const incompleteSize = barsize - completeSize
|
||||||
const bar =
|
const bar =
|
||||||
options.barCompleteString && options.barIncompleteString
|
options.barCompleteString && options.barIncompleteString
|
||||||
? options.barCompleteString.substr(0, completeSize) +
|
? options.barCompleteString.substr(0, completeSize) +
|
||||||
options.barGlue +
|
options.barGlue +
|
||||||
options.barIncompleteString.substr(0, incompleteSize)
|
options.barIncompleteString.substr(0, incompleteSize)
|
||||||
: '-'.repeat(barsize)
|
: '-'.repeat(barsize)
|
||||||
|
|
||||||
return `${filename} [${bar}] ${percent}% | ETA: ${params.eta}s | ${total} | ${speed}`
|
return `${filename} [${bar}] ${percent}% | ETA: ${params.eta}s | ${total} | ${speed}`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async load(): Promise<DataLoaderData> {
|
async load(): Promise<DataLoaderData> {
|
||||||
const [
|
const [
|
||||||
countries,
|
countries,
|
||||||
regions,
|
regions,
|
||||||
subdivisions,
|
subdivisions,
|
||||||
languages,
|
languages,
|
||||||
categories,
|
categories,
|
||||||
blocklist,
|
blocklist,
|
||||||
channels,
|
channels,
|
||||||
feeds,
|
feeds,
|
||||||
timezones,
|
timezones,
|
||||||
guides,
|
guides,
|
||||||
streams,
|
streams,
|
||||||
logos
|
logos
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.storage.json('countries.json'),
|
this.storage.json('countries.json'),
|
||||||
this.storage.json('regions.json'),
|
this.storage.json('regions.json'),
|
||||||
this.storage.json('subdivisions.json'),
|
this.storage.json('subdivisions.json'),
|
||||||
this.storage.json('languages.json'),
|
this.storage.json('languages.json'),
|
||||||
this.storage.json('categories.json'),
|
this.storage.json('categories.json'),
|
||||||
this.storage.json('blocklist.json'),
|
this.storage.json('blocklist.json'),
|
||||||
this.storage.json('channels.json'),
|
this.storage.json('channels.json'),
|
||||||
this.storage.json('feeds.json'),
|
this.storage.json('feeds.json'),
|
||||||
this.storage.json('timezones.json'),
|
this.storage.json('timezones.json'),
|
||||||
this.storage.json('guides.json'),
|
this.storage.json('guides.json'),
|
||||||
this.storage.json('streams.json'),
|
this.storage.json('streams.json'),
|
||||||
this.storage.json('logos.json')
|
this.storage.json('logos.json')
|
||||||
])
|
])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
countries,
|
countries,
|
||||||
regions,
|
regions,
|
||||||
subdivisions,
|
subdivisions,
|
||||||
languages,
|
languages,
|
||||||
categories,
|
categories,
|
||||||
blocklist,
|
blocklist,
|
||||||
channels,
|
channels,
|
||||||
feeds,
|
feeds,
|
||||||
timezones,
|
timezones,
|
||||||
guides,
|
guides,
|
||||||
streams,
|
streams,
|
||||||
logos
|
logos
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async download(filename: string) {
|
async download(filename: string) {
|
||||||
if (!this.storage || !this.progressBar) return
|
if (!this.storage || !this.progressBar) return
|
||||||
|
|
||||||
const stream = await this.storage.createStream(filename)
|
const stream = await this.storage.createStream(filename)
|
||||||
const progressBar = this.progressBar.create(0, 0, { filename })
|
const progressBar = this.progressBar.create(0, 0, { filename })
|
||||||
|
|
||||||
this.client
|
this.client
|
||||||
.get(filename, {
|
.get(filename, {
|
||||||
responseType: 'stream',
|
responseType: 'stream',
|
||||||
onDownloadProgress({ total, loaded, rate }) {
|
onDownloadProgress({ total, loaded, rate }) {
|
||||||
if (total) progressBar.setTotal(total)
|
if (total) progressBar.setTotal(total)
|
||||||
progressBar.update(loaded, { speed: rate })
|
progressBar.update(loaded, { speed: rate })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
response.data.pipe(stream)
|
response.data.pipe(stream)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,55 @@
|
|||||||
import { Channel, Feed, GuideChannel, Logo, Stream } from '../models'
|
import { Channel, Feed, GuideChannel, Logo, Stream } from '../models'
|
||||||
import { DataLoaderData } from '../types/dataLoader'
|
import { DataLoaderData } from '../types/dataLoader'
|
||||||
import { Collection } from '@freearhey/core'
|
import { Collection } from '@freearhey/core'
|
||||||
|
|
||||||
export class DataProcessor {
|
export class DataProcessor {
|
||||||
constructor() {}
|
|
||||||
|
process(data: DataLoaderData) {
|
||||||
process(data: DataLoaderData) {
|
let channels = new Collection(data.channels).map(data => new Channel(data))
|
||||||
let channels = new Collection(data.channels).map(data => new Channel(data))
|
const channelsKeyById = channels.keyBy((channel: Channel) => channel.id)
|
||||||
const channelsKeyById = channels.keyBy((channel: Channel) => channel.id)
|
|
||||||
|
const guideChannels = new Collection(data.guides).map(data => new GuideChannel(data))
|
||||||
const guideChannels = new Collection(data.guides).map(data => new GuideChannel(data))
|
const guideChannelsGroupedByStreamId = guideChannels.groupBy((channel: GuideChannel) =>
|
||||||
const guideChannelsGroupedByStreamId = guideChannels.groupBy((channel: GuideChannel) =>
|
channel.getStreamId()
|
||||||
channel.getStreamId()
|
)
|
||||||
)
|
|
||||||
|
const streams = new Collection(data.streams).map(data => new Stream(data))
|
||||||
const streams = new Collection(data.streams).map(data => new Stream(data))
|
const streamsGroupedById = streams.groupBy((stream: Stream) => stream.getId())
|
||||||
const streamsGroupedById = streams.groupBy((stream: Stream) => stream.getId())
|
|
||||||
|
let feeds = new Collection(data.feeds).map(data =>
|
||||||
let feeds = new Collection(data.feeds).map(data =>
|
new Feed(data)
|
||||||
new Feed(data)
|
.withGuideChannels(guideChannelsGroupedByStreamId)
|
||||||
.withGuideChannels(guideChannelsGroupedByStreamId)
|
.withStreams(streamsGroupedById)
|
||||||
.withStreams(streamsGroupedById)
|
.withChannel(channelsKeyById)
|
||||||
.withChannel(channelsKeyById)
|
)
|
||||||
)
|
const feedsKeyByStreamId = feeds.keyBy((feed: Feed) => feed.getStreamId())
|
||||||
const feedsKeyByStreamId = feeds.keyBy((feed: Feed) => feed.getStreamId())
|
|
||||||
|
const logos = new Collection(data.logos).map(data =>
|
||||||
const logos = new Collection(data.logos).map(data =>
|
new Logo(data).withFeed(feedsKeyByStreamId)
|
||||||
new Logo(data).withFeed(feedsKeyByStreamId)
|
)
|
||||||
)
|
const logosGroupedByChannelId = logos.groupBy((logo: Logo) => logo.channelId)
|
||||||
const logosGroupedByChannelId = logos.groupBy((logo: Logo) => logo.channelId)
|
const logosGroupedByStreamId = logos.groupBy((logo: Logo) => logo.getStreamId())
|
||||||
const logosGroupedByStreamId = logos.groupBy((logo: Logo) => logo.getStreamId())
|
|
||||||
|
feeds = feeds.map((feed: Feed) => feed.withLogos(logosGroupedByStreamId))
|
||||||
feeds = feeds.map((feed: Feed) => feed.withLogos(logosGroupedByStreamId))
|
const feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => feed.channelId)
|
||||||
const feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => feed.channelId)
|
|
||||||
|
channels = channels.map((channel: Channel) =>
|
||||||
channels = channels.map((channel: Channel) =>
|
channel.withFeeds(feedsGroupedByChannelId).withLogos(logosGroupedByChannelId)
|
||||||
channel.withFeeds(feedsGroupedByChannelId).withLogos(logosGroupedByChannelId)
|
)
|
||||||
)
|
|
||||||
|
return {
|
||||||
return {
|
guideChannelsGroupedByStreamId,
|
||||||
guideChannelsGroupedByStreamId,
|
feedsGroupedByChannelId,
|
||||||
feedsGroupedByChannelId,
|
logosGroupedByChannelId,
|
||||||
logosGroupedByChannelId,
|
logosGroupedByStreamId,
|
||||||
logosGroupedByStreamId,
|
streamsGroupedById,
|
||||||
streamsGroupedById,
|
feedsKeyByStreamId,
|
||||||
feedsKeyByStreamId,
|
channelsKeyById,
|
||||||
channelsKeyById,
|
guideChannels,
|
||||||
guideChannels,
|
channels,
|
||||||
channels,
|
streams,
|
||||||
streams,
|
feeds,
|
||||||
feeds,
|
logos
|
||||||
logos
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import utc from 'dayjs/plugin/utc'
|
import utc from 'dayjs/plugin/utc'
|
||||||
|
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
|
|
||||||
const date = {}
|
const date = {}
|
||||||
|
|
||||||
date.getUTC = function (d = null) {
|
date.getUTC = function (d = null) {
|
||||||
if (typeof d === 'string') return dayjs.utc(d).startOf('d')
|
if (typeof d === 'string') return dayjs.utc(d).startOf('d')
|
||||||
|
|
||||||
return dayjs.utc().startOf('d')
|
return dayjs.utc().startOf('d')
|
||||||
}
|
}
|
||||||
|
|
||||||
export default date
|
export default date
|
||||||
|
|||||||
@@ -1,105 +1,105 @@
|
|||||||
import { EPGGrabber, GrabCallbackData, EPGGrabberMock, SiteConfig, Channel } from 'epg-grabber'
|
import { EPGGrabber, GrabCallbackData, EPGGrabberMock, SiteConfig, Channel } from 'epg-grabber'
|
||||||
import { Logger, Collection } from '@freearhey/core'
|
import { Logger, Collection } from '@freearhey/core'
|
||||||
import { Queue, ProxyParser } from './'
|
import { Queue, ProxyParser } from './'
|
||||||
import { GrabOptions } from '../commands/epg/grab'
|
import { GrabOptions } from '../commands/epg/grab'
|
||||||
import { TaskQueue, PromisyClass } from 'cwait'
|
import { TaskQueue, PromisyClass } from 'cwait'
|
||||||
import { SocksProxyAgent } from 'socks-proxy-agent'
|
import { SocksProxyAgent } from 'socks-proxy-agent'
|
||||||
|
|
||||||
type GrabberProps = {
|
interface GrabberProps {
|
||||||
logger: Logger
|
logger: Logger
|
||||||
queue: Queue
|
queue: Queue
|
||||||
options: GrabOptions
|
options: GrabOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Grabber {
|
export class Grabber {
|
||||||
logger: Logger
|
logger: Logger
|
||||||
queue: Queue
|
queue: Queue
|
||||||
options: GrabOptions
|
options: GrabOptions
|
||||||
grabber: EPGGrabber | EPGGrabberMock
|
grabber: EPGGrabber | EPGGrabberMock
|
||||||
|
|
||||||
constructor({ logger, queue, options }: GrabberProps) {
|
constructor({ logger, queue, options }: GrabberProps) {
|
||||||
this.logger = logger
|
this.logger = logger
|
||||||
this.queue = queue
|
this.queue = queue
|
||||||
this.options = options
|
this.options = options
|
||||||
this.grabber = process.env.NODE_ENV === 'test' ? new EPGGrabberMock() : new EPGGrabber()
|
this.grabber = process.env.NODE_ENV === 'test' ? new EPGGrabberMock() : new EPGGrabber()
|
||||||
}
|
}
|
||||||
|
|
||||||
async grab(): Promise<{ channels: Collection; programs: Collection }> {
|
async grab(): Promise<{ channels: Collection; programs: Collection }> {
|
||||||
const proxyParser = new ProxyParser()
|
const proxyParser = new ProxyParser()
|
||||||
const taskQueue = new TaskQueue(Promise as PromisyClass, this.options.maxConnections)
|
const taskQueue = new TaskQueue(Promise as PromisyClass, this.options.maxConnections)
|
||||||
|
|
||||||
const total = this.queue.size()
|
const total = this.queue.size()
|
||||||
|
|
||||||
const channels = new Collection()
|
const channels = new Collection()
|
||||||
let programs = new Collection()
|
let programs = new Collection()
|
||||||
let i = 1
|
let i = 1
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
this.queue.items().map(
|
this.queue.items().map(
|
||||||
taskQueue.wrap(
|
taskQueue.wrap(
|
||||||
async (queueItem: { channel: Channel; config: SiteConfig; date: string }) => {
|
async (queueItem: { channel: Channel; config: SiteConfig; date: string }) => {
|
||||||
const { channel, config, date } = queueItem
|
const { channel, config, date } = queueItem
|
||||||
|
|
||||||
channels.add(channel)
|
channels.add(channel)
|
||||||
|
|
||||||
if (this.options.timeout !== undefined) {
|
if (this.options.timeout !== undefined) {
|
||||||
const timeout = parseInt(this.options.timeout)
|
const timeout = parseInt(this.options.timeout)
|
||||||
config.request = { ...config.request, ...{ timeout } }
|
config.request = { ...config.request, ...{ timeout } }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.delay !== undefined) {
|
if (this.options.delay !== undefined) {
|
||||||
const delay = parseInt(this.options.delay)
|
const delay = parseInt(this.options.delay)
|
||||||
config.delay = delay
|
config.delay = delay
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.proxy !== undefined) {
|
if (this.options.proxy !== undefined) {
|
||||||
const proxy = proxyParser.parse(this.options.proxy)
|
const proxy = proxyParser.parse(this.options.proxy)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
proxy.protocol &&
|
proxy.protocol &&
|
||||||
['socks', 'socks5', 'socks5h', 'socks4', 'socks4a'].includes(String(proxy.protocol))
|
['socks', 'socks5', 'socks5h', 'socks4', 'socks4a'].includes(String(proxy.protocol))
|
||||||
) {
|
) {
|
||||||
const socksProxyAgent = new SocksProxyAgent(this.options.proxy)
|
const socksProxyAgent = new SocksProxyAgent(this.options.proxy)
|
||||||
|
|
||||||
config.request = {
|
config.request = {
|
||||||
...config.request,
|
...config.request,
|
||||||
...{ httpAgent: socksProxyAgent, httpsAgent: socksProxyAgent }
|
...{ httpAgent: socksProxyAgent, httpsAgent: socksProxyAgent }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
config.request = { ...config.request, ...{ proxy } }
|
config.request = { ...config.request, ...{ proxy } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.curl === true) {
|
if (this.options.curl === true) {
|
||||||
config.curl = true
|
config.curl = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const _programs = await this.grabber.grab(
|
const _programs = await this.grabber.grab(
|
||||||
channel,
|
channel,
|
||||||
date,
|
date,
|
||||||
config,
|
config,
|
||||||
(data: GrabCallbackData, error: Error | null) => {
|
(data: GrabCallbackData, error: Error | null) => {
|
||||||
const { programs, date } = data
|
const { programs, date } = data
|
||||||
|
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
` [${i}/${total}] ${channel.site} (${channel.lang}) - ${
|
` [${i}/${total}] ${channel.site} (${channel.lang}) - ${
|
||||||
channel.xmltv_id
|
channel.xmltv_id
|
||||||
} - ${date.format('MMM D, YYYY')} (${programs.length} programs)`
|
} - ${date.format('MMM D, YYYY')} (${programs.length} programs)`
|
||||||
)
|
)
|
||||||
if (i < total) i++
|
if (i < total) i++
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
this.logger.info(` ERR: ${error.message}`)
|
this.logger.info(` ERR: ${error.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
programs = programs.concat(new Collection(_programs))
|
programs = programs.concat(new Collection(_programs))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return { channels, programs }
|
return { channels, programs }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,111 +1,111 @@
|
|||||||
import { Collection, Logger, Zip, Storage, StringTemplate } from '@freearhey/core'
|
import { Collection, Logger, Zip, Storage, StringTemplate } from '@freearhey/core'
|
||||||
import epgGrabber from 'epg-grabber'
|
import epgGrabber from 'epg-grabber'
|
||||||
import { OptionValues } from 'commander'
|
import { OptionValues } from 'commander'
|
||||||
import { Channel, Feed, Guide } from '../models'
|
import { Channel, Feed, Guide } from '../models'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { DataLoader, DataProcessor } from '.'
|
import { DataLoader, DataProcessor } from '.'
|
||||||
import { DataLoaderData } from '../types/dataLoader'
|
import { DataLoaderData } from '../types/dataLoader'
|
||||||
import { DataProcessorData } from '../types/dataProcessor'
|
import { DataProcessorData } from '../types/dataProcessor'
|
||||||
import { DATA_DIR } from '../constants'
|
import { DATA_DIR } from '../constants'
|
||||||
|
|
||||||
type GuideManagerProps = {
|
interface GuideManagerProps {
|
||||||
options: OptionValues
|
options: OptionValues
|
||||||
logger: Logger
|
logger: Logger
|
||||||
channels: Collection
|
channels: Collection
|
||||||
programs: Collection
|
programs: Collection
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GuideManager {
|
export class GuideManager {
|
||||||
options: OptionValues
|
options: OptionValues
|
||||||
logger: Logger
|
logger: Logger
|
||||||
channels: Collection
|
channels: Collection
|
||||||
programs: Collection
|
programs: Collection
|
||||||
|
|
||||||
constructor({ channels, programs, logger, options }: GuideManagerProps) {
|
constructor({ channels, programs, logger, options }: GuideManagerProps) {
|
||||||
this.options = options
|
this.options = options
|
||||||
this.logger = logger
|
this.logger = logger
|
||||||
this.channels = channels
|
this.channels = channels
|
||||||
this.programs = programs
|
this.programs = programs
|
||||||
}
|
}
|
||||||
|
|
||||||
async createGuides() {
|
async createGuides() {
|
||||||
const pathTemplate = new StringTemplate(this.options.output)
|
const pathTemplate = new StringTemplate(this.options.output)
|
||||||
|
|
||||||
const processor = new DataProcessor()
|
const processor = new DataProcessor()
|
||||||
const dataStorage = new Storage(DATA_DIR)
|
const dataStorage = new Storage(DATA_DIR)
|
||||||
const loader = new DataLoader({ storage: dataStorage })
|
const loader = new DataLoader({ storage: dataStorage })
|
||||||
const data: DataLoaderData = await loader.load()
|
const data: DataLoaderData = await loader.load()
|
||||||
const { feedsKeyByStreamId, channelsKeyById }: DataProcessorData = processor.process(data)
|
const { feedsKeyByStreamId, channelsKeyById }: DataProcessorData = processor.process(data)
|
||||||
|
|
||||||
const groupedChannels = this.channels
|
const groupedChannels = this.channels
|
||||||
.map((channel: epgGrabber.Channel) => {
|
.map((channel: epgGrabber.Channel) => {
|
||||||
if (channel.xmltv_id && !channel.icon) {
|
if (channel.xmltv_id && !channel.icon) {
|
||||||
const foundFeed: Feed = feedsKeyByStreamId.get(channel.xmltv_id)
|
const foundFeed: Feed = feedsKeyByStreamId.get(channel.xmltv_id)
|
||||||
if (foundFeed && foundFeed.hasLogo()) {
|
if (foundFeed && foundFeed.hasLogo()) {
|
||||||
channel.icon = foundFeed.getLogoUrl()
|
channel.icon = foundFeed.getLogoUrl()
|
||||||
} else {
|
} else {
|
||||||
const [channelId] = channel.xmltv_id.split('@')
|
const [channelId] = channel.xmltv_id.split('@')
|
||||||
const foundChannel: Channel = channelsKeyById.get(channelId)
|
const foundChannel: Channel = channelsKeyById.get(channelId)
|
||||||
if (foundChannel && foundChannel.hasLogo()) {
|
if (foundChannel && foundChannel.hasLogo()) {
|
||||||
channel.icon = foundChannel.getLogoUrl()
|
channel.icon = foundChannel.getLogoUrl()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return channel
|
return channel
|
||||||
})
|
})
|
||||||
.orderBy([
|
.orderBy([
|
||||||
(channel: epgGrabber.Channel) => channel.index,
|
(channel: epgGrabber.Channel) => channel.index,
|
||||||
(channel: epgGrabber.Channel) => channel.xmltv_id
|
(channel: epgGrabber.Channel) => channel.xmltv_id
|
||||||
])
|
])
|
||||||
.uniqBy(
|
.uniqBy(
|
||||||
(channel: epgGrabber.Channel) => `${channel.xmltv_id}:${channel.site}:${channel.lang}`
|
(channel: epgGrabber.Channel) => `${channel.xmltv_id}:${channel.site}:${channel.lang}`
|
||||||
)
|
)
|
||||||
.groupBy((channel: epgGrabber.Channel) => {
|
.groupBy((channel: epgGrabber.Channel) => {
|
||||||
return pathTemplate.format({ lang: channel.lang || 'en', site: channel.site || '' })
|
return pathTemplate.format({ lang: channel.lang || 'en', site: channel.site || '' })
|
||||||
})
|
})
|
||||||
|
|
||||||
const groupedPrograms = this.programs
|
const groupedPrograms = this.programs
|
||||||
.orderBy([
|
.orderBy([
|
||||||
(program: epgGrabber.Program) => program.channel,
|
(program: epgGrabber.Program) => program.channel,
|
||||||
(program: epgGrabber.Program) => program.start
|
(program: epgGrabber.Program) => program.start
|
||||||
])
|
])
|
||||||
.groupBy((program: epgGrabber.Program) => {
|
.groupBy((program: epgGrabber.Program) => {
|
||||||
const lang =
|
const lang =
|
||||||
program.titles && program.titles.length && program.titles[0].lang
|
program.titles && program.titles.length && program.titles[0].lang
|
||||||
? program.titles[0].lang
|
? program.titles[0].lang
|
||||||
: 'en'
|
: 'en'
|
||||||
|
|
||||||
return pathTemplate.format({ lang, site: program.site || '' })
|
return pathTemplate.format({ lang, site: program.site || '' })
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const groupKey of groupedPrograms.keys()) {
|
for (const groupKey of groupedPrograms.keys()) {
|
||||||
const guide = new Guide({
|
const guide = new Guide({
|
||||||
filepath: groupKey,
|
filepath: groupKey,
|
||||||
gzip: this.options.gzip,
|
gzip: this.options.gzip,
|
||||||
channels: new Collection(groupedChannels.get(groupKey)),
|
channels: new Collection(groupedChannels.get(groupKey)),
|
||||||
programs: new Collection(groupedPrograms.get(groupKey))
|
programs: new Collection(groupedPrograms.get(groupKey))
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.save(guide)
|
await this.save(guide)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async save(guide: Guide) {
|
async save(guide: Guide) {
|
||||||
const storage = new Storage(path.dirname(guide.filepath))
|
const storage = new Storage(path.dirname(guide.filepath))
|
||||||
const xmlFilepath = guide.filepath
|
const xmlFilepath = guide.filepath
|
||||||
const xmlFilename = path.basename(xmlFilepath)
|
const xmlFilename = path.basename(xmlFilepath)
|
||||||
this.logger.info(` saving to "${xmlFilepath}"...`)
|
this.logger.info(` saving to "${xmlFilepath}"...`)
|
||||||
const xmltv = guide.toString()
|
const xmltv = guide.toString()
|
||||||
await storage.save(xmlFilename, xmltv)
|
await storage.save(xmlFilename, xmltv)
|
||||||
|
|
||||||
if (guide.gzip) {
|
if (guide.gzip) {
|
||||||
const zip = new Zip()
|
const zip = new Zip()
|
||||||
const compressed = zip.compress(xmltv)
|
const compressed = zip.compress(xmltv)
|
||||||
const gzFilepath = `${guide.filepath}.gz`
|
const gzFilepath = `${guide.filepath}.gz`
|
||||||
const gzFilename = path.basename(gzFilepath)
|
const gzFilename = path.basename(gzFilepath)
|
||||||
this.logger.info(` saving to "${gzFilepath}"...`)
|
this.logger.info(` saving to "${gzFilepath}"...`)
|
||||||
await storage.save(gzFilename, compressed)
|
await storage.save(gzFilename, compressed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +1,55 @@
|
|||||||
type Column = {
|
interface Column {
|
||||||
name: string
|
name: string
|
||||||
nowrap?: boolean
|
nowrap?: boolean
|
||||||
align?: string
|
align?: string
|
||||||
colspan?: number
|
colspan?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type DataItem = {
|
type DataItem = {
|
||||||
value: string
|
value: string
|
||||||
nowrap?: boolean
|
nowrap?: boolean
|
||||||
align?: string
|
align?: string
|
||||||
colspan?: number
|
colspan?: number
|
||||||
}[]
|
}[]
|
||||||
|
|
||||||
export class HTMLTable {
|
export class HTMLTable {
|
||||||
data: DataItem[]
|
data: DataItem[]
|
||||||
columns: Column[]
|
columns: Column[]
|
||||||
|
|
||||||
constructor(data: DataItem[], columns: Column[]) {
|
constructor(data: DataItem[], columns: Column[]) {
|
||||||
this.data = data
|
this.data = data
|
||||||
this.columns = columns
|
this.columns = columns
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
let output = '<table>\r\n'
|
let output = '<table>\r\n'
|
||||||
|
|
||||||
output += ' <thead>\r\n <tr>'
|
output += ' <thead>\r\n <tr>'
|
||||||
for (const column of this.columns) {
|
for (const column of this.columns) {
|
||||||
const nowrap = column.nowrap ? ' nowrap' : ''
|
const nowrap = column.nowrap ? ' nowrap' : ''
|
||||||
const align = column.align ? ` align="${column.align}"` : ''
|
const align = column.align ? ` align="${column.align}"` : ''
|
||||||
const colspan = column.colspan ? ` colspan="${column.colspan}"` : ''
|
const colspan = column.colspan ? ` colspan="${column.colspan}"` : ''
|
||||||
|
|
||||||
output += `<th${align}${nowrap}${colspan}>${column.name}</th>`
|
output += `<th${align}${nowrap}${colspan}>${column.name}</th>`
|
||||||
}
|
}
|
||||||
output += '</tr>\r\n </thead>\r\n'
|
output += '</tr>\r\n </thead>\r\n'
|
||||||
|
|
||||||
output += ' <tbody>\r\n'
|
output += ' <tbody>\r\n'
|
||||||
for (const row of this.data) {
|
for (const row of this.data) {
|
||||||
output += ' <tr>'
|
output += ' <tr>'
|
||||||
for (const item of row) {
|
for (const item of row) {
|
||||||
const nowrap = item.nowrap ? ' nowrap' : ''
|
const nowrap = item.nowrap ? ' nowrap' : ''
|
||||||
const align = item.align ? ` align="${item.align}"` : ''
|
const align = item.align ? ` align="${item.align}"` : ''
|
||||||
const colspan = item.colspan ? ` colspan="${item.colspan}"` : ''
|
const colspan = item.colspan ? ` colspan="${item.colspan}"` : ''
|
||||||
|
|
||||||
output += `<td${align}${nowrap}${colspan}>${item.value}</td>`
|
output += `<td${align}${nowrap}${colspan}>${item.value}</td>`
|
||||||
}
|
}
|
||||||
output += '</tr>\r\n'
|
output += '</tr>\r\n'
|
||||||
}
|
}
|
||||||
output += ' </tbody>\r\n'
|
output += ' </tbody>\r\n'
|
||||||
|
|
||||||
output += '</table>'
|
output += '</table>'
|
||||||
|
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
export * from './apiClient'
|
export * from './apiClient'
|
||||||
export * from './channelsParser'
|
export * from './channelsParser'
|
||||||
export * from './configLoader'
|
export * from './configLoader'
|
||||||
export * from './dataLoader'
|
export * from './dataLoader'
|
||||||
export * from './dataProcessor'
|
export * from './dataProcessor'
|
||||||
export * from './grabber'
|
export * from './grabber'
|
||||||
export * from './guideManager'
|
export * from './guideManager'
|
||||||
export * from './htmlTable'
|
export * from './htmlTable'
|
||||||
export * from './issueLoader'
|
export * from './issueLoader'
|
||||||
export * from './issueParser'
|
export * from './issueParser'
|
||||||
export * from './job'
|
export * from './job'
|
||||||
export * from './proxyParser'
|
export * from './proxyParser'
|
||||||
export * from './queue'
|
export * from './queue'
|
||||||
export * from './queueCreator'
|
export * from './queueCreator'
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
import { restEndpointMethods } from '@octokit/plugin-rest-endpoint-methods'
|
import { restEndpointMethods } from '@octokit/plugin-rest-endpoint-methods'
|
||||||
import { paginateRest } from '@octokit/plugin-paginate-rest'
|
import { paginateRest } from '@octokit/plugin-paginate-rest'
|
||||||
import { TESTING, OWNER, REPO } from '../constants'
|
import { TESTING, OWNER, REPO } from '../constants'
|
||||||
import { Collection } from '@freearhey/core'
|
import { Collection } from '@freearhey/core'
|
||||||
import { Octokit } from '@octokit/core'
|
import { Octokit } from '@octokit/core'
|
||||||
import { IssueParser } from './'
|
import { IssueParser } from './'
|
||||||
|
|
||||||
const CustomOctokit = Octokit.plugin(paginateRest, restEndpointMethods)
|
const CustomOctokit = Octokit.plugin(paginateRest, restEndpointMethods)
|
||||||
const octokit = new CustomOctokit()
|
const octokit = new CustomOctokit()
|
||||||
|
|
||||||
export class IssueLoader {
|
export class IssueLoader {
|
||||||
async load(props?: { labels: string[] | string }) {
|
async load(props?: { labels: string[] | string }) {
|
||||||
let labels = ''
|
let labels = ''
|
||||||
if (props && props.labels) {
|
if (props && props.labels) {
|
||||||
labels = Array.isArray(props.labels) ? props.labels.join(',') : props.labels
|
labels = Array.isArray(props.labels) ? props.labels.join(',') : props.labels
|
||||||
}
|
}
|
||||||
let issues: object[] = []
|
let issues: object[] = []
|
||||||
if (TESTING) {
|
if (TESTING) {
|
||||||
issues = (await import('../../tests/__data__/input/sites_update/issues.mjs')).default
|
issues = (await import('../../tests/__data__/input/sites_update/issues.mjs')).default
|
||||||
} else {
|
} else {
|
||||||
issues = await octokit.paginate(octokit.rest.issues.listForRepo, {
|
issues = await octokit.paginate(octokit.rest.issues.listForRepo, {
|
||||||
owner: OWNER,
|
owner: OWNER,
|
||||||
repo: REPO,
|
repo: REPO,
|
||||||
per_page: 100,
|
per_page: 100,
|
||||||
labels,
|
labels,
|
||||||
state: 'open',
|
state: 'open',
|
||||||
headers: {
|
headers: {
|
||||||
'X-GitHub-Api-Version': '2022-11-28'
|
'X-GitHub-Api-Version': '2022-11-28'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const parser = new IssueParser()
|
const parser = new IssueParser()
|
||||||
|
|
||||||
return new Collection(issues).map(parser.parse)
|
return new Collection(issues).map(parser.parse)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
import { Dictionary } from '@freearhey/core'
|
import { Dictionary } from '@freearhey/core'
|
||||||
import { Issue } from '../models'
|
import { Issue } from '../models'
|
||||||
|
|
||||||
const FIELDS = new Dictionary({
|
const FIELDS = new Dictionary({
|
||||||
Site: 'site'
|
Site: 'site'
|
||||||
})
|
})
|
||||||
|
|
||||||
export class IssueParser {
|
export class IssueParser {
|
||||||
parse(issue: { number: number; body: string; labels: { name: string }[] }): Issue {
|
parse(issue: { number: number; body: string; labels: { name: string }[] }): Issue {
|
||||||
const fields = issue.body.split('###')
|
const fields = issue.body.split('###')
|
||||||
|
|
||||||
const data = new Dictionary()
|
const data = new Dictionary()
|
||||||
fields.forEach((field: string) => {
|
fields.forEach((field: string) => {
|
||||||
const parsed = field.split(/\r?\n/).filter(Boolean)
|
const parsed = field.split(/\r?\n/).filter(Boolean)
|
||||||
let _label = parsed.shift()
|
let _label = parsed.shift()
|
||||||
_label = _label ? _label.trim() : ''
|
_label = _label ? _label.trim() : ''
|
||||||
let _value = parsed.join('\r\n')
|
let _value = parsed.join('\r\n')
|
||||||
_value = _value ? _value.trim() : ''
|
_value = _value ? _value.trim() : ''
|
||||||
|
|
||||||
if (!_label || !_value) return data
|
if (!_label || !_value) return data
|
||||||
|
|
||||||
const id: string = FIELDS.get(_label)
|
const id: string = FIELDS.get(_label)
|
||||||
const value: string = _value === '_No response_' || _value === 'None' ? '' : _value
|
const value: string = _value === '_No response_' || _value === 'None' ? '' : _value
|
||||||
|
|
||||||
if (!id) return
|
if (!id) return
|
||||||
|
|
||||||
data.set(id, value)
|
data.set(id, value)
|
||||||
})
|
})
|
||||||
|
|
||||||
const labels = issue.labels.map(label => label.name)
|
const labels = issue.labels.map(label => label.name)
|
||||||
|
|
||||||
return new Issue({ number: issue.number, labels, data })
|
return new Issue({ number: issue.number, labels, data })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
import { Logger } from '@freearhey/core'
|
import { Logger } from '@freearhey/core'
|
||||||
import { Queue, Grabber, GuideManager } from '.'
|
import { Queue, Grabber, GuideManager } from '.'
|
||||||
import { GrabOptions } from '../commands/epg/grab'
|
import { GrabOptions } from '../commands/epg/grab'
|
||||||
|
|
||||||
type JobProps = {
|
interface JobProps {
|
||||||
options: GrabOptions
|
options: GrabOptions
|
||||||
logger: Logger
|
logger: Logger
|
||||||
queue: Queue
|
queue: Queue
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Job {
|
export class Job {
|
||||||
options: GrabOptions
|
options: GrabOptions
|
||||||
logger: Logger
|
logger: Logger
|
||||||
grabber: Grabber
|
grabber: Grabber
|
||||||
|
|
||||||
constructor({ queue, logger, options }: JobProps) {
|
constructor({ queue, logger, options }: JobProps) {
|
||||||
this.options = options
|
this.options = options
|
||||||
this.logger = logger
|
this.logger = logger
|
||||||
this.grabber = new Grabber({ logger, queue, options })
|
this.grabber = new Grabber({ logger, queue, options })
|
||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
const { channels, programs } = await this.grabber.grab()
|
const { channels, programs } = await this.grabber.grab()
|
||||||
|
|
||||||
const manager = new GuideManager({
|
const manager = new GuideManager({
|
||||||
channels,
|
channels,
|
||||||
programs,
|
programs,
|
||||||
options: this.options,
|
options: this.options,
|
||||||
logger: this.logger
|
logger: this.logger
|
||||||
})
|
})
|
||||||
|
|
||||||
await manager.createGuides()
|
await manager.createGuides()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
import { URL } from 'node:url'
|
import { URL } from 'node:url'
|
||||||
|
|
||||||
type ProxyParserResult = {
|
interface ProxyParserResult {
|
||||||
protocol: string | null
|
protocol: string | null
|
||||||
auth?: {
|
auth?: {
|
||||||
username?: string
|
username?: string
|
||||||
password?: string
|
password?: string
|
||||||
}
|
}
|
||||||
host: string
|
host: string
|
||||||
port: number | null
|
port: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ProxyParser {
|
export class ProxyParser {
|
||||||
parse(_url: string): ProxyParserResult {
|
parse(_url: string): ProxyParserResult {
|
||||||
const parsed = new URL(_url)
|
const parsed = new URL(_url)
|
||||||
|
|
||||||
const result: ProxyParserResult = {
|
const result: ProxyParserResult = {
|
||||||
protocol: parsed.protocol.replace(':', '') || null,
|
protocol: parsed.protocol.replace(':', '') || null,
|
||||||
host: parsed.hostname,
|
host: parsed.hostname,
|
||||||
port: parsed.port ? parseInt(parsed.port) : null
|
port: parsed.port ? parseInt(parsed.port) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.username || parsed.password) {
|
if (parsed.username || parsed.password) {
|
||||||
result.auth = {}
|
result.auth = {}
|
||||||
if (parsed.username) result.auth.username = parsed.username
|
if (parsed.username) result.auth.username = parsed.username
|
||||||
if (parsed.password) result.auth.password = parsed.password
|
if (parsed.password) result.auth.password = parsed.password
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,45 @@
|
|||||||
import { Dictionary } from '@freearhey/core'
|
import { Dictionary } from '@freearhey/core'
|
||||||
import { SiteConfig, Channel } from 'epg-grabber'
|
import { SiteConfig, Channel } from 'epg-grabber'
|
||||||
|
|
||||||
export type QueueItem = {
|
export interface QueueItem {
|
||||||
channel: Channel
|
channel: Channel
|
||||||
date: string
|
date: string
|
||||||
config: SiteConfig
|
config: SiteConfig
|
||||||
error: string | null
|
error: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Queue {
|
export class Queue {
|
||||||
_data: Dictionary
|
_data: Dictionary
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._data = new Dictionary()
|
this._data = new Dictionary()
|
||||||
}
|
}
|
||||||
|
|
||||||
missing(key: string): boolean {
|
missing(key: string): boolean {
|
||||||
return this._data.missing(key)
|
return this._data.missing(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
add(
|
add(
|
||||||
key: string,
|
key: string,
|
||||||
{ channel, config, date }: { channel: Channel; date: string | null; config: SiteConfig }
|
{ channel, config, date }: { channel: Channel; date: string | null; config: SiteConfig }
|
||||||
) {
|
) {
|
||||||
this._data.set(key, {
|
this._data.set(key, {
|
||||||
channel,
|
channel,
|
||||||
date,
|
date,
|
||||||
config,
|
config,
|
||||||
error: null
|
error: null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
size(): number {
|
size(): number {
|
||||||
return Object.values(this._data.data()).length
|
return Object.values(this._data.data()).length
|
||||||
}
|
}
|
||||||
|
|
||||||
items(): QueueItem[] {
|
items(): QueueItem[] {
|
||||||
return Object.values(this._data.data()) as QueueItem[]
|
return Object.values(this._data.data()) as QueueItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
isEmpty(): boolean {
|
isEmpty(): boolean {
|
||||||
return this.size() === 0
|
return this.size() === 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +1,63 @@
|
|||||||
import { Storage, Collection, DateTime, Logger } from '@freearhey/core'
|
import { Storage, Collection, DateTime, Logger } from '@freearhey/core'
|
||||||
import { SITES_DIR, DATA_DIR } from '../constants'
|
import { SITES_DIR, DATA_DIR } from '../constants'
|
||||||
import { GrabOptions } from '../commands/epg/grab'
|
import { GrabOptions } from '../commands/epg/grab'
|
||||||
import { ConfigLoader, Queue } from './'
|
import { ConfigLoader, Queue } from './'
|
||||||
import { SiteConfig } from 'epg-grabber'
|
import { SiteConfig } from 'epg-grabber'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
type QueueCreatorProps = {
|
interface QueueCreatorProps {
|
||||||
logger: Logger
|
logger: Logger
|
||||||
options: GrabOptions
|
options: GrabOptions
|
||||||
channels: Collection
|
channels: Collection
|
||||||
}
|
}
|
||||||
|
|
||||||
export class QueueCreator {
|
export class QueueCreator {
|
||||||
configLoader: ConfigLoader
|
configLoader: ConfigLoader
|
||||||
logger: Logger
|
logger: Logger
|
||||||
sitesStorage: Storage
|
sitesStorage: Storage
|
||||||
dataStorage: Storage
|
dataStorage: Storage
|
||||||
channels: Collection
|
channels: Collection
|
||||||
options: GrabOptions
|
options: GrabOptions
|
||||||
|
|
||||||
constructor({ channels, logger, options }: QueueCreatorProps) {
|
constructor({ channels, logger, options }: QueueCreatorProps) {
|
||||||
this.channels = channels
|
this.channels = channels
|
||||||
this.logger = logger
|
this.logger = logger
|
||||||
this.sitesStorage = new Storage()
|
this.sitesStorage = new Storage()
|
||||||
this.dataStorage = new Storage(DATA_DIR)
|
this.dataStorage = new Storage(DATA_DIR)
|
||||||
this.options = options
|
this.options = options
|
||||||
this.configLoader = new ConfigLoader()
|
this.configLoader = new ConfigLoader()
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(): Promise<Queue> {
|
async create(): Promise<Queue> {
|
||||||
let index = 0
|
let index = 0
|
||||||
const queue = new Queue()
|
const queue = new Queue()
|
||||||
for (const channel of this.channels.all()) {
|
for (const channel of this.channels.all()) {
|
||||||
channel.index = index++
|
channel.index = index++
|
||||||
if (!channel.site || !channel.site_id || !channel.name) continue
|
if (!channel.site || !channel.site_id || !channel.name) continue
|
||||||
|
|
||||||
const configPath = path.resolve(SITES_DIR, `${channel.site}/${channel.site}.config.js`)
|
const configPath = path.resolve(SITES_DIR, `${channel.site}/${channel.site}.config.js`)
|
||||||
const config: SiteConfig = await this.configLoader.load(configPath)
|
const config: SiteConfig = await this.configLoader.load(configPath)
|
||||||
|
|
||||||
if (!channel.xmltv_id) {
|
if (!channel.xmltv_id) {
|
||||||
channel.xmltv_id = channel.site_id
|
channel.xmltv_id = channel.site_id
|
||||||
}
|
}
|
||||||
|
|
||||||
const days = this.options.days || config.days || 1
|
const days = this.options.days || config.days || 1
|
||||||
const currDate = new DateTime(process.env.CURR_DATE || new Date().toISOString())
|
const currDate = new DateTime(process.env.CURR_DATE || new Date().toISOString())
|
||||||
const dates = Array.from({ length: days }, (_, day) => currDate.add(day, 'd'))
|
const dates = Array.from({ length: days }, (_, day) => currDate.add(day, 'd'))
|
||||||
dates.forEach((date: DateTime) => {
|
dates.forEach((date: DateTime) => {
|
||||||
const dateString = date.toJSON()
|
const dateString = date.toJSON()
|
||||||
const key = `${channel.site}:${channel.lang}:${channel.xmltv_id}:${dateString}`
|
const key = `${channel.site}:${channel.lang}:${channel.xmltv_id}:${dateString}`
|
||||||
if (queue.missing(key)) {
|
if (queue.missing(key)) {
|
||||||
queue.add(key, {
|
queue.add(key, {
|
||||||
channel,
|
channel,
|
||||||
date: dateString,
|
date: dateString,
|
||||||
config
|
config
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return queue
|
return queue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,164 +1,164 @@
|
|||||||
import { ChannelData, ChannelSearchableData } from '../types/channel'
|
import { ChannelData, ChannelSearchableData } from '../types/channel'
|
||||||
import { Collection, Dictionary } from '@freearhey/core'
|
import { Collection, Dictionary } from '@freearhey/core'
|
||||||
import { Stream, Feed, Logo, GuideChannel } from './'
|
import { Stream, Feed, Logo, GuideChannel } from './'
|
||||||
|
|
||||||
export class Channel {
|
export class Channel {
|
||||||
id?: string
|
id?: string
|
||||||
name?: string
|
name?: string
|
||||||
altNames?: Collection
|
altNames?: Collection
|
||||||
network?: string
|
network?: string
|
||||||
owners?: Collection
|
owners?: Collection
|
||||||
countryCode?: string
|
countryCode?: string
|
||||||
subdivisionCode?: string
|
subdivisionCode?: string
|
||||||
cityName?: string
|
cityName?: string
|
||||||
categoryIds?: Collection
|
categoryIds?: Collection
|
||||||
isNSFW: boolean = false
|
isNSFW = false
|
||||||
launched?: string
|
launched?: string
|
||||||
closed?: string
|
closed?: string
|
||||||
replacedBy?: string
|
replacedBy?: string
|
||||||
website?: string
|
website?: string
|
||||||
feeds?: Collection
|
feeds?: Collection
|
||||||
logos: Collection = new Collection()
|
logos: Collection = new Collection()
|
||||||
|
|
||||||
constructor(data?: ChannelData) {
|
constructor(data?: ChannelData) {
|
||||||
if (!data) return
|
if (!data) return
|
||||||
|
|
||||||
this.id = data.id
|
this.id = data.id
|
||||||
this.name = data.name
|
this.name = data.name
|
||||||
this.altNames = new Collection(data.alt_names)
|
this.altNames = new Collection(data.alt_names)
|
||||||
this.network = data.network || undefined
|
this.network = data.network || undefined
|
||||||
this.owners = new Collection(data.owners)
|
this.owners = new Collection(data.owners)
|
||||||
this.countryCode = data.country
|
this.countryCode = data.country
|
||||||
this.subdivisionCode = data.subdivision || undefined
|
this.subdivisionCode = data.subdivision || undefined
|
||||||
this.cityName = data.city || undefined
|
this.cityName = data.city || undefined
|
||||||
this.categoryIds = new Collection(data.categories)
|
this.categoryIds = new Collection(data.categories)
|
||||||
this.isNSFW = data.is_nsfw
|
this.isNSFW = data.is_nsfw
|
||||||
this.launched = data.launched || undefined
|
this.launched = data.launched || undefined
|
||||||
this.closed = data.closed || undefined
|
this.closed = data.closed || undefined
|
||||||
this.replacedBy = data.replaced_by || undefined
|
this.replacedBy = data.replaced_by || undefined
|
||||||
this.website = data.website || undefined
|
this.website = data.website || undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
withFeeds(feedsGroupedByChannelId: Dictionary): this {
|
withFeeds(feedsGroupedByChannelId: Dictionary): this {
|
||||||
if (this.id) this.feeds = new Collection(feedsGroupedByChannelId.get(this.id))
|
if (this.id) this.feeds = new Collection(feedsGroupedByChannelId.get(this.id))
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
withLogos(logosGroupedByChannelId: Dictionary): this {
|
withLogos(logosGroupedByChannelId: Dictionary): this {
|
||||||
if (this.id) this.logos = new Collection(logosGroupedByChannelId.get(this.id))
|
if (this.id) this.logos = new Collection(logosGroupedByChannelId.get(this.id))
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
getFeeds(): Collection {
|
getFeeds(): Collection {
|
||||||
if (!this.feeds) return new Collection()
|
if (!this.feeds) return new Collection()
|
||||||
|
|
||||||
return this.feeds
|
return this.feeds
|
||||||
}
|
}
|
||||||
|
|
||||||
getGuideChannels(): Collection {
|
getGuideChannels(): Collection {
|
||||||
let channels = new Collection()
|
let channels = new Collection()
|
||||||
|
|
||||||
this.getFeeds().forEach((feed: Feed) => {
|
this.getFeeds().forEach((feed: Feed) => {
|
||||||
channels = channels.concat(feed.getGuideChannels())
|
channels = channels.concat(feed.getGuideChannels())
|
||||||
})
|
})
|
||||||
|
|
||||||
return channels
|
return channels
|
||||||
}
|
}
|
||||||
|
|
||||||
getGuideChannelNames(): Collection {
|
getGuideChannelNames(): Collection {
|
||||||
return this.getGuideChannels()
|
return this.getGuideChannels()
|
||||||
.map((channel: GuideChannel) => channel.siteName)
|
.map((channel: GuideChannel) => channel.siteName)
|
||||||
.uniq()
|
.uniq()
|
||||||
}
|
}
|
||||||
|
|
||||||
getStreams(): Collection {
|
getStreams(): Collection {
|
||||||
let streams = new Collection()
|
let streams = new Collection()
|
||||||
|
|
||||||
this.getFeeds().forEach((feed: Feed) => {
|
this.getFeeds().forEach((feed: Feed) => {
|
||||||
streams = streams.concat(feed.getStreams())
|
streams = streams.concat(feed.getStreams())
|
||||||
})
|
})
|
||||||
|
|
||||||
return streams
|
return streams
|
||||||
}
|
}
|
||||||
|
|
||||||
getStreamNames(): Collection {
|
getStreamNames(): Collection {
|
||||||
return this.getStreams()
|
return this.getStreams()
|
||||||
.map((stream: Stream) => stream.getName())
|
.map((stream: Stream) => stream.getName())
|
||||||
.uniq()
|
.uniq()
|
||||||
}
|
}
|
||||||
|
|
||||||
getFeedFullNames(): Collection {
|
getFeedFullNames(): Collection {
|
||||||
return this.getFeeds()
|
return this.getFeeds()
|
||||||
.map((feed: Feed) => feed.getFullName())
|
.map((feed: Feed) => feed.getFullName())
|
||||||
.uniq()
|
.uniq()
|
||||||
}
|
}
|
||||||
|
|
||||||
getName(): string {
|
getName(): string {
|
||||||
return this.name || ''
|
return this.name || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
getId(): string {
|
getId(): string {
|
||||||
return this.id || ''
|
return this.id || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
getAltNames(): Collection {
|
getAltNames(): Collection {
|
||||||
return this.altNames || new Collection()
|
return this.altNames || new Collection()
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogos(): Collection {
|
getLogos(): Collection {
|
||||||
function feed(logo: Logo): number {
|
function feed(logo: Logo): number {
|
||||||
if (!logo.feed) return 1
|
if (!logo.feed) return 1
|
||||||
if (logo.feed.isMain) return 1
|
if (logo.feed.isMain) return 1
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function format(logo: Logo): number {
|
function format(logo: Logo): number {
|
||||||
const levelByFormat: { [key: string]: number } = {
|
const levelByFormat: Record<string, number> = {
|
||||||
SVG: 0,
|
SVG: 0,
|
||||||
PNG: 3,
|
PNG: 3,
|
||||||
APNG: 1,
|
APNG: 1,
|
||||||
WebP: 1,
|
WebP: 1,
|
||||||
AVIF: 1,
|
AVIF: 1,
|
||||||
JPEG: 2,
|
JPEG: 2,
|
||||||
GIF: 1
|
GIF: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
return logo.format ? levelByFormat[logo.format] : 0
|
return logo.format ? levelByFormat[logo.format] : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function size(logo: Logo): number {
|
function size(logo: Logo): number {
|
||||||
return Math.abs(512 - logo.width) + Math.abs(512 - logo.height)
|
return Math.abs(512 - logo.width) + Math.abs(512 - logo.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.logos.orderBy([feed, format, size], ['desc', 'desc', 'asc'], false)
|
return this.logos.orderBy([feed, format, size], ['desc', 'desc', 'asc'], false)
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogo(): Logo | undefined {
|
getLogo(): Logo | undefined {
|
||||||
return this.getLogos().first()
|
return this.getLogos().first()
|
||||||
}
|
}
|
||||||
|
|
||||||
hasLogo(): boolean {
|
hasLogo(): boolean {
|
||||||
return this.getLogos().notEmpty()
|
return this.getLogos().notEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogoUrl(): string {
|
getLogoUrl(): string {
|
||||||
const logo = this.getLogo()
|
const logo = this.getLogo()
|
||||||
if (!logo) return ''
|
if (!logo) return ''
|
||||||
|
|
||||||
return logo.url || ''
|
return logo.url || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
getSearchable(): ChannelSearchableData {
|
getSearchable(): ChannelSearchableData {
|
||||||
return {
|
return {
|
||||||
id: this.getId(),
|
id: this.getId(),
|
||||||
name: this.getName(),
|
name: this.getName(),
|
||||||
altNames: this.getAltNames().all(),
|
altNames: this.getAltNames().all(),
|
||||||
guideNames: this.getGuideChannelNames().all(),
|
guideNames: this.getGuideChannelNames().all(),
|
||||||
streamNames: this.getStreamNames().all(),
|
streamNames: this.getStreamNames().all(),
|
||||||
feedFullNames: this.getFeedFullNames().all()
|
feedFullNames: this.getFeedFullNames().all()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,77 +1,77 @@
|
|||||||
import { Collection } from '@freearhey/core'
|
import { Collection } from '@freearhey/core'
|
||||||
import epgGrabber from 'epg-grabber'
|
import epgGrabber from 'epg-grabber'
|
||||||
|
|
||||||
export class ChannelList {
|
export class ChannelList {
|
||||||
channels: Collection = new Collection()
|
channels: Collection = new Collection()
|
||||||
|
|
||||||
constructor(data: { channels: epgGrabber.Channel[] }) {
|
constructor(data: { channels: epgGrabber.Channel[] }) {
|
||||||
this.channels = new Collection(data.channels)
|
this.channels = new Collection(data.channels)
|
||||||
}
|
}
|
||||||
|
|
||||||
add(channel: epgGrabber.Channel): this {
|
add(channel: epgGrabber.Channel): this {
|
||||||
this.channels.add(channel)
|
this.channels.add(channel)
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
get(siteId: string): epgGrabber.Channel | undefined {
|
get(siteId: string): epgGrabber.Channel | undefined {
|
||||||
return this.channels.find((channel: epgGrabber.Channel) => channel.site_id == siteId)
|
return this.channels.find((channel: epgGrabber.Channel) => channel.site_id == siteId)
|
||||||
}
|
}
|
||||||
|
|
||||||
sort(): this {
|
sort(): this {
|
||||||
this.channels = this.channels.orderBy([
|
this.channels = this.channels.orderBy([
|
||||||
(channel: epgGrabber.Channel) => channel.lang || '_',
|
(channel: epgGrabber.Channel) => channel.lang || '_',
|
||||||
(channel: epgGrabber.Channel) => (channel.xmltv_id ? channel.xmltv_id.toLowerCase() : '0'),
|
(channel: epgGrabber.Channel) => (channel.xmltv_id ? channel.xmltv_id.toLowerCase() : '0'),
|
||||||
(channel: epgGrabber.Channel) => channel.site_id
|
(channel: epgGrabber.Channel) => channel.site_id
|
||||||
])
|
])
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
function escapeString(value: string, defaultValue: string = '') {
|
function escapeString(value: string, defaultValue = '') {
|
||||||
if (!value) return defaultValue
|
if (!value) return defaultValue
|
||||||
|
|
||||||
const regex = new RegExp(
|
const regex = new RegExp(
|
||||||
'((?:[\0-\x08\x0B\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))|([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|(?:\\uD83F[\\uDFFE\\uDFFF])|(?:\\uD87F[\\uDF' +
|
'((?:[\0-\x08\x0B\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))|([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|(?:\\uD83F[\\uDFFE\\uDFFF])|(?:\\uD87F[\\uDF' +
|
||||||
'FE\\uDFFF])|(?:\\uD8BF[\\uDFFE\\uDFFF])|(?:\\uD8FF[\\uDFFE\\uDFFF])|(?:\\uD93F[\\uDFFE\\uD' +
|
'FE\\uDFFF])|(?:\\uD8BF[\\uDFFE\\uDFFF])|(?:\\uD8FF[\\uDFFE\\uDFFF])|(?:\\uD93F[\\uDFFE\\uD' +
|
||||||
'FFF])|(?:\\uD97F[\\uDFFE\\uDFFF])|(?:\\uD9BF[\\uDFFE\\uDFFF])|(?:\\uD9FF[\\uDFFE\\uDFFF])' +
|
'FFF])|(?:\\uD97F[\\uDFFE\\uDFFF])|(?:\\uD9BF[\\uDFFE\\uDFFF])|(?:\\uD9FF[\\uDFFE\\uDFFF])' +
|
||||||
'|(?:\\uDA3F[\\uDFFE\\uDFFF])|(?:\\uDA7F[\\uDFFE\\uDFFF])|(?:\\uDABF[\\uDFFE\\uDFFF])|(?:\\' +
|
'|(?:\\uDA3F[\\uDFFE\\uDFFF])|(?:\\uDA7F[\\uDFFE\\uDFFF])|(?:\\uDABF[\\uDFFE\\uDFFF])|(?:\\' +
|
||||||
'uDAFF[\\uDFFE\\uDFFF])|(?:\\uDB3F[\\uDFFE\\uDFFF])|(?:\\uDB7F[\\uDFFE\\uDFFF])|(?:\\uDBBF' +
|
'uDAFF[\\uDFFE\\uDFFF])|(?:\\uDB3F[\\uDFFE\\uDFFF])|(?:\\uDB7F[\\uDFFE\\uDFFF])|(?:\\uDBBF' +
|
||||||
'[\\uDFFE\\uDFFF])|(?:\\uDBFF[\\uDFFE\\uDFFF])(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\' +
|
'[\\uDFFE\\uDFFF])|(?:\\uDBFF[\\uDFFE\\uDFFF])(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\' +
|
||||||
'uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|' +
|
'uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|' +
|
||||||
'(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))',
|
'(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))',
|
||||||
'g'
|
'g'
|
||||||
)
|
)
|
||||||
|
|
||||||
value = String(value || '').replace(regex, '')
|
value = String(value || '').replace(regex, '')
|
||||||
|
|
||||||
return value
|
return value
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
.replace(/>/g, '>')
|
.replace(/>/g, '>')
|
||||||
.replace(/"/g, '"')
|
.replace(/"/g, '"')
|
||||||
.replace(/'/g, ''')
|
.replace(/'/g, ''')
|
||||||
.replace(/\n|\r/g, ' ')
|
.replace(/\n|\r/g, ' ')
|
||||||
.replace(/ +/g, ' ')
|
.replace(/ +/g, ' ')
|
||||||
.trim()
|
.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = '<?xml version="1.0" encoding="UTF-8"?>\r\n<channels>\r\n'
|
let output = '<?xml version="1.0" encoding="UTF-8"?>\r\n<channels>\r\n'
|
||||||
|
|
||||||
this.channels.forEach((channel: epgGrabber.Channel) => {
|
this.channels.forEach((channel: epgGrabber.Channel) => {
|
||||||
const logo = channel.logo ? ` logo="${channel.logo}"` : ''
|
const logo = channel.logo ? ` logo="${channel.logo}"` : ''
|
||||||
const xmltv_id = channel.xmltv_id ? escapeString(channel.xmltv_id) : ''
|
const xmltv_id = channel.xmltv_id ? escapeString(channel.xmltv_id) : ''
|
||||||
const lang = channel.lang || ''
|
const lang = channel.lang || ''
|
||||||
const site_id = channel.site_id || ''
|
const site_id = channel.site_id || ''
|
||||||
const site = channel.site || ''
|
const site = channel.site || ''
|
||||||
const displayName = channel.name ? escapeString(channel.name) : ''
|
const displayName = channel.name ? escapeString(channel.name) : ''
|
||||||
|
|
||||||
output += ` <channel site="${site}" lang="${lang}" xmltv_id="${xmltv_id}" site_id="${site_id}"${logo}>${displayName}</channel>\r\n`
|
output += ` <channel site="${site}" lang="${lang}" xmltv_id="${xmltv_id}" site_id="${site_id}"${logo}>${displayName}</channel>\r\n`
|
||||||
})
|
})
|
||||||
|
|
||||||
output += '</channels>\r\n'
|
output += '</channels>\r\n'
|
||||||
|
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,124 +1,124 @@
|
|||||||
import { Collection, Dictionary } from '@freearhey/core'
|
import { Collection, Dictionary } from '@freearhey/core'
|
||||||
import { FeedData } from '../types/feed'
|
import { FeedData } from '../types/feed'
|
||||||
import { Logo, Channel } from '.'
|
import { Logo, Channel } from '.'
|
||||||
|
|
||||||
export class Feed {
|
export class Feed {
|
||||||
channelId: string
|
channelId: string
|
||||||
channel?: Channel
|
channel?: Channel
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
isMain: boolean
|
isMain: boolean
|
||||||
broadcastAreaCodes: Collection
|
broadcastAreaCodes: Collection
|
||||||
languageCodes: Collection
|
languageCodes: Collection
|
||||||
timezoneIds: Collection
|
timezoneIds: Collection
|
||||||
videoFormat: string
|
videoFormat: string
|
||||||
guideChannels?: Collection
|
guideChannels?: Collection
|
||||||
streams?: Collection
|
streams?: Collection
|
||||||
logos: Collection = new Collection()
|
logos: Collection = new Collection()
|
||||||
|
|
||||||
constructor(data: FeedData) {
|
constructor(data: FeedData) {
|
||||||
this.channelId = data.channel
|
this.channelId = data.channel
|
||||||
this.id = data.id
|
this.id = data.id
|
||||||
this.name = data.name
|
this.name = data.name
|
||||||
this.isMain = data.is_main
|
this.isMain = data.is_main
|
||||||
this.broadcastAreaCodes = new Collection(data.broadcast_area)
|
this.broadcastAreaCodes = new Collection(data.broadcast_area)
|
||||||
this.languageCodes = new Collection(data.languages)
|
this.languageCodes = new Collection(data.languages)
|
||||||
this.timezoneIds = new Collection(data.timezones)
|
this.timezoneIds = new Collection(data.timezones)
|
||||||
this.videoFormat = data.video_format
|
this.videoFormat = data.video_format
|
||||||
}
|
}
|
||||||
|
|
||||||
withChannel(channelsKeyById: Dictionary): this {
|
withChannel(channelsKeyById: Dictionary): this {
|
||||||
this.channel = channelsKeyById.get(this.channelId)
|
this.channel = channelsKeyById.get(this.channelId)
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
withStreams(streamsGroupedById: Dictionary): this {
|
withStreams(streamsGroupedById: Dictionary): this {
|
||||||
this.streams = new Collection(streamsGroupedById.get(`${this.channelId}@${this.id}`))
|
this.streams = new Collection(streamsGroupedById.get(`${this.channelId}@${this.id}`))
|
||||||
|
|
||||||
if (this.isMain) {
|
if (this.isMain) {
|
||||||
this.streams = this.streams.concat(new Collection(streamsGroupedById.get(this.channelId)))
|
this.streams = this.streams.concat(new Collection(streamsGroupedById.get(this.channelId)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
withGuideChannels(guideChannelsGroupedByStreamId: Dictionary): this {
|
withGuideChannels(guideChannelsGroupedByStreamId: Dictionary): this {
|
||||||
this.guideChannels = new Collection(
|
this.guideChannels = new Collection(
|
||||||
guideChannelsGroupedByStreamId.get(`${this.channelId}@${this.id}`)
|
guideChannelsGroupedByStreamId.get(`${this.channelId}@${this.id}`)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (this.isMain) {
|
if (this.isMain) {
|
||||||
this.guideChannels = this.guideChannels.concat(
|
this.guideChannels = this.guideChannels.concat(
|
||||||
new Collection(guideChannelsGroupedByStreamId.get(this.channelId))
|
new Collection(guideChannelsGroupedByStreamId.get(this.channelId))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
withLogos(logosGroupedByStreamId: Dictionary): this {
|
withLogos(logosGroupedByStreamId: Dictionary): this {
|
||||||
this.logos = new Collection(logosGroupedByStreamId.get(this.getStreamId()))
|
this.logos = new Collection(logosGroupedByStreamId.get(this.getStreamId()))
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
getGuideChannels(): Collection {
|
getGuideChannels(): Collection {
|
||||||
if (!this.guideChannels) return new Collection()
|
if (!this.guideChannels) return new Collection()
|
||||||
|
|
||||||
return this.guideChannels
|
return this.guideChannels
|
||||||
}
|
}
|
||||||
|
|
||||||
getStreams(): Collection {
|
getStreams(): Collection {
|
||||||
if (!this.streams) return new Collection()
|
if (!this.streams) return new Collection()
|
||||||
|
|
||||||
return this.streams
|
return this.streams
|
||||||
}
|
}
|
||||||
|
|
||||||
getFullName(): string {
|
getFullName(): string {
|
||||||
if (!this.channel) return ''
|
if (!this.channel) return ''
|
||||||
|
|
||||||
return `${this.channel.name} ${this.name}`
|
return `${this.channel.name} ${this.name}`
|
||||||
}
|
}
|
||||||
|
|
||||||
getStreamId(): string {
|
getStreamId(): string {
|
||||||
return `${this.channelId}@${this.id}`
|
return `${this.channelId}@${this.id}`
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogos(): Collection {
|
getLogos(): Collection {
|
||||||
function format(logo: Logo): number {
|
function format(logo: Logo): number {
|
||||||
const levelByFormat: { [key: string]: number } = {
|
const levelByFormat: Record<string, number> = {
|
||||||
SVG: 0,
|
SVG: 0,
|
||||||
PNG: 3,
|
PNG: 3,
|
||||||
APNG: 1,
|
APNG: 1,
|
||||||
WebP: 1,
|
WebP: 1,
|
||||||
AVIF: 1,
|
AVIF: 1,
|
||||||
JPEG: 2,
|
JPEG: 2,
|
||||||
GIF: 1
|
GIF: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
return logo.format ? levelByFormat[logo.format] : 0
|
return logo.format ? levelByFormat[logo.format] : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function size(logo: Logo): number {
|
function size(logo: Logo): number {
|
||||||
return Math.abs(512 - logo.width) + Math.abs(512 - logo.height)
|
return Math.abs(512 - logo.width) + Math.abs(512 - logo.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.logos.orderBy([format, size], ['desc', 'asc'], false)
|
return this.logos.orderBy([format, size], ['desc', 'asc'], false)
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogo(): Logo | undefined {
|
getLogo(): Logo | undefined {
|
||||||
return this.getLogos().first()
|
return this.getLogos().first()
|
||||||
}
|
}
|
||||||
|
|
||||||
hasLogo(): boolean {
|
hasLogo(): boolean {
|
||||||
return this.getLogos().notEmpty()
|
return this.getLogos().notEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogoUrl(): string {
|
getLogoUrl(): string {
|
||||||
const logo = this.getLogo()
|
const logo = this.getLogo()
|
||||||
if (!logo) return ''
|
if (!logo) return ''
|
||||||
|
|
||||||
return logo.url || ''
|
return logo.url || ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,35 @@
|
|||||||
import { Collection, DateTime } from '@freearhey/core'
|
import { Collection, DateTime } from '@freearhey/core'
|
||||||
import { generateXMLTV } from 'epg-grabber'
|
import { generateXMLTV } from 'epg-grabber'
|
||||||
|
|
||||||
type GuideData = {
|
interface GuideData {
|
||||||
channels: Collection
|
channels: Collection
|
||||||
programs: Collection
|
programs: Collection
|
||||||
filepath: string
|
filepath: string
|
||||||
gzip: boolean
|
gzip: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Guide {
|
export class Guide {
|
||||||
channels: Collection
|
channels: Collection
|
||||||
programs: Collection
|
programs: Collection
|
||||||
filepath: string
|
filepath: string
|
||||||
gzip: boolean
|
gzip: boolean
|
||||||
|
|
||||||
constructor({ channels, programs, filepath, gzip }: GuideData) {
|
constructor({ channels, programs, filepath, gzip }: GuideData) {
|
||||||
this.channels = channels
|
this.channels = channels
|
||||||
this.programs = programs
|
this.programs = programs
|
||||||
this.filepath = filepath
|
this.filepath = filepath
|
||||||
this.gzip = gzip || false
|
this.gzip = gzip || false
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
const currDate = new DateTime(process.env.CURR_DATE || new Date().toISOString(), {
|
const currDate = new DateTime(process.env.CURR_DATE || new Date().toISOString(), {
|
||||||
timezone: 'UTC'
|
timezone: 'UTC'
|
||||||
})
|
})
|
||||||
|
|
||||||
return generateXMLTV({
|
return generateXMLTV({
|
||||||
channels: this.channels.all(),
|
channels: this.channels.all(),
|
||||||
programs: this.programs.all(),
|
programs: this.programs.all(),
|
||||||
date: currDate.toJSON()
|
date: currDate.toJSON()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +1,59 @@
|
|||||||
import { Dictionary } from '@freearhey/core'
|
import { Dictionary } from '@freearhey/core'
|
||||||
import epgGrabber from 'epg-grabber'
|
import epgGrabber from 'epg-grabber'
|
||||||
import { Feed, Channel } from '.'
|
import { Feed, Channel } from '.'
|
||||||
|
|
||||||
export class GuideChannel {
|
export class GuideChannel {
|
||||||
channelId?: string
|
channelId?: string
|
||||||
channel?: Channel
|
channel?: Channel
|
||||||
feedId?: string
|
feedId?: string
|
||||||
feed?: Feed
|
feed?: Feed
|
||||||
xmltvId?: string
|
xmltvId?: string
|
||||||
languageCode?: string
|
languageCode?: string
|
||||||
siteId?: string
|
siteId?: string
|
||||||
logoUrl?: string
|
logoUrl?: string
|
||||||
siteDomain?: string
|
siteDomain?: string
|
||||||
siteName?: string
|
siteName?: string
|
||||||
|
|
||||||
constructor(data: epgGrabber.Channel) {
|
constructor(data: epgGrabber.Channel) {
|
||||||
const [channelId, feedId] = data.xmltv_id ? data.xmltv_id.split('@') : [undefined, undefined]
|
const [channelId, feedId] = data.xmltv_id ? data.xmltv_id.split('@') : [undefined, undefined]
|
||||||
|
|
||||||
this.channelId = channelId
|
this.channelId = channelId
|
||||||
this.feedId = feedId
|
this.feedId = feedId
|
||||||
this.xmltvId = data.xmltv_id
|
this.xmltvId = data.xmltv_id
|
||||||
this.languageCode = data.lang
|
this.languageCode = data.lang
|
||||||
this.siteId = data.site_id
|
this.siteId = data.site_id
|
||||||
this.logoUrl = data.logo
|
this.logoUrl = data.logo
|
||||||
this.siteDomain = data.site
|
this.siteDomain = data.site
|
||||||
this.siteName = data.name
|
this.siteName = data.name
|
||||||
}
|
}
|
||||||
|
|
||||||
withChannel(channelsKeyById: Dictionary): this {
|
withChannel(channelsKeyById: Dictionary): this {
|
||||||
if (this.channelId) this.channel = channelsKeyById.get(this.channelId)
|
if (this.channelId) this.channel = channelsKeyById.get(this.channelId)
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
withFeed(feedsKeyByStreamId: Dictionary): this {
|
withFeed(feedsKeyByStreamId: Dictionary): this {
|
||||||
if (this.feedId) this.feed = feedsKeyByStreamId.get(this.getStreamId())
|
if (this.feedId) this.feed = feedsKeyByStreamId.get(this.getStreamId())
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
getStreamId(): string {
|
getStreamId(): string {
|
||||||
if (!this.channelId) return ''
|
if (!this.channelId) return ''
|
||||||
if (!this.feedId) return this.channelId
|
if (!this.feedId) return this.channelId
|
||||||
|
|
||||||
return `${this.channelId}@${this.feedId}`
|
return `${this.channelId}@${this.feedId}`
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
channel: this.channelId || null,
|
channel: this.channelId || null,
|
||||||
feed: this.feedId || null,
|
feed: this.feedId || null,
|
||||||
site: this.siteDomain || '',
|
site: this.siteDomain || '',
|
||||||
site_id: this.siteId || '',
|
site_id: this.siteId || '',
|
||||||
site_name: this.siteName || '',
|
site_name: this.siteName || '',
|
||||||
lang: this.languageCode || ''
|
lang: this.languageCode || ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
export * from './channel'
|
export * from './channel'
|
||||||
export * from './feed'
|
export * from './feed'
|
||||||
export * from './guide'
|
export * from './guide'
|
||||||
export * from './guideChannel'
|
export * from './guideChannel'
|
||||||
export * from './issue'
|
export * from './issue'
|
||||||
export * from './logo'
|
export * from './logo'
|
||||||
export * from './site'
|
export * from './site'
|
||||||
export * from './stream'
|
export * from './stream'
|
||||||
export * from './channelList'
|
export * from './channelList'
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
import { Dictionary } from '@freearhey/core'
|
import { Dictionary } from '@freearhey/core'
|
||||||
import { OWNER, REPO } from '../constants'
|
import { OWNER, REPO } from '../constants'
|
||||||
|
|
||||||
type IssueProps = {
|
interface IssueProps {
|
||||||
number: number
|
number: number
|
||||||
labels: string[]
|
labels: string[]
|
||||||
data: Dictionary
|
data: Dictionary
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Issue {
|
export class Issue {
|
||||||
number: number
|
number: number
|
||||||
labels: string[]
|
labels: string[]
|
||||||
data: Dictionary
|
data: Dictionary
|
||||||
|
|
||||||
constructor({ number, labels, data }: IssueProps) {
|
constructor({ number, labels, data }: IssueProps) {
|
||||||
this.number = number
|
this.number = number
|
||||||
this.labels = labels
|
this.labels = labels
|
||||||
this.data = data
|
this.data = data
|
||||||
}
|
}
|
||||||
|
|
||||||
getURL() {
|
getURL() {
|
||||||
return `https://github.com/${OWNER}/${REPO}/issues/${this.number}`
|
return `https://github.com/${OWNER}/${REPO}/issues/${this.number}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,41 @@
|
|||||||
import { Collection, type Dictionary } from '@freearhey/core'
|
import { Collection, type Dictionary } from '@freearhey/core'
|
||||||
import type { LogoData } from '../types/logo'
|
import type { LogoData } from '../types/logo'
|
||||||
import { type Feed } from './feed'
|
import { type Feed } from './feed'
|
||||||
|
|
||||||
export class Logo {
|
export class Logo {
|
||||||
channelId?: string
|
channelId?: string
|
||||||
feedId?: string
|
feedId?: string
|
||||||
feed?: Feed
|
feed?: Feed
|
||||||
tags: Collection = new Collection()
|
tags: Collection = new Collection()
|
||||||
width: number = 0
|
width = 0
|
||||||
height: number = 0
|
height = 0
|
||||||
format?: string
|
format?: string
|
||||||
url?: string
|
url?: string
|
||||||
|
|
||||||
constructor(data?: LogoData) {
|
constructor(data?: LogoData) {
|
||||||
if (!data) return
|
if (!data) return
|
||||||
|
|
||||||
this.channelId = data.channel
|
this.channelId = data.channel
|
||||||
this.feedId = data.feed || undefined
|
this.feedId = data.feed || undefined
|
||||||
this.tags = new Collection(data.tags)
|
this.tags = new Collection(data.tags)
|
||||||
this.width = data.width
|
this.width = data.width
|
||||||
this.height = data.height
|
this.height = data.height
|
||||||
this.format = data.format || undefined
|
this.format = data.format || undefined
|
||||||
this.url = data.url
|
this.url = data.url
|
||||||
}
|
}
|
||||||
|
|
||||||
withFeed(feedsKeyByStreamId: Dictionary): this {
|
withFeed(feedsKeyByStreamId: Dictionary): this {
|
||||||
if (!this.feedId) return this
|
if (!this.feedId) return this
|
||||||
|
|
||||||
this.feed = feedsKeyByStreamId.get(this.getStreamId())
|
this.feed = feedsKeyByStreamId.get(this.getStreamId())
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
getStreamId(): string {
|
getStreamId(): string {
|
||||||
if (!this.channelId) return ''
|
if (!this.channelId) return ''
|
||||||
if (!this.feedId) return this.channelId
|
if (!this.feedId) return this.channelId
|
||||||
|
|
||||||
return `${this.channelId}@${this.feedId}`
|
return `${this.channelId}@${this.feedId}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +1,63 @@
|
|||||||
import { Collection } from '@freearhey/core'
|
import { Collection } from '@freearhey/core'
|
||||||
import { Issue } from './'
|
import { Issue } from './'
|
||||||
|
|
||||||
enum StatusCode {
|
enum StatusCode {
|
||||||
DOWN = 'down',
|
DOWN = 'down',
|
||||||
WARNING = 'warning',
|
WARNING = 'warning',
|
||||||
OK = 'ok'
|
OK = 'ok'
|
||||||
}
|
}
|
||||||
|
|
||||||
type Status = {
|
interface Status {
|
||||||
code: StatusCode
|
code: StatusCode
|
||||||
emoji: string
|
emoji: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type SiteProps = {
|
interface SiteProps {
|
||||||
domain: string
|
domain: string
|
||||||
totalChannels?: number
|
totalChannels?: number
|
||||||
markedChannels?: number
|
markedChannels?: number
|
||||||
issues: Collection
|
issues: Collection
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Site {
|
export class Site {
|
||||||
domain: string
|
domain: string
|
||||||
totalChannels: number
|
totalChannels: number
|
||||||
markedChannels: number
|
markedChannels: number
|
||||||
issues: Collection
|
issues: Collection
|
||||||
|
|
||||||
constructor({ domain, totalChannels = 0, markedChannels = 0, issues }: SiteProps) {
|
constructor({ domain, totalChannels = 0, markedChannels = 0, issues }: SiteProps) {
|
||||||
this.domain = domain
|
this.domain = domain
|
||||||
this.totalChannels = totalChannels
|
this.totalChannels = totalChannels
|
||||||
this.markedChannels = markedChannels
|
this.markedChannels = markedChannels
|
||||||
this.issues = issues
|
this.issues = issues
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatus(): Status {
|
getStatus(): Status {
|
||||||
const issuesWithStatusDown = this.issues.filter((issue: Issue) =>
|
const issuesWithStatusDown = this.issues.filter((issue: Issue) =>
|
||||||
issue.labels.find(label => label === 'status:down')
|
issue.labels.find(label => label === 'status:down')
|
||||||
)
|
)
|
||||||
if (issuesWithStatusDown.notEmpty())
|
if (issuesWithStatusDown.notEmpty())
|
||||||
return {
|
return {
|
||||||
code: StatusCode.DOWN,
|
code: StatusCode.DOWN,
|
||||||
emoji: '🔴'
|
emoji: '🔴'
|
||||||
}
|
}
|
||||||
|
|
||||||
const issuesWithStatusWarning = this.issues.filter((issue: Issue) =>
|
const issuesWithStatusWarning = this.issues.filter((issue: Issue) =>
|
||||||
issue.labels.find(label => label === 'status:warning')
|
issue.labels.find(label => label === 'status:warning')
|
||||||
)
|
)
|
||||||
if (issuesWithStatusWarning.notEmpty())
|
if (issuesWithStatusWarning.notEmpty())
|
||||||
return {
|
return {
|
||||||
code: StatusCode.WARNING,
|
code: StatusCode.WARNING,
|
||||||
emoji: '🟡'
|
emoji: '🟡'
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code: StatusCode.OK,
|
code: StatusCode.OK,
|
||||||
emoji: '🟢'
|
emoji: '🟢'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getIssues(): Collection {
|
getIssues(): Collection {
|
||||||
return this.issues.map((issue: Issue) => issue.getURL())
|
return this.issues.map((issue: Issue) => issue.getURL())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +1,58 @@
|
|||||||
import type { StreamData } from '../types/stream'
|
import type { StreamData } from '../types/stream'
|
||||||
import { Feed, Channel } from './index'
|
import { Feed, Channel } from './index'
|
||||||
|
|
||||||
export class Stream {
|
export class Stream {
|
||||||
name?: string
|
name?: string
|
||||||
url: string
|
url: string
|
||||||
id?: string
|
id?: string
|
||||||
channelId?: string
|
channelId?: string
|
||||||
channel?: Channel
|
channel?: Channel
|
||||||
feedId?: string
|
feedId?: string
|
||||||
feed?: Feed
|
feed?: Feed
|
||||||
filepath?: string
|
filepath?: string
|
||||||
line?: number
|
line?: number
|
||||||
label?: string
|
label?: string
|
||||||
verticalResolution?: number
|
verticalResolution?: number
|
||||||
isInterlaced?: boolean
|
isInterlaced?: boolean
|
||||||
referrer?: string
|
referrer?: string
|
||||||
userAgent?: string
|
userAgent?: string
|
||||||
groupTitle: string = 'Undefined'
|
groupTitle = 'Undefined'
|
||||||
removed: boolean = false
|
removed = false
|
||||||
|
|
||||||
constructor(data: StreamData) {
|
constructor(data: StreamData) {
|
||||||
const id = data.channel && data.feed ? [data.channel, data.feed].join('@') : data.channel
|
const id = data.channel && data.feed ? [data.channel, data.feed].join('@') : data.channel
|
||||||
const { verticalResolution, isInterlaced } = parseQuality(data.quality)
|
const { verticalResolution, isInterlaced } = parseQuality(data.quality)
|
||||||
|
|
||||||
this.id = id || undefined
|
this.id = id || undefined
|
||||||
this.channelId = data.channel || undefined
|
this.channelId = data.channel || undefined
|
||||||
this.feedId = data.feed || undefined
|
this.feedId = data.feed || undefined
|
||||||
this.name = data.name || undefined
|
this.name = data.name || undefined
|
||||||
this.url = data.url
|
this.url = data.url
|
||||||
this.referrer = data.referrer || undefined
|
this.referrer = data.referrer || undefined
|
||||||
this.userAgent = data.user_agent || undefined
|
this.userAgent = data.user_agent || undefined
|
||||||
this.verticalResolution = verticalResolution || undefined
|
this.verticalResolution = verticalResolution || undefined
|
||||||
this.isInterlaced = isInterlaced || undefined
|
this.isInterlaced = isInterlaced || undefined
|
||||||
this.label = data.label || undefined
|
this.label = data.label || undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
getId(): string {
|
getId(): string {
|
||||||
return this.id || ''
|
return this.id || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
getName(): string {
|
getName(): string {
|
||||||
return this.name || ''
|
return this.name || ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseQuality(quality: string | null): {
|
function parseQuality(quality: string | null): {
|
||||||
verticalResolution: number | null
|
verticalResolution: number | null
|
||||||
isInterlaced: boolean | null
|
isInterlaced: boolean | null
|
||||||
} {
|
} {
|
||||||
if (!quality) return { verticalResolution: null, isInterlaced: null }
|
if (!quality) return { verticalResolution: null, isInterlaced: null }
|
||||||
const [, verticalResolutionString] = quality.match(/^(\d+)/) || [null, undefined]
|
const [, verticalResolutionString] = quality.match(/^(\d+)/) || [null, undefined]
|
||||||
const isInterlaced = /i$/i.test(quality)
|
const isInterlaced = /i$/i.test(quality)
|
||||||
let verticalResolution = 0
|
let verticalResolution = 0
|
||||||
if (verticalResolutionString) verticalResolution = parseInt(verticalResolutionString)
|
if (verticalResolutionString) verticalResolution = parseInt(verticalResolutionString)
|
||||||
|
|
||||||
return { verticalResolution, isInterlaced }
|
return { verticalResolution, isInterlaced }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
site: '<DOMAIN>',
|
site: '<DOMAIN>',
|
||||||
url({ channel, date }) {
|
url({ channel, date }) {
|
||||||
return `https://example.com/api/${channel.site_id}/${date.format('YYYY-MM-DD')}`
|
return `https://example.com/api/${channel.site_id}/${date.format('YYYY-MM-DD')}`
|
||||||
},
|
},
|
||||||
parser({ content }) {
|
parser({ content }) {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(content)
|
return JSON.parse(content)
|
||||||
} catch {
|
} catch {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
channels() {
|
channels() {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,38 @@
|
|||||||
const { parser, url } = require('./<DOMAIN>.config.js')
|
const { parser, url } = require('./<DOMAIN>.config.js')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require('dayjs/plugin/utc')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
|
|
||||||
const date = dayjs.utc('2025-01-12', 'YYYY-MM-DD').startOf('d')
|
const date = dayjs.utc('2025-01-12', 'YYYY-MM-DD').startOf('d')
|
||||||
const channel = { site_id: 'bbc1' }
|
const channel = { site_id: 'bbc1' }
|
||||||
|
|
||||||
it('can generate valid url', () => {
|
it('can generate valid url', () => {
|
||||||
expect(url({ channel, date })).toBe('https://example.com/api/bbc1/2025-01-12')
|
expect(url({ channel, date })).toBe('https://example.com/api/bbc1/2025-01-12')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can parse response', () => {
|
it('can parse response', () => {
|
||||||
const content =
|
const content =
|
||||||
'[{"title":"Program 1","start":"2025-01-12T00:00:00.000Z","stop":"2025-01-12T00:30:00.000Z"},{"title":"Program 2","start":"2025-01-12T00:30:00.000Z","stop":"2025-01-12T01:00:00.000Z"}]'
|
'[{"title":"Program 1","start":"2025-01-12T00:00:00.000Z","stop":"2025-01-12T00:30:00.000Z"},{"title":"Program 2","start":"2025-01-12T00:30:00.000Z","stop":"2025-01-12T01:00:00.000Z"}]'
|
||||||
|
|
||||||
const results = parser({ content })
|
const results = parser({ content })
|
||||||
|
|
||||||
expect(results.length).toBe(2)
|
expect(results.length).toBe(2)
|
||||||
expect(results[0]).toMatchObject({
|
expect(results[0]).toMatchObject({
|
||||||
title: 'Program 1',
|
title: 'Program 1',
|
||||||
start: '2025-01-12T00:00:00.000Z',
|
start: '2025-01-12T00:00:00.000Z',
|
||||||
stop: '2025-01-12T00:30:00.000Z'
|
stop: '2025-01-12T00:30:00.000Z'
|
||||||
})
|
})
|
||||||
expect(results[1]).toMatchObject({
|
expect(results[1]).toMatchObject({
|
||||||
title: 'Program 2',
|
title: 'Program 2',
|
||||||
start: '2025-01-12T00:30:00.000Z',
|
start: '2025-01-12T00:30:00.000Z',
|
||||||
stop: '2025-01-12T01:00:00.000Z'
|
stop: '2025-01-12T01:00:00.000Z'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can handle empty guide', () => {
|
it('can handle empty guide', () => {
|
||||||
const results = parser({ content: '' })
|
const results = parser({ content: '' })
|
||||||
|
|
||||||
expect(results).toMatchObject([])
|
expect(results).toMatchObject([])
|
||||||
})
|
})
|
||||||
|
|||||||
54
scripts/types/channel.d.ts
vendored
54
scripts/types/channel.d.ts
vendored
@@ -1,27 +1,27 @@
|
|||||||
import { Collection } from '@freearhey/core'
|
import { Collection } from '@freearhey/core'
|
||||||
|
|
||||||
export type ChannelData = {
|
export interface ChannelData {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
alt_names: string[]
|
alt_names: string[]
|
||||||
network: string
|
network: string
|
||||||
owners: Collection
|
owners: Collection
|
||||||
country: string
|
country: string
|
||||||
subdivision: string
|
subdivision: string
|
||||||
city: string
|
city: string
|
||||||
categories: Collection
|
categories: Collection
|
||||||
is_nsfw: boolean
|
is_nsfw: boolean
|
||||||
launched: string
|
launched: string
|
||||||
closed: string
|
closed: string
|
||||||
replaced_by: string
|
replaced_by: string
|
||||||
website: string
|
website: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ChannelSearchableData = {
|
export interface ChannelSearchableData {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
altNames: string[]
|
altNames: string[]
|
||||||
guideNames: string[]
|
guideNames: string[]
|
||||||
streamNames: string[]
|
streamNames: string[]
|
||||||
feedFullNames: string[]
|
feedFullNames: string[]
|
||||||
}
|
}
|
||||||
|
|||||||
40
scripts/types/dataLoader.d.ts
vendored
40
scripts/types/dataLoader.d.ts
vendored
@@ -1,20 +1,20 @@
|
|||||||
import { Storage } from '@freearhey/core'
|
import { Storage } from '@freearhey/core'
|
||||||
|
|
||||||
export type DataLoaderProps = {
|
export interface DataLoaderProps {
|
||||||
storage: Storage
|
storage: Storage
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DataLoaderData = {
|
export interface DataLoaderData {
|
||||||
countries: object | object[]
|
countries: object | object[]
|
||||||
regions: object | object[]
|
regions: object | object[]
|
||||||
subdivisions: object | object[]
|
subdivisions: object | object[]
|
||||||
languages: object | object[]
|
languages: object | object[]
|
||||||
categories: object | object[]
|
categories: object | object[]
|
||||||
blocklist: object | object[]
|
blocklist: object | object[]
|
||||||
channels: object | object[]
|
channels: object | object[]
|
||||||
feeds: object | object[]
|
feeds: object | object[]
|
||||||
timezones: object | object[]
|
timezones: object | object[]
|
||||||
guides: object | object[]
|
guides: object | object[]
|
||||||
streams: object | object[]
|
streams: object | object[]
|
||||||
logos: object | object[]
|
logos: object | object[]
|
||||||
}
|
}
|
||||||
|
|||||||
32
scripts/types/dataProcessor.d.ts
vendored
32
scripts/types/dataProcessor.d.ts
vendored
@@ -1,16 +1,16 @@
|
|||||||
import { Collection, Dictionary } from '@freearhey/core'
|
import { Collection, Dictionary } from '@freearhey/core'
|
||||||
|
|
||||||
export type DataProcessorData = {
|
export interface DataProcessorData {
|
||||||
guideChannelsGroupedByStreamId: Dictionary
|
guideChannelsGroupedByStreamId: Dictionary
|
||||||
feedsGroupedByChannelId: Dictionary
|
feedsGroupedByChannelId: Dictionary
|
||||||
logosGroupedByChannelId: Dictionary
|
logosGroupedByChannelId: Dictionary
|
||||||
logosGroupedByStreamId: Dictionary
|
logosGroupedByStreamId: Dictionary
|
||||||
feedsKeyByStreamId: Dictionary
|
feedsKeyByStreamId: Dictionary
|
||||||
streamsGroupedById: Dictionary
|
streamsGroupedById: Dictionary
|
||||||
channelsKeyById: Dictionary
|
channelsKeyById: Dictionary
|
||||||
guideChannels: Collection
|
guideChannels: Collection
|
||||||
channels: Collection
|
channels: Collection
|
||||||
streams: Collection
|
streams: Collection
|
||||||
feeds: Collection
|
feeds: Collection
|
||||||
logos: Collection
|
logos: Collection
|
||||||
}
|
}
|
||||||
|
|||||||
24
scripts/types/feed.d.ts
vendored
24
scripts/types/feed.d.ts
vendored
@@ -1,12 +1,12 @@
|
|||||||
import { Collection } from '@freearhey/core'
|
import { Collection } from '@freearhey/core'
|
||||||
|
|
||||||
export type FeedData = {
|
export interface FeedData {
|
||||||
channel: string
|
channel: string
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
is_main: boolean
|
is_main: boolean
|
||||||
broadcast_area: Collection
|
broadcast_area: Collection
|
||||||
languages: Collection
|
languages: Collection
|
||||||
timezones: Collection
|
timezones: Collection
|
||||||
video_format: string
|
video_format: string
|
||||||
}
|
}
|
||||||
|
|||||||
16
scripts/types/guide.d.ts
vendored
16
scripts/types/guide.d.ts
vendored
@@ -1,8 +1,8 @@
|
|||||||
export type GuideData = {
|
export interface GuideData {
|
||||||
channel: string
|
channel: string
|
||||||
feed: string
|
feed: string
|
||||||
site: string
|
site: string
|
||||||
site_id: string
|
site_id: string
|
||||||
site_name: string
|
site_name: string
|
||||||
lang: string
|
lang: string
|
||||||
}
|
}
|
||||||
|
|||||||
2
scripts/types/langs.d.ts
vendored
2
scripts/types/langs.d.ts
vendored
@@ -1 +1 @@
|
|||||||
declare module 'langs'
|
declare module 'langs'
|
||||||
|
|||||||
18
scripts/types/logo.d.ts
vendored
18
scripts/types/logo.d.ts
vendored
@@ -1,9 +1,9 @@
|
|||||||
export type LogoData = {
|
export interface LogoData {
|
||||||
channel: string
|
channel: string
|
||||||
feed: string | null
|
feed: string | null
|
||||||
tags: string[]
|
tags: string[]
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
format: string | null
|
format: string | null
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
|||||||
20
scripts/types/stream.d.ts
vendored
20
scripts/types/stream.d.ts
vendored
@@ -1,10 +1,10 @@
|
|||||||
export type StreamData = {
|
export interface StreamData {
|
||||||
channel: string | null
|
channel: string | null
|
||||||
feed: string | null
|
feed: string | null
|
||||||
name?: string
|
name?: string
|
||||||
url: string
|
url: string
|
||||||
referrer: string | null
|
referrer: string | null
|
||||||
user_agent: string | null
|
user_agent: string | null
|
||||||
quality: string | null
|
quality: string | null
|
||||||
label: string | null
|
label: string | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +1,69 @@
|
|||||||
const cheerio = require('cheerio')
|
const cheerio = require('cheerio')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require('dayjs/plugin/utc')
|
||||||
const timezone = require('dayjs/plugin/timezone')
|
const timezone = require('dayjs/plugin/timezone')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
|
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
site: '9tv.co.il',
|
site: '9tv.co.il',
|
||||||
days: 2,
|
days: 2,
|
||||||
url: function ({ date }) {
|
url: function ({ date }) {
|
||||||
return `https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=${date.format(
|
return `https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=${date.format(
|
||||||
'DD/MM/YYYY 00:00:00'
|
'DD/MM/YYYY 00:00:00'
|
||||||
)}`
|
)}`
|
||||||
},
|
},
|
||||||
parser: function ({ content, date }) {
|
parser: function ({ content, date }) {
|
||||||
const programs = []
|
const programs = []
|
||||||
const items = parseItems(content)
|
const items = parseItems(content)
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
const prev = programs[programs.length - 1]
|
const prev = programs[programs.length - 1]
|
||||||
const $item = cheerio.load(item)
|
const $item = cheerio.load(item)
|
||||||
const start = parseStart($item, date)
|
const start = parseStart($item, date)
|
||||||
if (prev) prev.stop = start
|
if (prev) prev.stop = start
|
||||||
const stop = start.add(1, 'h')
|
const stop = start.add(1, 'h')
|
||||||
programs.push({
|
programs.push({
|
||||||
title: parseTitle($item),
|
title: parseTitle($item),
|
||||||
image: parseImage($item),
|
image: parseImage($item),
|
||||||
description: parseDescription($item),
|
description: parseDescription($item),
|
||||||
start,
|
start,
|
||||||
stop
|
stop
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return programs
|
return programs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseStart($item, date) {
|
function parseStart($item, date) {
|
||||||
let time = $item('a > div.guide_list_time').text().trim()
|
let time = $item('a > div.guide_list_time').text().trim()
|
||||||
|
|
||||||
return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Jerusalem')
|
return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Jerusalem')
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseImage($item) {
|
function parseImage($item) {
|
||||||
const backgroundImage = $item('a > div.guide_info_group > div.guide_info_pict').css(
|
const backgroundImage = $item('a > div.guide_info_group > div.guide_info_pict').css(
|
||||||
'background-image'
|
'background-image'
|
||||||
)
|
)
|
||||||
if (!backgroundImage) return null
|
if (!backgroundImage) return null
|
||||||
const [, relativePath] = backgroundImage.match(/url\((.*)\)/) || [null, null]
|
const [, relativePath] = backgroundImage.match(/url\((.*)\)/) || [null, null]
|
||||||
|
|
||||||
return relativePath ? `https://www.9tv.co.il${relativePath}` : null
|
return relativePath ? `https://www.9tv.co.il${relativePath}` : null
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDescription($item) {
|
function parseDescription($item) {
|
||||||
return $item('a > div.guide_info_group > div.guide_txt_group > div').text().trim()
|
return $item('a > div.guide_info_group > div.guide_txt_group > div').text().trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTitle($item) {
|
function parseTitle($item) {
|
||||||
return $item('a > div.guide_info_group > div.guide_txt_group > h3').text().trim()
|
return $item('a > div.guide_info_group > div.guide_txt_group > h3').text().trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseItems(content) {
|
function parseItems(content) {
|
||||||
const $ = cheerio.load(content)
|
const $ = cheerio.load(content)
|
||||||
|
|
||||||
return $('li').toArray()
|
return $('li').toArray()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +1,56 @@
|
|||||||
const { parser, url } = require('./9tv.co.il.config.js')
|
const { parser, url } = require('./9tv.co.il.config.js')
|
||||||
const dayjs = require('dayjs')
|
const fs = require('fs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const path = require('path')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const dayjs = require('dayjs')
|
||||||
dayjs.extend(customParseFormat)
|
const utc = require('dayjs/plugin/utc')
|
||||||
dayjs.extend(utc)
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
|
dayjs.extend(customParseFormat)
|
||||||
const date = dayjs.utc('2022-03-06', 'YYYY-MM-DD').startOf('d')
|
dayjs.extend(utc)
|
||||||
const channel = {
|
|
||||||
site_id: '#',
|
const date = dayjs.utc('2022-03-06', 'YYYY-MM-DD').startOf('d')
|
||||||
xmltv_id: 'Channel9.il'
|
const channel = {
|
||||||
}
|
site_id: '#',
|
||||||
|
xmltv_id: 'Channel9.il'
|
||||||
it('can generate valid url', () => {
|
}
|
||||||
expect(url({ date })).toBe(
|
|
||||||
'https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=06/03/2022 00:00:00'
|
it('can generate valid url', () => {
|
||||||
)
|
expect(url({ date })).toBe(
|
||||||
})
|
'https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=06/03/2022 00:00:00'
|
||||||
|
)
|
||||||
it('can parse response', () => {
|
})
|
||||||
const content =
|
|
||||||
'<li> <a href="#" class="guide_list_link w-inline-block"> <div class="guide_list_time">06:30</div><div class="guide_info_group"> <div class="guide_info_pict" style="background-image: url(/download/pictures/img_id=8484.jpg);"></div><div class="guide_txt_group"> <h3 class="guide_info_title">Слепая</h3> <div>Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы. </div></div></div></a></li><li> <a href="#" class="guide_list_link even w-inline-block"> <div class="guide_list_time">09:10</div><div class="guide_info_group"> <div class="guide_info_pict" style="background-image: url(/download/pictures/img_id=23694.jpg);"></div><div class="guide_txt_group"> <h3 class="guide_info_title">Орел и решка. Морской сезон</h3> <div>Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.</div></div></div></a></li>'
|
it('can parse response', () => {
|
||||||
const result = parser({ content, date }).map(p => {
|
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
|
||||||
p.start = p.start.toJSON()
|
const result = parser({ content, date }).map(p => {
|
||||||
p.stop = p.stop.toJSON()
|
p.start = p.start.toJSON()
|
||||||
return p
|
p.stop = p.stop.toJSON()
|
||||||
})
|
return p
|
||||||
|
})
|
||||||
expect(result).toMatchObject([
|
|
||||||
{
|
expect(result).toMatchObject([
|
||||||
start: '2022-03-06T04:30:00.000Z',
|
{
|
||||||
stop: '2022-03-06T07:10:00.000Z',
|
start: '2022-03-06T04:30:00.000Z',
|
||||||
title: 'Слепая',
|
stop: '2022-03-06T07:10:00.000Z',
|
||||||
image: 'https://www.9tv.co.il/download/pictures/img_id=8484.jpg',
|
title: 'Слепая',
|
||||||
description:
|
image: 'https://www.9tv.co.il/download/pictures/img_id=8484.jpg',
|
||||||
'Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы.'
|
description:
|
||||||
},
|
'Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы.'
|
||||||
{
|
},
|
||||||
start: '2022-03-06T07:10:00.000Z',
|
{
|
||||||
stop: '2022-03-06T08:10:00.000Z',
|
start: '2022-03-06T07:10:00.000Z',
|
||||||
image: 'https://www.9tv.co.il/download/pictures/img_id=23694.jpg',
|
stop: '2022-03-06T08:10:00.000Z',
|
||||||
title: 'Орел и решка. Морской сезон',
|
image: 'https://www.9tv.co.il/download/pictures/img_id=23694.jpg',
|
||||||
description: 'Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.'
|
title: 'Орел и решка. Морской сезон',
|
||||||
}
|
description: 'Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.'
|
||||||
])
|
}
|
||||||
})
|
])
|
||||||
|
})
|
||||||
it('can handle empty guide', () => {
|
|
||||||
const result = parser({
|
it('can handle empty guide', () => {
|
||||||
date,
|
const result = parser({
|
||||||
channel,
|
date,
|
||||||
content: '<!DOCTYPE html><html><head></head><body></body></html>'
|
channel,
|
||||||
})
|
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
|
||||||
expect(result).toMatchObject([])
|
})
|
||||||
})
|
expect(result).toMatchObject([])
|
||||||
|
})
|
||||||
|
|||||||
1
sites/9tv.co.il/__data__/content.html
Normal file
1
sites/9tv.co.il/__data__/content.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<li> <a href="#" class="guide_list_link w-inline-block"> <div class="guide_list_time">06:30</div><div class="guide_info_group"> <div class="guide_info_pict" style="background-image: url(/download/pictures/img_id=8484.jpg);"></div><div class="guide_txt_group"> <h3 class="guide_info_title">Слепая</h3> <div>Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы. </div></div></div></a></li><li> <a href="#" class="guide_list_link even w-inline-block"> <div class="guide_list_time">09:10</div><div class="guide_info_group"> <div class="guide_info_pict" style="background-image: url(/download/pictures/img_id=23694.jpg);"></div><div class="guide_txt_group"> <h3 class="guide_info_title">Орел и решка. Морской сезон</h3> <div>Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.</div></div></div></a></li>
|
||||||
1
sites/9tv.co.il/__data__/no_content.html
Normal file
1
sites/9tv.co.il/__data__/no_content.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!DOCTYPE html><html><head></head><body></body></html>
|
||||||
@@ -1,122 +1,122 @@
|
|||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require('dayjs/plugin/utc')
|
||||||
const timezone = require('dayjs/plugin/timezone')
|
const timezone = require('dayjs/plugin/timezone')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
|
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
site: 'abc.net.au',
|
site: 'abc.net.au',
|
||||||
days: 3,
|
days: 3,
|
||||||
request: {
|
request: {
|
||||||
cache: {
|
cache: {
|
||||||
ttl: 60 * 60 * 1000 // 1 hour
|
ttl: 60 * 60 * 1000 // 1 hour
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
url({ date, channel }) {
|
url({ date, channel }) {
|
||||||
const [region] = channel.site_id.split('#')
|
const [region] = channel.site_id.split('#')
|
||||||
|
|
||||||
return `https://cdn.iview.abc.net.au/epg/processed/${region}_${date.format('YYYY-MM-DD')}.json`
|
return `https://cdn.iview.abc.net.au/epg/processed/${region}_${date.format('YYYY-MM-DD')}.json`
|
||||||
},
|
},
|
||||||
parser({ content, channel }) {
|
parser({ content, channel }) {
|
||||||
let programs = []
|
let programs = []
|
||||||
const items = parseItems(content, channel)
|
const items = parseItems(content, channel)
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
programs.push({
|
programs.push({
|
||||||
title: item.title,
|
title: item.title,
|
||||||
sub_title: item.episode_title,
|
sub_title: item.episode_title,
|
||||||
category: item.genres,
|
category: item.genres,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
season: parseSeason(item),
|
season: parseSeason(item),
|
||||||
episode: parseEpisode(item),
|
episode: parseEpisode(item),
|
||||||
rating: parseRating(item),
|
rating: parseRating(item),
|
||||||
image: parseImage(item),
|
image: parseImage(item),
|
||||||
start: parseTime(item.start_time),
|
start: parseTime(item.start_time),
|
||||||
stop: parseTime(item.end_time)
|
stop: parseTime(item.end_time)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return programs
|
return programs
|
||||||
},
|
},
|
||||||
async channels({ region = 'syd' }) {
|
async channels({ region = 'syd' }) {
|
||||||
const now = dayjs()
|
const now = dayjs()
|
||||||
const regions = {
|
const regions = {
|
||||||
syd: 'Sydney',
|
syd: 'Sydney',
|
||||||
mel: 'Melbourne',
|
mel: 'Melbourne',
|
||||||
bri: 'Brisbane',
|
bri: 'Brisbane',
|
||||||
gc: 'GoldCoast',
|
gc: 'GoldCoast',
|
||||||
per: 'Perth',
|
per: 'Perth',
|
||||||
adl: 'Adelaide',
|
adl: 'Adelaide',
|
||||||
hbr: 'Hobart',
|
hbr: 'Hobart',
|
||||||
drw: 'Darwin',
|
drw: 'Darwin',
|
||||||
cbr: 'Canberra',
|
cbr: 'Canberra',
|
||||||
nsw: 'New South Wales',
|
nsw: 'New South Wales',
|
||||||
vic: 'Victoria',
|
vic: 'Victoria',
|
||||||
tsv: 'Townsville',
|
tsv: 'Townsville',
|
||||||
qld: 'Queensland',
|
qld: 'Queensland',
|
||||||
wa: 'Western Australia',
|
wa: 'Western Australia',
|
||||||
sa: 'South Australia',
|
sa: 'South Australia',
|
||||||
tas: 'Tasmania',
|
tas: 'Tasmania',
|
||||||
nt: 'Northern Territory'
|
nt: 'Northern Territory'
|
||||||
}
|
}
|
||||||
|
|
||||||
let channels = []
|
let channels = []
|
||||||
const regionName = regions[region]
|
const regionName = regions[region]
|
||||||
const data = await axios
|
const data = await axios
|
||||||
.get(
|
.get(
|
||||||
`https://cdn.iview.abc.net.au/epg/processed/${regionName}_${now.format('YYYY-MM-DD')}.json`
|
`https://cdn.iview.abc.net.au/epg/processed/${regionName}_${now.format('YYYY-MM-DD')}.json`
|
||||||
)
|
)
|
||||||
.then(r => r.data)
|
.then(r => r.data)
|
||||||
.catch(console.log)
|
.catch(console.log)
|
||||||
|
|
||||||
for (let item of data.schedule) {
|
for (let item of data.schedule) {
|
||||||
channels.push({
|
channels.push({
|
||||||
lang: 'en',
|
lang: 'en',
|
||||||
site_id: `${regionName}#${item.channel}`,
|
site_id: `${regionName}#${item.channel}`,
|
||||||
name: item.channel
|
name: item.channel
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return channels
|
return channels
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseItems(content, channel) {
|
function parseItems(content, channel) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(content)
|
const data = JSON.parse(content)
|
||||||
if (!data) return []
|
if (!data) return []
|
||||||
if (!Array.isArray(data.schedule)) return []
|
if (!Array.isArray(data.schedule)) return []
|
||||||
|
|
||||||
const [, channelId] = channel.site_id.split('#')
|
const [, channelId] = channel.site_id.split('#')
|
||||||
const channelData = data.schedule.find(i => i.channel == channelId)
|
const channelData = data.schedule.find(i => i.channel == channelId)
|
||||||
return channelData.listing && Array.isArray(channelData.listing) ? channelData.listing : []
|
return channelData.listing && Array.isArray(channelData.listing) ? channelData.listing : []
|
||||||
} catch {
|
} catch {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseSeason(item) {
|
function parseSeason(item) {
|
||||||
return item.series_num || null
|
return item.series_num || null
|
||||||
}
|
}
|
||||||
function parseEpisode(item) {
|
function parseEpisode(item) {
|
||||||
return item.episode_num || null
|
return item.episode_num || null
|
||||||
}
|
}
|
||||||
function parseTime(time) {
|
function parseTime(time) {
|
||||||
return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'Australia/Sydney')
|
return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'Australia/Sydney')
|
||||||
}
|
}
|
||||||
function parseImage(item) {
|
function parseImage(item) {
|
||||||
return item.image_file
|
return item.image_file
|
||||||
? `https://www.abc.net.au/tv/common/images/publicity/${item.image_file}`
|
? `https://www.abc.net.au/tv/common/images/publicity/${item.image_file}`
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
function parseRating(item) {
|
function parseRating(item) {
|
||||||
return item.rating
|
return item.rating
|
||||||
? {
|
? {
|
||||||
system: 'ACB',
|
system: 'ACB',
|
||||||
value: item.rating
|
value: item.rating
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,51 @@
|
|||||||
const { parser, url } = require('./abc.net.au.config.js')
|
const { parser, url } = require('./abc.net.au.config.js')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require('dayjs/plugin/utc')
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
|
|
||||||
const date = dayjs.utc('2025-02-04', 'YYYY-MM-DD').startOf('d')
|
const date = dayjs.utc('2025-02-04', 'YYYY-MM-DD').startOf('d')
|
||||||
const channel = { site_id: 'Sydney#ABC1' }
|
const channel = { site_id: 'Sydney#ABC1' }
|
||||||
|
|
||||||
it('can generate valid url', () => {
|
it('can generate valid url', () => {
|
||||||
expect(url({ date, channel })).toBe(
|
expect(url({ date, channel })).toBe(
|
||||||
'https://cdn.iview.abc.net.au/epg/processed/Sydney_2025-02-04.json'
|
'https://cdn.iview.abc.net.au/epg/processed/Sydney_2025-02-04.json'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can parse response', () => {
|
it('can parse response', () => {
|
||||||
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
|
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
|
||||||
const results = parser({ content, channel }).map(p => {
|
const results = parser({ content, channel }).map(p => {
|
||||||
p.start = p.start.toJSON()
|
p.start = p.start.toJSON()
|
||||||
p.stop = p.stop.toJSON()
|
p.stop = p.stop.toJSON()
|
||||||
return p
|
return p
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(results.length).toBe(30)
|
expect(results.length).toBe(30)
|
||||||
expect(results[0]).toMatchObject({
|
expect(results[0]).toMatchObject({
|
||||||
title: "Julia Zemiro's Home Delivery",
|
title: "Julia Zemiro's Home Delivery",
|
||||||
sub_title: 'Maggie Beer',
|
sub_title: 'Maggie Beer',
|
||||||
description:
|
description:
|
||||||
"The kitchen Maggie Beer made famous in The Cook and the Chef may be in the heart of the Barossa Valley, but our most beloved foodie meets up with Julia where she grew up in Sydney's Lakemba.",
|
"The kitchen Maggie Beer made famous in The Cook and the Chef may be in the heart of the Barossa Valley, but our most beloved foodie meets up with Julia where she grew up in Sydney's Lakemba.",
|
||||||
category: ['Entertainment', 'Factual'],
|
category: ['Entertainment', 'Factual'],
|
||||||
rating: {
|
rating: {
|
||||||
system: 'ACB',
|
system: 'ACB',
|
||||||
value: 'G'
|
value: 'G'
|
||||||
},
|
},
|
||||||
season: null,
|
season: null,
|
||||||
episode: null,
|
episode: null,
|
||||||
image: 'https://www.abc.net.au/tv/common/images/publicity/LE1761H002S00_460.jpg',
|
image: 'https://www.abc.net.au/tv/common/images/publicity/LE1761H002S00_460.jpg',
|
||||||
start: '2025-02-03T12:40:00.000Z',
|
start: '2025-02-03T12:40:00.000Z',
|
||||||
stop: '2025-02-03T13:09:00.000Z'
|
stop: '2025-02-03T13:09:00.000Z'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can handle empty guide', () => {
|
it('can handle empty guide', () => {
|
||||||
const results = parser({
|
const results = parser({
|
||||||
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')),
|
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')),
|
||||||
channel
|
channel
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(results).toMatchObject([])
|
expect(results).toMatchObject([])
|
||||||
})
|
})
|
||||||
|
|||||||
1
sites/allente.dk/__data__/content.json
Normal file
1
sites/allente.dk/__data__/content.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}
|
||||||
1
sites/allente.dk/__data__/no_content.json
Normal file
1
sites/allente.dk/__data__/no_content.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"date":"2001-11-17","categories":[],"channels":[]}
|
||||||
@@ -1,65 +1,65 @@
|
|||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
site: 'allente.dk',
|
site: 'allente.dk',
|
||||||
days: 2,
|
days: 2,
|
||||||
request: {
|
request: {
|
||||||
cache: {
|
cache: {
|
||||||
ttl: 60 * 60 * 1000 // 1 hour
|
ttl: 60 * 60 * 1000 // 1 hour
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
url({ date }) {
|
url({ date }) {
|
||||||
return `https://cs-vcb.allente.dk/epg/events?date=${date.format('YYYY-MM-DD')}`
|
return `https://cs-vcb.allente.dk/epg/events?date=${date.format('YYYY-MM-DD')}`
|
||||||
},
|
},
|
||||||
parser({ content, channel }) {
|
parser({ content, channel }) {
|
||||||
let programs = []
|
let programs = []
|
||||||
const items = parseItems(content, channel)
|
const items = parseItems(content, channel)
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
if (!item.details) return
|
if (!item.details) return
|
||||||
const start = dayjs(item.time)
|
const start = dayjs(item.time)
|
||||||
const stop = start.add(item.details.duration, 'm')
|
const stop = start.add(item.details.duration, 'm')
|
||||||
programs.push({
|
programs.push({
|
||||||
title: item.title,
|
title: item.title,
|
||||||
category: item.details.categories,
|
category: item.details.categories,
|
||||||
description: item.details.description,
|
description: item.details.description,
|
||||||
image: item.details.image,
|
image: item.details.image,
|
||||||
season: parseSeason(item),
|
season: parseSeason(item),
|
||||||
episode: parseEpisode(item),
|
episode: parseEpisode(item),
|
||||||
start,
|
start,
|
||||||
stop
|
stop
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return programs
|
return programs
|
||||||
},
|
},
|
||||||
async channels() {
|
async channels() {
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const data = await axios
|
const data = await axios
|
||||||
.get(`https://cs-vcb.allente.dk/epg/events?date=${dayjs().format('YYYY-MM-DD')}`)
|
.get(`https://cs-vcb.allente.dk/epg/events?date=${dayjs().format('YYYY-MM-DD')}`)
|
||||||
.then(r => r.data)
|
.then(r => r.data)
|
||||||
.catch(console.log)
|
.catch(console.log)
|
||||||
|
|
||||||
return data.channels.map(item => {
|
return data.channels.map(item => {
|
||||||
return {
|
return {
|
||||||
lang: 'da',
|
lang: 'da',
|
||||||
site_id: item.id,
|
site_id: item.id,
|
||||||
name: item.name
|
name: item.name
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseItems(content, channel) {
|
function parseItems(content, channel) {
|
||||||
const data = JSON.parse(content)
|
const data = JSON.parse(content)
|
||||||
if (!data || !Array.isArray(data.channels)) return []
|
if (!data || !Array.isArray(data.channels)) return []
|
||||||
const channelData = data.channels.find(i => i.id === channel.site_id)
|
const channelData = data.channels.find(i => i.id === channel.site_id)
|
||||||
|
|
||||||
return channelData && Array.isArray(channelData.events) ? channelData.events : []
|
return channelData && Array.isArray(channelData.events) ? channelData.events : []
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseSeason(item) {
|
function parseSeason(item) {
|
||||||
return item.details.season || null
|
return item.details.season || null
|
||||||
}
|
}
|
||||||
function parseEpisode(item) {
|
function parseEpisode(item) {
|
||||||
return item.details.episode || null
|
return item.details.episode || null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,51 @@
|
|||||||
const { parser, url } = require('./allente.dk.config.js')
|
const { parser, url } = require('./allente.dk.config.js')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require('dayjs/plugin/utc')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
dayjs.extend(customParseFormat)
|
const { readFileSync } = require('fs')
|
||||||
dayjs.extend(utc)
|
const { resolve } = require('path')
|
||||||
|
dayjs.extend(customParseFormat)
|
||||||
const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d')
|
dayjs.extend(utc)
|
||||||
const channel = {
|
|
||||||
site_id: '0148',
|
const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d')
|
||||||
xmltv_id: 'SVT1.se'
|
const channel = {
|
||||||
}
|
site_id: '0148',
|
||||||
|
xmltv_id: 'SVT1.se'
|
||||||
it('can generate valid url', () => {
|
}
|
||||||
expect(url({ date, channel })).toBe('https://cs-vcb.allente.dk/epg/events?date=2021-11-17')
|
|
||||||
})
|
it('can generate valid url', () => {
|
||||||
|
expect(url({ date, channel })).toBe('https://cs-vcb.allente.dk/epg/events?date=2021-11-17')
|
||||||
it('can parse response', () => {
|
})
|
||||||
const content =
|
|
||||||
'{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}'
|
it('can parse response', () => {
|
||||||
const result = parser({ content, channel }).map(p => {
|
const content = readFileSync(resolve(__dirname, '__data__/content.json'))
|
||||||
p.start = p.start.toJSON()
|
const result = parser({ content, channel }).map(p => {
|
||||||
p.stop = p.stop.toJSON()
|
p.start = p.start.toJSON()
|
||||||
return p
|
p.stop = p.stop.toJSON()
|
||||||
})
|
return p
|
||||||
|
})
|
||||||
expect(result).toMatchObject([
|
|
||||||
{
|
expect(result).toMatchObject([
|
||||||
start: '2022-08-22T07:10:00.000Z',
|
{
|
||||||
stop: '2022-08-22T07:30:00.000Z',
|
start: '2022-08-22T07:10:00.000Z',
|
||||||
title: 'Hemmagympa med Sofia',
|
stop: '2022-08-22T07:30:00.000Z',
|
||||||
category: ['other'],
|
title: 'Hemmagympa med Sofia',
|
||||||
description:
|
category: ['other'],
|
||||||
'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.',
|
description:
|
||||||
image:
|
'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.',
|
||||||
'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440',
|
image:
|
||||||
season: 4,
|
'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440',
|
||||||
episode: 1
|
season: 4,
|
||||||
}
|
episode: 1
|
||||||
])
|
}
|
||||||
})
|
])
|
||||||
|
})
|
||||||
it('can handle empty guide', () => {
|
|
||||||
const result = parser({
|
it('can handle empty guide', () => {
|
||||||
date,
|
const result = parser({
|
||||||
channel,
|
date,
|
||||||
content: '{"date":"2001-11-17","categories":[],"channels":[]}'
|
channel,
|
||||||
})
|
content: '{"date":"2001-11-17","categories":[],"channels":[]}'
|
||||||
expect(result).toMatchObject([])
|
})
|
||||||
})
|
expect(result).toMatchObject([])
|
||||||
|
})
|
||||||
|
|||||||
1
sites/allente.fi/__data__/content.json
Normal file
1
sites/allente.fi/__data__/content.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}
|
||||||
1
sites/allente.fi/__data__/no_content.json
Normal file
1
sites/allente.fi/__data__/no_content.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"date":"2001-11-17","categories":[],"channels":[]}
|
||||||
@@ -1,65 +1,65 @@
|
|||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
site: 'allente.fi',
|
site: 'allente.fi',
|
||||||
days: 2,
|
days: 2,
|
||||||
request: {
|
request: {
|
||||||
cache: {
|
cache: {
|
||||||
ttl: 60 * 60 * 1000 // 1 hour
|
ttl: 60 * 60 * 1000 // 1 hour
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
url({ date }) {
|
url({ date }) {
|
||||||
return `https://cs-vcb.allente.fi/epg/events?date=${date.format('YYYY-MM-DD')}`
|
return `https://cs-vcb.allente.fi/epg/events?date=${date.format('YYYY-MM-DD')}`
|
||||||
},
|
},
|
||||||
parser({ content, channel }) {
|
parser({ content, channel }) {
|
||||||
let programs = []
|
let programs = []
|
||||||
const items = parseItems(content, channel)
|
const items = parseItems(content, channel)
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
if (!item.details) return
|
if (!item.details) return
|
||||||
const start = dayjs(item.time)
|
const start = dayjs(item.time)
|
||||||
const stop = start.add(item.details.duration, 'm')
|
const stop = start.add(item.details.duration, 'm')
|
||||||
programs.push({
|
programs.push({
|
||||||
title: item.title,
|
title: item.title,
|
||||||
category: item.details.categories,
|
category: item.details.categories,
|
||||||
description: item.details.description,
|
description: item.details.description,
|
||||||
image: item.details.image,
|
image: item.details.image,
|
||||||
season: parseSeason(item),
|
season: parseSeason(item),
|
||||||
episode: parseEpisode(item),
|
episode: parseEpisode(item),
|
||||||
start,
|
start,
|
||||||
stop
|
stop
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return programs
|
return programs
|
||||||
},
|
},
|
||||||
async channels() {
|
async channels() {
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const data = await axios
|
const data = await axios
|
||||||
.get(`https://cs-vcb.allente.fi/epg/events?date=${dayjs().format('YYYY-MM-DD')}`)
|
.get(`https://cs-vcb.allente.fi/epg/events?date=${dayjs().format('YYYY-MM-DD')}`)
|
||||||
.then(r => r.data)
|
.then(r => r.data)
|
||||||
.catch(console.log)
|
.catch(console.log)
|
||||||
|
|
||||||
return data.channels.map(item => {
|
return data.channels.map(item => {
|
||||||
return {
|
return {
|
||||||
lang: 'fi',
|
lang: 'fi',
|
||||||
site_id: item.id,
|
site_id: item.id,
|
||||||
name: item.name
|
name: item.name
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseItems(content, channel) {
|
function parseItems(content, channel) {
|
||||||
const data = JSON.parse(content)
|
const data = JSON.parse(content)
|
||||||
if (!data || !Array.isArray(data.channels)) return []
|
if (!data || !Array.isArray(data.channels)) return []
|
||||||
const channelData = data.channels.find(i => i.id === channel.site_id)
|
const channelData = data.channels.find(i => i.id === channel.site_id)
|
||||||
|
|
||||||
return channelData && Array.isArray(channelData.events) ? channelData.events : []
|
return channelData && Array.isArray(channelData.events) ? channelData.events : []
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseSeason(item) {
|
function parseSeason(item) {
|
||||||
return item.details.season || null
|
return item.details.season || null
|
||||||
}
|
}
|
||||||
function parseEpisode(item) {
|
function parseEpisode(item) {
|
||||||
return item.details.episode || null
|
return item.details.episode || null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,51 @@
|
|||||||
const { parser, url } = require('./allente.fi.config.js')
|
const { parser, url } = require('./allente.fi.config.js')
|
||||||
const dayjs = require('dayjs')
|
const fs = require('fs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const path = require('path')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const dayjs = require('dayjs')
|
||||||
dayjs.extend(customParseFormat)
|
const utc = require('dayjs/plugin/utc')
|
||||||
dayjs.extend(utc)
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
|
dayjs.extend(customParseFormat)
|
||||||
const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d')
|
dayjs.extend(utc)
|
||||||
const channel = {
|
|
||||||
site_id: '0148',
|
const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d')
|
||||||
xmltv_id: 'SVT1.se'
|
const channel = {
|
||||||
}
|
site_id: '0148',
|
||||||
|
xmltv_id: 'SVT1.se'
|
||||||
it('can generate valid url', () => {
|
}
|
||||||
expect(url({ date, channel })).toBe('https://cs-vcb.allente.fi/epg/events?date=2021-11-17')
|
|
||||||
})
|
it('can generate valid url', () => {
|
||||||
|
expect(url({ date, channel })).toBe('https://cs-vcb.allente.fi/epg/events?date=2021-11-17')
|
||||||
it('can parse response', () => {
|
})
|
||||||
const content =
|
|
||||||
'{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}'
|
it('can parse response', () => {
|
||||||
const result = parser({ content, channel }).map(p => {
|
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
|
||||||
p.start = p.start.toJSON()
|
const result = parser({ content, channel }).map(p => {
|
||||||
p.stop = p.stop.toJSON()
|
p.start = p.start.toJSON()
|
||||||
return p
|
p.stop = p.stop.toJSON()
|
||||||
})
|
return p
|
||||||
|
})
|
||||||
expect(result).toMatchObject([
|
|
||||||
{
|
expect(result).toMatchObject([
|
||||||
start: '2022-08-22T07:10:00.000Z',
|
{
|
||||||
stop: '2022-08-22T07:30:00.000Z',
|
start: '2022-08-22T07:10:00.000Z',
|
||||||
title: 'Hemmagympa med Sofia',
|
stop: '2022-08-22T07:30:00.000Z',
|
||||||
category: ['other'],
|
title: 'Hemmagympa med Sofia',
|
||||||
description:
|
category: ['other'],
|
||||||
'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.',
|
description:
|
||||||
image:
|
'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.',
|
||||||
'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440',
|
image:
|
||||||
season: 4,
|
'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440',
|
||||||
episode: 1
|
season: 4,
|
||||||
}
|
episode: 1
|
||||||
])
|
}
|
||||||
})
|
])
|
||||||
|
})
|
||||||
it('can handle empty guide', () => {
|
|
||||||
const result = parser({
|
it('can handle empty guide', () => {
|
||||||
date,
|
const result = parser({
|
||||||
channel,
|
date,
|
||||||
content: '{"date":"2001-11-17","categories":[],"channels":[]}'
|
channel,
|
||||||
})
|
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
|
||||||
expect(result).toMatchObject([])
|
})
|
||||||
})
|
expect(result).toMatchObject([])
|
||||||
|
})
|
||||||
|
|||||||
1
sites/allente.no/__data__/content.json
Normal file
1
sites/allente.no/__data__/content.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}
|
||||||
1
sites/allente.no/__data__/no_content.json
Normal file
1
sites/allente.no/__data__/no_content.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"date":"2001-11-17","categories":[],"channels":[]}
|
||||||
@@ -1,65 +1,65 @@
|
|||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
site: 'allente.no',
|
site: 'allente.no',
|
||||||
days: 2,
|
days: 2,
|
||||||
request: {
|
request: {
|
||||||
cache: {
|
cache: {
|
||||||
ttl: 60 * 60 * 1000 // 1 hour
|
ttl: 60 * 60 * 1000 // 1 hour
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
url({ date }) {
|
url({ date }) {
|
||||||
return `https://cs-vcb.allente.no/epg/events?date=${date.format('YYYY-MM-DD')}`
|
return `https://cs-vcb.allente.no/epg/events?date=${date.format('YYYY-MM-DD')}`
|
||||||
},
|
},
|
||||||
parser({ content, channel }) {
|
parser({ content, channel }) {
|
||||||
let programs = []
|
let programs = []
|
||||||
const items = parseItems(content, channel)
|
const items = parseItems(content, channel)
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
if (!item.details) return
|
if (!item.details) return
|
||||||
const start = dayjs(item.time)
|
const start = dayjs(item.time)
|
||||||
const stop = start.add(item.details.duration, 'm')
|
const stop = start.add(item.details.duration, 'm')
|
||||||
programs.push({
|
programs.push({
|
||||||
title: item.title,
|
title: item.title,
|
||||||
category: item.details.categories,
|
category: item.details.categories,
|
||||||
description: item.details.description,
|
description: item.details.description,
|
||||||
image: item.details.image,
|
image: item.details.image,
|
||||||
season: parseSeason(item),
|
season: parseSeason(item),
|
||||||
episode: parseEpisode(item),
|
episode: parseEpisode(item),
|
||||||
start,
|
start,
|
||||||
stop
|
stop
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return programs
|
return programs
|
||||||
},
|
},
|
||||||
async channels() {
|
async channels() {
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const data = await axios
|
const data = await axios
|
||||||
.get(`https://cs-vcb.allente.no/epg/events?date=${dayjs().format('YYYY-MM-DD')}`)
|
.get(`https://cs-vcb.allente.no/epg/events?date=${dayjs().format('YYYY-MM-DD')}`)
|
||||||
.then(r => r.data)
|
.then(r => r.data)
|
||||||
.catch(console.log)
|
.catch(console.log)
|
||||||
|
|
||||||
return data.channels.map(item => {
|
return data.channels.map(item => {
|
||||||
return {
|
return {
|
||||||
lang: 'no',
|
lang: 'no',
|
||||||
site_id: item.id,
|
site_id: item.id,
|
||||||
name: item.name
|
name: item.name
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseItems(content, channel) {
|
function parseItems(content, channel) {
|
||||||
const data = JSON.parse(content)
|
const data = JSON.parse(content)
|
||||||
if (!data || !Array.isArray(data.channels)) return []
|
if (!data || !Array.isArray(data.channels)) return []
|
||||||
const channelData = data.channels.find(i => i.id === channel.site_id)
|
const channelData = data.channels.find(i => i.id === channel.site_id)
|
||||||
|
|
||||||
return channelData && Array.isArray(channelData.events) ? channelData.events : []
|
return channelData && Array.isArray(channelData.events) ? channelData.events : []
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseSeason(item) {
|
function parseSeason(item) {
|
||||||
return item.details.season || null
|
return item.details.season || null
|
||||||
}
|
}
|
||||||
function parseEpisode(item) {
|
function parseEpisode(item) {
|
||||||
return item.details.episode || null
|
return item.details.episode || null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,51 @@
|
|||||||
const { parser, url } = require('./allente.no.config.js')
|
const { parser, url } = require('./allente.no.config.js')
|
||||||
const dayjs = require('dayjs')
|
const fs = require('fs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const path = require('path')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const dayjs = require('dayjs')
|
||||||
dayjs.extend(customParseFormat)
|
const utc = require('dayjs/plugin/utc')
|
||||||
dayjs.extend(utc)
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
|
dayjs.extend(customParseFormat)
|
||||||
const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d')
|
dayjs.extend(utc)
|
||||||
const channel = {
|
|
||||||
site_id: '0148',
|
const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d')
|
||||||
xmltv_id: 'SVT1.se'
|
const channel = {
|
||||||
}
|
site_id: '0148',
|
||||||
|
xmltv_id: 'SVT1.se'
|
||||||
it('can generate valid url', () => {
|
}
|
||||||
expect(url({ date, channel })).toBe('https://cs-vcb.allente.no/epg/events?date=2021-11-17')
|
|
||||||
})
|
it('can generate valid url', () => {
|
||||||
|
expect(url({ date, channel })).toBe('https://cs-vcb.allente.no/epg/events?date=2021-11-17')
|
||||||
it('can parse response', () => {
|
})
|
||||||
const content =
|
|
||||||
'{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}'
|
it('can parse response', () => {
|
||||||
const result = parser({ content, channel }).map(p => {
|
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
|
||||||
p.start = p.start.toJSON()
|
const result = parser({ content, channel }).map(p => {
|
||||||
p.stop = p.stop.toJSON()
|
p.start = p.start.toJSON()
|
||||||
return p
|
p.stop = p.stop.toJSON()
|
||||||
})
|
return p
|
||||||
|
})
|
||||||
expect(result).toMatchObject([
|
|
||||||
{
|
expect(result).toMatchObject([
|
||||||
start: '2022-08-22T07:10:00.000Z',
|
{
|
||||||
stop: '2022-08-22T07:30:00.000Z',
|
start: '2022-08-22T07:10:00.000Z',
|
||||||
title: 'Hemmagympa med Sofia',
|
stop: '2022-08-22T07:30:00.000Z',
|
||||||
category: ['other'],
|
title: 'Hemmagympa med Sofia',
|
||||||
description:
|
category: ['other'],
|
||||||
'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.',
|
description:
|
||||||
image:
|
'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.',
|
||||||
'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440',
|
image:
|
||||||
season: 4,
|
'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440',
|
||||||
episode: 1
|
season: 4,
|
||||||
}
|
episode: 1
|
||||||
])
|
}
|
||||||
})
|
])
|
||||||
|
})
|
||||||
it('can handle empty guide', () => {
|
|
||||||
const result = parser({
|
it('can handle empty guide', () => {
|
||||||
date,
|
const result = parser({
|
||||||
channel,
|
date,
|
||||||
content: '{"date":"2001-11-17","categories":[],"channels":[]}'
|
channel,
|
||||||
})
|
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
|
||||||
expect(result).toMatchObject([])
|
})
|
||||||
})
|
expect(result).toMatchObject([])
|
||||||
|
})
|
||||||
|
|||||||
1
sites/allente.se/__data__/content.json
Normal file
1
sites/allente.se/__data__/content.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}
|
||||||
1
sites/allente.se/__data__/no_content.json
Normal file
1
sites/allente.se/__data__/no_content.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"date":"2001-11-17","categories":[],"channels":[]}
|
||||||
@@ -1,65 +1,65 @@
|
|||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
site: 'allente.se',
|
site: 'allente.se',
|
||||||
days: 2,
|
days: 2,
|
||||||
request: {
|
request: {
|
||||||
cache: {
|
cache: {
|
||||||
ttl: 60 * 60 * 1000 // 1 hour
|
ttl: 60 * 60 * 1000 // 1 hour
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
url({ date }) {
|
url({ date }) {
|
||||||
return `https://cs-vcb.allente.se/epg/events?date=${date.format('YYYY-MM-DD')}`
|
return `https://cs-vcb.allente.se/epg/events?date=${date.format('YYYY-MM-DD')}`
|
||||||
},
|
},
|
||||||
parser({ content, channel }) {
|
parser({ content, channel }) {
|
||||||
let programs = []
|
let programs = []
|
||||||
const items = parseItems(content, channel)
|
const items = parseItems(content, channel)
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
if (!item.details) return
|
if (!item.details) return
|
||||||
const start = dayjs(item.time)
|
const start = dayjs(item.time)
|
||||||
const stop = start.add(item.details.duration, 'm')
|
const stop = start.add(item.details.duration, 'm')
|
||||||
programs.push({
|
programs.push({
|
||||||
title: item.title,
|
title: item.title,
|
||||||
category: item.details.categories,
|
category: item.details.categories,
|
||||||
description: item.details.description,
|
description: item.details.description,
|
||||||
image: item.details.image,
|
image: item.details.image,
|
||||||
season: parseSeason(item),
|
season: parseSeason(item),
|
||||||
episode: parseEpisode(item),
|
episode: parseEpisode(item),
|
||||||
start,
|
start,
|
||||||
stop
|
stop
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return programs
|
return programs
|
||||||
},
|
},
|
||||||
async channels() {
|
async channels() {
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const data = await axios
|
const data = await axios
|
||||||
.get(`https://cs-vcb.allente.se/epg/events?date=${dayjs().format('YYYY-MM-DD')}`)
|
.get(`https://cs-vcb.allente.se/epg/events?date=${dayjs().format('YYYY-MM-DD')}`)
|
||||||
.then(r => r.data)
|
.then(r => r.data)
|
||||||
.catch(console.log)
|
.catch(console.log)
|
||||||
|
|
||||||
return data.channels.map(item => {
|
return data.channels.map(item => {
|
||||||
return {
|
return {
|
||||||
lang: 'sv',
|
lang: 'sv',
|
||||||
site_id: item.id,
|
site_id: item.id,
|
||||||
name: item.name
|
name: item.name
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseItems(content, channel) {
|
function parseItems(content, channel) {
|
||||||
const data = JSON.parse(content)
|
const data = JSON.parse(content)
|
||||||
if (!data || !Array.isArray(data.channels)) return []
|
if (!data || !Array.isArray(data.channels)) return []
|
||||||
const channelData = data.channels.find(i => i.id === channel.site_id)
|
const channelData = data.channels.find(i => i.id === channel.site_id)
|
||||||
|
|
||||||
return channelData && Array.isArray(channelData.events) ? channelData.events : []
|
return channelData && Array.isArray(channelData.events) ? channelData.events : []
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseSeason(item) {
|
function parseSeason(item) {
|
||||||
return item.details.season || null
|
return item.details.season || null
|
||||||
}
|
}
|
||||||
function parseEpisode(item) {
|
function parseEpisode(item) {
|
||||||
return item.details.episode || null
|
return item.details.episode || null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,51 @@
|
|||||||
const { parser, url } = require('./allente.se.config.js')
|
const { parser, url } = require('./allente.se.config.js')
|
||||||
const dayjs = require('dayjs')
|
const fs = require('fs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const path = require('path')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const dayjs = require('dayjs')
|
||||||
dayjs.extend(customParseFormat)
|
const utc = require('dayjs/plugin/utc')
|
||||||
dayjs.extend(utc)
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
|
dayjs.extend(customParseFormat)
|
||||||
const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d')
|
dayjs.extend(utc)
|
||||||
const channel = {
|
|
||||||
site_id: '0148',
|
const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d')
|
||||||
xmltv_id: 'SVT1.se'
|
const channel = {
|
||||||
}
|
site_id: '0148',
|
||||||
|
xmltv_id: 'SVT1.se'
|
||||||
it('can generate valid url', () => {
|
}
|
||||||
expect(url({ date, channel })).toBe('https://cs-vcb.allente.se/epg/events?date=2021-11-17')
|
|
||||||
})
|
it('can generate valid url', () => {
|
||||||
|
expect(url({ date, channel })).toBe('https://cs-vcb.allente.se/epg/events?date=2021-11-17')
|
||||||
it('can parse response', () => {
|
})
|
||||||
const content =
|
|
||||||
'{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}'
|
it('can parse response', () => {
|
||||||
const result = parser({ content, channel }).map(p => {
|
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
|
||||||
p.start = p.start.toJSON()
|
const result = parser({ content, channel }).map(p => {
|
||||||
p.stop = p.stop.toJSON()
|
p.start = p.start.toJSON()
|
||||||
return p
|
p.stop = p.stop.toJSON()
|
||||||
})
|
return p
|
||||||
|
})
|
||||||
expect(result).toMatchObject([
|
|
||||||
{
|
expect(result).toMatchObject([
|
||||||
start: '2022-08-22T07:10:00.000Z',
|
{
|
||||||
stop: '2022-08-22T07:30:00.000Z',
|
start: '2022-08-22T07:10:00.000Z',
|
||||||
title: 'Hemmagympa med Sofia',
|
stop: '2022-08-22T07:30:00.000Z',
|
||||||
category: ['other'],
|
title: 'Hemmagympa med Sofia',
|
||||||
description:
|
category: ['other'],
|
||||||
'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.',
|
description:
|
||||||
image:
|
'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.',
|
||||||
'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440',
|
image:
|
||||||
season: 4,
|
'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440',
|
||||||
episode: 1
|
season: 4,
|
||||||
}
|
episode: 1
|
||||||
])
|
}
|
||||||
})
|
])
|
||||||
|
})
|
||||||
it('can handle empty guide', () => {
|
|
||||||
const result = parser({
|
it('can handle empty guide', () => {
|
||||||
date,
|
const result = parser({
|
||||||
channel,
|
date,
|
||||||
content: '{"date":"2001-11-17","categories":[],"channels":[]}'
|
channel,
|
||||||
})
|
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
|
||||||
expect(result).toMatchObject([])
|
})
|
||||||
})
|
expect(result).toMatchObject([])
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,59 +1,59 @@
|
|||||||
const cheerio = require('cheerio')
|
const cheerio = require('cheerio')
|
||||||
const { DateTime } = require('luxon')
|
const { DateTime } = require('luxon')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
site: 'andorradifusio.ad',
|
site: 'andorradifusio.ad',
|
||||||
days: 2,
|
days: 2,
|
||||||
url({ channel }) {
|
url({ channel }) {
|
||||||
return `https://www.andorradifusio.ad/programacio/${channel.site_id}`
|
return `https://www.andorradifusio.ad/programacio/${channel.site_id}`
|
||||||
},
|
},
|
||||||
parser({ content, date }) {
|
parser({ content, date }) {
|
||||||
const programs = []
|
const programs = []
|
||||||
const items = parseItems(content, date)
|
const items = parseItems(content, date)
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
const prev = programs[programs.length - 1]
|
const prev = programs[programs.length - 1]
|
||||||
let start = parseStart(item, date)
|
let start = parseStart(item, date)
|
||||||
if (prev) {
|
if (prev) {
|
||||||
if (start < prev.start) {
|
if (start < prev.start) {
|
||||||
start = start.plus({ days: 1 })
|
start = start.plus({ days: 1 })
|
||||||
date = date.add(1, 'd')
|
date = date.add(1, 'd')
|
||||||
}
|
}
|
||||||
prev.stop = start
|
prev.stop = start
|
||||||
}
|
}
|
||||||
const stop = start.plus({ hours: 1 })
|
const stop = start.plus({ hours: 1 })
|
||||||
programs.push({
|
programs.push({
|
||||||
title: item.title,
|
title: item.title,
|
||||||
start,
|
start,
|
||||||
stop
|
stop
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return programs
|
return programs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseStart(item, date) {
|
function parseStart(item, date) {
|
||||||
const dateString = `${date.format('MM/DD/YYYY')} ${item.time}`
|
const dateString = `${date.format('MM/DD/YYYY')} ${item.time}`
|
||||||
|
|
||||||
return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH:mm', { zone: 'Europe/Madrid' }).toUTC()
|
return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH:mm', { zone: 'Europe/Madrid' }).toUTC()
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseItems(content, date) {
|
function parseItems(content, date) {
|
||||||
const $ = cheerio.load(content)
|
const $ = cheerio.load(content)
|
||||||
const day = DateTime.fromMillis(date.valueOf()).setLocale('ca').toFormat('dd LLLL').toLowerCase()
|
const day = DateTime.fromMillis(date.valueOf()).setLocale('ca').toFormat('dd LLLL').toLowerCase()
|
||||||
const column = $('.programacio-dia > h3 > .dia')
|
const column = $('.programacio-dia > h3 > .dia')
|
||||||
.filter((i, el) => $(el).text() === day.slice(0, 6) + '.')
|
.filter((i, el) => $(el).text() === day.slice(0, 6) + '.')
|
||||||
.first()
|
.first()
|
||||||
.parent()
|
.parent()
|
||||||
.parent()
|
.parent()
|
||||||
const items = []
|
const items = []
|
||||||
const titles = column.find('p').toArray()
|
const titles = column.find('p').toArray()
|
||||||
column.find('h4').each((i, time) => {
|
column.find('h4').each((i, time) => {
|
||||||
items.push({
|
items.push({
|
||||||
time: $(time).text(),
|
time: $(time).text(),
|
||||||
title: $(titles[i]).text()
|
title: $(titles[i]).text()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,47 @@
|
|||||||
const { parser, url } = require('./andorradifusio.ad.config.js')
|
const { parser, url } = require('./andorradifusio.ad.config.js')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require('dayjs/plugin/utc')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
|
|
||||||
const date = dayjs.utc('2023-06-07', 'YYYY-MM-DD').startOf('d')
|
const date = dayjs.utc('2023-06-07', 'YYYY-MM-DD').startOf('d')
|
||||||
const channel = {
|
const channel = {
|
||||||
site_id: 'atv',
|
site_id: 'atv',
|
||||||
xmltv_id: 'AndorraTV.ad'
|
xmltv_id: 'AndorraTV.ad'
|
||||||
}
|
}
|
||||||
|
|
||||||
it('can generate valid url', () => {
|
it('can generate valid url', () => {
|
||||||
expect(url({ channel })).toBe('https://www.andorradifusio.ad/programacio/atv')
|
expect(url({ channel })).toBe('https://www.andorradifusio.ad/programacio/atv')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can parse response', () => {
|
it('can parse response', () => {
|
||||||
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
|
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
|
||||||
const results = parser({ content, date }).map(p => {
|
const results = parser({ content, date }).map(p => {
|
||||||
p.start = p.start.toJSON()
|
p.start = p.start.toJSON()
|
||||||
p.stop = p.stop.toJSON()
|
p.stop = p.stop.toJSON()
|
||||||
return p
|
return p
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(results[0]).toMatchObject({
|
expect(results[0]).toMatchObject({
|
||||||
start: '2023-06-07T05:00:00.000Z',
|
start: '2023-06-07T05:00:00.000Z',
|
||||||
stop: '2023-06-07T06:00:00.000Z',
|
stop: '2023-06-07T06:00:00.000Z',
|
||||||
title: 'Club Piolet'
|
title: 'Club Piolet'
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(results[20]).toMatchObject({
|
expect(results[20]).toMatchObject({
|
||||||
start: '2023-06-07T23:00:00.000Z',
|
start: '2023-06-07T23:00:00.000Z',
|
||||||
stop: '2023-06-08T00:00:00.000Z',
|
stop: '2023-06-08T00:00:00.000Z',
|
||||||
title: 'Àrea Andorra Difusió'
|
title: 'Àrea Andorra Difusió'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can handle empty guide', () => {
|
it('can handle empty guide', () => {
|
||||||
const result = parser({
|
const result = parser({
|
||||||
date,
|
date,
|
||||||
content: '<!DOCTYPE html><html><head></head><body></body></html>'
|
content: '<!DOCTYPE html><html><head></head><body></body></html>'
|
||||||
})
|
})
|
||||||
expect(result).toMatchObject([])
|
expect(result).toMatchObject([])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,108 +1,108 @@
|
|||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require('dayjs/plugin/utc')
|
||||||
const timezone = require('dayjs/plugin/timezone')
|
const timezone = require('dayjs/plugin/timezone')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
|
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
|
|
||||||
const API_ENDPOINT = 'https://cds-frontend.vera.com.uy/api-contenidos'
|
const API_ENDPOINT = 'https://cds-frontend.vera.com.uy/api-contenidos'
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
site: 'anteltv.com.uy',
|
site: 'anteltv.com.uy',
|
||||||
days: 2,
|
days: 2,
|
||||||
async url({ date, channel }) {
|
async url({ date, channel }) {
|
||||||
const session = await loadSessionDetails()
|
const session = await loadSessionDetails()
|
||||||
if (!session || !session.token) return null
|
if (!session || !session.token) return null
|
||||||
|
|
||||||
return `${API_ENDPOINT}/canales/epg/${
|
return `${API_ENDPOINT}/canales/epg/${
|
||||||
channel.site_id
|
channel.site_id
|
||||||
}?limit=500&dias_siguientes=0&fecha=${date.format('YYYY-MM-DD')}&token=${session.token}`
|
}?limit=500&dias_siguientes=0&fecha=${date.format('YYYY-MM-DD')}&token=${session.token}`
|
||||||
},
|
},
|
||||||
request: {
|
request: {
|
||||||
async headers() {
|
async headers() {
|
||||||
const session = await loadSessionDetails()
|
const session = await loadSessionDetails()
|
||||||
if (!session || !session.jwt) return null
|
if (!session || !session.jwt) return null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
authorization: `Bearer ${session.jwt}`,
|
authorization: `Bearer ${session.jwt}`,
|
||||||
'x-frontend-id': 1196,
|
'x-frontend-id': 1196,
|
||||||
'x-service-id': 3,
|
'x-service-id': 3,
|
||||||
'x-system-id': 1
|
'x-system-id': 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
parser({ content }) {
|
parser({ content }) {
|
||||||
let programs = []
|
let programs = []
|
||||||
let items = parseItems(content)
|
let items = parseItems(content)
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
programs.push({
|
programs.push({
|
||||||
title: item.nombre_programa,
|
title: item.nombre_programa,
|
||||||
sub_title: item.subtitle,
|
sub_title: item.subtitle,
|
||||||
description: item.descripcion_programa,
|
description: item.descripcion_programa,
|
||||||
start: parseStart(item),
|
start: parseStart(item),
|
||||||
stop: parseStop(item)
|
stop: parseStop(item)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return programs
|
return programs
|
||||||
},
|
},
|
||||||
async channels() {
|
async channels() {
|
||||||
const session = await loadSessionDetails()
|
const session = await loadSessionDetails()
|
||||||
if (!session || !session.jwt || !session.token) return null
|
if (!session || !session.jwt || !session.token) return null
|
||||||
|
|
||||||
const data = await axios
|
const data = await axios
|
||||||
.get(`${API_ENDPOINT}/listas/68?token=${session.token}`, {
|
.get(`${API_ENDPOINT}/listas/68?token=${session.token}`, {
|
||||||
headers: {
|
headers: {
|
||||||
authorization: `Bearer ${session.jwt}`,
|
authorization: `Bearer ${session.jwt}`,
|
||||||
'x-frontend-id': 1196,
|
'x-frontend-id': 1196,
|
||||||
'x-service-id': 3,
|
'x-service-id': 3,
|
||||||
'x-system-id': 1
|
'x-system-id': 1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(r => r.data)
|
.then(r => r.data)
|
||||||
.catch(console.error)
|
.catch(console.error)
|
||||||
|
|
||||||
return data.contenidos.map(c => {
|
return data.contenidos.map(c => {
|
||||||
return {
|
return {
|
||||||
lang: 'es',
|
lang: 'es',
|
||||||
site_id: c.public_id,
|
site_id: c.public_id,
|
||||||
name: c.nombre
|
name: c.nombre
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseStart(item) {
|
function parseStart(item) {
|
||||||
return dayjs.tz(item.fecha_hora_inicio, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo')
|
return dayjs.tz(item.fecha_hora_inicio, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo')
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseStop(item) {
|
function parseStop(item) {
|
||||||
return dayjs.tz(item.fecha_hora_fin, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo')
|
return dayjs.tz(item.fecha_hora_fin, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo')
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseItems(content) {
|
function parseItems(content) {
|
||||||
const data = JSON.parse(content)
|
const data = JSON.parse(content)
|
||||||
if (!data || !Array.isArray(data.data)) return []
|
if (!data || !Array.isArray(data.data)) return []
|
||||||
|
|
||||||
return data.data
|
return data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadSessionDetails() {
|
function loadSessionDetails() {
|
||||||
return axios
|
return axios
|
||||||
.post(
|
.post(
|
||||||
'https://veratv-be.vera.com.uy/api/sesiones',
|
'https://veratv-be.vera.com.uy/api/sesiones',
|
||||||
{
|
{
|
||||||
tipo: 'anonima'
|
tipo: 'anonima'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.then(r => r.data)
|
.then(r => r.data)
|
||||||
.catch(console.log)
|
.catch(console.log)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,85 +1,85 @@
|
|||||||
const { parser, url, request } = require('./anteltv.com.uy.config.js')
|
const { parser, url, request } = require('./anteltv.com.uy.config.js')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require('dayjs/plugin/utc')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
|
|
||||||
jest.mock('axios')
|
jest.mock('axios')
|
||||||
|
|
||||||
axios.post.mockImplementation((url, data, opts) => {
|
axios.post.mockImplementation((url, data, opts) => {
|
||||||
if (
|
if (
|
||||||
url === 'https://veratv-be.vera.com.uy/api/sesiones' &&
|
url === 'https://veratv-be.vera.com.uy/api/sesiones' &&
|
||||||
JSON.stringify(opts.headers) ===
|
JSON.stringify(opts.headers) ===
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}) &&
|
}) &&
|
||||||
JSON.stringify(data) ===
|
JSON.stringify(data) ===
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
tipo: 'anonima'
|
tipo: 'anonima'
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/session.json')))
|
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/session.json')))
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/no_session.json')))
|
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/no_session.json')))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const date = dayjs.utc('2023-02-11', 'YYYY-MM-DD').startOf('d')
|
const date = dayjs.utc('2023-02-11', 'YYYY-MM-DD').startOf('d')
|
||||||
const channel = {
|
const channel = {
|
||||||
site_id: '2s6nd',
|
site_id: '2s6nd',
|
||||||
xmltv_id: 'Canal5.uy'
|
xmltv_id: 'Canal5.uy'
|
||||||
}
|
}
|
||||||
|
|
||||||
it('can generate valid url', async () => {
|
it('can generate valid url', async () => {
|
||||||
const result = await url({ date, channel })
|
const result = await url({ date, channel })
|
||||||
|
|
||||||
expect(result).toBe(
|
expect(result).toBe(
|
||||||
'https://cds-frontend.vera.com.uy/api-contenidos/canales/epg/2s6nd?limit=500&dias_siguientes=0&fecha=2023-02-11&token=MpDY52p1V6g511VSABp1015B'
|
'https://cds-frontend.vera.com.uy/api-contenidos/canales/epg/2s6nd?limit=500&dias_siguientes=0&fecha=2023-02-11&token=MpDY52p1V6g511VSABp1015B'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can generate valid request headers', async () => {
|
it('can generate valid request headers', async () => {
|
||||||
const result = await request.headers()
|
const result = await request.headers()
|
||||||
|
|
||||||
expect(result).toMatchObject({
|
expect(result).toMatchObject({
|
||||||
authorization:
|
authorization:
|
||||||
'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOnsidGlwbyI6ImFub25pbWEifSwic3ViIjoiTXBEWTUycDFWNmc1MTFWU0FCcDEwMTVCIiwicHJuIjp7ImlkX3NlcnZpY2lvIjozLCJpZF9mcm9udGVuZCI6MTE5NiwiaXAiOiIxNzkuMjcuMTU0LjI0MiIsImlwX3JlZmVyZW5jaWFkYSI6IjE4OC4yNDIuNDguOTMiLCJpZF9kaXNwb3NpdGl2byI6MH0sImF1ZCI6IkFwcHNcL1dlYnMgRnJvbnRlbmRzIiwiaWF0IjoxNjc1ODI3NDU2LCJleHAiOjE2NzU4NDkwNTZ9.8bAQciQl5DOIZF7GgCl6ad-KJUSpqQREetozGv_IH5s',
|
'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOnsidGlwbyI6ImFub25pbWEifSwic3ViIjoiTXBEWTUycDFWNmc1MTFWU0FCcDEwMTVCIiwicHJuIjp7ImlkX3NlcnZpY2lvIjozLCJpZF9mcm9udGVuZCI6MTE5NiwiaXAiOiIxNzkuMjcuMTU0LjI0MiIsImlwX3JlZmVyZW5jaWFkYSI6IjE4OC4yNDIuNDguOTMiLCJpZF9kaXNwb3NpdGl2byI6MH0sImF1ZCI6IkFwcHNcL1dlYnMgRnJvbnRlbmRzIiwiaWF0IjoxNjc1ODI3NDU2LCJleHAiOjE2NzU4NDkwNTZ9.8bAQciQl5DOIZF7GgCl6ad-KJUSpqQREetozGv_IH5s',
|
||||||
'x-frontend-id': 1196,
|
'x-frontend-id': 1196,
|
||||||
'x-service-id': 3,
|
'x-service-id': 3,
|
||||||
'x-system-id': 1
|
'x-system-id': 1
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can parse response', () => {
|
it('can parse response', () => {
|
||||||
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
|
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
|
||||||
let results = parser({ content })
|
let results = parser({ content })
|
||||||
results = results.map(p => {
|
results = results.map(p => {
|
||||||
p.start = p.start.toJSON()
|
p.start = p.start.toJSON()
|
||||||
p.stop = p.stop.toJSON()
|
p.stop = p.stop.toJSON()
|
||||||
return p
|
return p
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(results[0]).toMatchObject({
|
expect(results[0]).toMatchObject({
|
||||||
start: '2023-02-11T02:30:00.000Z',
|
start: '2023-02-11T02:30:00.000Z',
|
||||||
stop: '2023-02-11T04:00:00.000Z',
|
stop: '2023-02-11T04:00:00.000Z',
|
||||||
title: 'Canal 5 Noticias rep.',
|
title: 'Canal 5 Noticias rep.',
|
||||||
sub_title: '',
|
sub_title: '',
|
||||||
description: ''
|
description: ''
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can handle empty guide', () => {
|
it('can handle empty guide', () => {
|
||||||
const results = parser({
|
const results = parser({
|
||||||
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8')
|
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8')
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(results).toMatchObject([])
|
expect(results).toMatchObject([])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,59 +1,59 @@
|
|||||||
const cheerio = require('cheerio')
|
const cheerio = require('cheerio')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require('dayjs/plugin/utc')
|
||||||
const timezone = require('dayjs/plugin/timezone')
|
const timezone = require('dayjs/plugin/timezone')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
|
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
site: 'antennaeurope.gr',
|
site: 'antennaeurope.gr',
|
||||||
days: 2,
|
days: 2,
|
||||||
url({ date }) {
|
url({ date }) {
|
||||||
return `https://www.antennaeurope.gr/el/tvguide.html?date=${date.format('YYYY-MM-DD')}`
|
return `https://www.antennaeurope.gr/el/tvguide.html?date=${date.format('YYYY-MM-DD')}`
|
||||||
},
|
},
|
||||||
parser({ content, date }) {
|
parser({ content, date }) {
|
||||||
const programs = []
|
const programs = []
|
||||||
const items = parseItems(content, date)
|
const items = parseItems(content, date)
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
const $item = cheerio.load(item)
|
const $item = cheerio.load(item)
|
||||||
const prev = programs[programs.length - 1]
|
const prev = programs[programs.length - 1]
|
||||||
let start = parseStart($item, date)
|
let start = parseStart($item, date)
|
||||||
if (prev) {
|
if (prev) {
|
||||||
if (start.isBefore(prev.start)) {
|
if (start.isBefore(prev.start)) {
|
||||||
start = start.add(1, 'd')
|
start = start.add(1, 'd')
|
||||||
date = date.add(1, 'd')
|
date = date.add(1, 'd')
|
||||||
}
|
}
|
||||||
prev.stop = start
|
prev.stop = start
|
||||||
}
|
}
|
||||||
const stop = start.add(30, 'm')
|
const stop = start.add(30, 'm')
|
||||||
programs.push({
|
programs.push({
|
||||||
title: parseTitle($item),
|
title: parseTitle($item),
|
||||||
start,
|
start,
|
||||||
stop
|
stop
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return programs
|
return programs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTitle($item) {
|
function parseTitle($item) {
|
||||||
return $item('.title').text().trim()
|
return $item('.title').text().trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseStart($item, date) {
|
function parseStart($item, date) {
|
||||||
const time = $item('dt.col-time').clone().children().remove().end().text().trim()
|
const time = $item('dt.col-time').clone().children().remove().end().text().trim()
|
||||||
|
|
||||||
return time
|
return time
|
||||||
? dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Athens')
|
? dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Athens')
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseItems(content) {
|
function parseItems(content) {
|
||||||
const $ = cheerio.load(content)
|
const $ = cheerio.load(content)
|
||||||
|
|
||||||
return $('dl.show').toArray()
|
return $('dl.show').toArray()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,46 @@
|
|||||||
const { parser, url } = require('./antennaeurope.gr.config.js')
|
const { parser, url } = require('./antennaeurope.gr.config.js')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require('dayjs/plugin/utc')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
|
|
||||||
const date = dayjs.utc('2025-01-21', 'YYYY-MM-DD').startOf('d')
|
const date = dayjs.utc('2025-01-21', 'YYYY-MM-DD').startOf('d')
|
||||||
|
|
||||||
it('can generate valid url', () => {
|
it('can generate valid url', () => {
|
||||||
expect(url({ date })).toBe('https://www.antennaeurope.gr/el/tvguide.html?date=2025-01-21')
|
expect(url({ date })).toBe('https://www.antennaeurope.gr/el/tvguide.html?date=2025-01-21')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can parse response', () => {
|
it('can parse response', () => {
|
||||||
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
|
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
|
||||||
let results = parser({ content, date })
|
let results = parser({ content, date })
|
||||||
results = results.map(p => {
|
results = results.map(p => {
|
||||||
p.start = p.start.toJSON()
|
p.start = p.start.toJSON()
|
||||||
p.stop = p.stop.toJSON()
|
p.stop = p.stop.toJSON()
|
||||||
return p
|
return p
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(results.length).toBe(16)
|
expect(results.length).toBe(16)
|
||||||
|
|
||||||
expect(results[0]).toMatchObject({
|
expect(results[0]).toMatchObject({
|
||||||
start: '2025-01-21T03:45:00.000Z',
|
start: '2025-01-21T03:45:00.000Z',
|
||||||
stop: '2025-01-21T07:50:00.000Z',
|
stop: '2025-01-21T07:50:00.000Z',
|
||||||
title: 'ΚΑΛΗΜΕΡΑ ΕΛΛΑΔΑ'
|
title: 'ΚΑΛΗΜΕΡΑ ΕΛΛΑΔΑ'
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(results[15]).toMatchObject({
|
expect(results[15]).toMatchObject({
|
||||||
start: '2025-01-22T01:30:00.000Z',
|
start: '2025-01-22T01:30:00.000Z',
|
||||||
stop: '2025-01-22T02:00:00.000Z',
|
stop: '2025-01-22T02:00:00.000Z',
|
||||||
title: 'ΤΟ ΠΡΩΙΝΟ'
|
title: 'ΤΟ ΠΡΩΙΝΟ'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can handle empty guide', () => {
|
it('can handle empty guide', () => {
|
||||||
const results = parser({
|
const results = parser({
|
||||||
date,
|
date,
|
||||||
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
|
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
|
||||||
})
|
})
|
||||||
expect(results).toMatchObject([])
|
expect(results).toMatchObject([])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,59 +1,59 @@
|
|||||||
const cheerio = require('cheerio')
|
const cheerio = require('cheerio')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require('dayjs/plugin/utc')
|
||||||
const timezone = require('dayjs/plugin/timezone')
|
const timezone = require('dayjs/plugin/timezone')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
|
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
site: 'antennapacific.gr',
|
site: 'antennapacific.gr',
|
||||||
days: 2,
|
days: 2,
|
||||||
url({ date }) {
|
url({ date }) {
|
||||||
return `https://www.antennapacific.gr/el/tvguide.html?date=${date.format('YYYY-MM-DD')}`
|
return `https://www.antennapacific.gr/el/tvguide.html?date=${date.format('YYYY-MM-DD')}`
|
||||||
},
|
},
|
||||||
parser({ content, date }) {
|
parser({ content, date }) {
|
||||||
const programs = []
|
const programs = []
|
||||||
const items = parseItems(content, date)
|
const items = parseItems(content, date)
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
const $item = cheerio.load(item)
|
const $item = cheerio.load(item)
|
||||||
const prev = programs[programs.length - 1]
|
const prev = programs[programs.length - 1]
|
||||||
let start = parseStart($item, date)
|
let start = parseStart($item, date)
|
||||||
if (prev) {
|
if (prev) {
|
||||||
if (start.isBefore(prev.start)) {
|
if (start.isBefore(prev.start)) {
|
||||||
start = start.add(1, 'd')
|
start = start.add(1, 'd')
|
||||||
date = date.add(1, 'd')
|
date = date.add(1, 'd')
|
||||||
}
|
}
|
||||||
prev.stop = start
|
prev.stop = start
|
||||||
}
|
}
|
||||||
const stop = start.add(30, 'm')
|
const stop = start.add(30, 'm')
|
||||||
programs.push({
|
programs.push({
|
||||||
title: parseTitle($item),
|
title: parseTitle($item),
|
||||||
start,
|
start,
|
||||||
stop
|
stop
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return programs
|
return programs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTitle($item) {
|
function parseTitle($item) {
|
||||||
return $item('.title').text().trim()
|
return $item('.title').text().trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseStart($item, date) {
|
function parseStart($item, date) {
|
||||||
const time = $item('dt.col-time').clone().children().remove().end().text().trim()
|
const time = $item('dt.col-time').clone().children().remove().end().text().trim()
|
||||||
|
|
||||||
return time
|
return time
|
||||||
? dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Athens')
|
? dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Athens')
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseItems(content) {
|
function parseItems(content) {
|
||||||
const $ = cheerio.load(content)
|
const $ = cheerio.load(content)
|
||||||
|
|
||||||
return $('dl.show').toArray()
|
return $('dl.show').toArray()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,46 @@
|
|||||||
const { parser, url } = require('./antennapacific.gr.config.js')
|
const { parser, url } = require('./antennapacific.gr.config.js')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require('dayjs/plugin/utc')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
|
|
||||||
const date = dayjs.utc('2025-01-21', 'YYYY-MM-DD').startOf('d')
|
const date = dayjs.utc('2025-01-21', 'YYYY-MM-DD').startOf('d')
|
||||||
|
|
||||||
it('can generate valid url', () => {
|
it('can generate valid url', () => {
|
||||||
expect(url({ date })).toBe('https://www.antennapacific.gr/el/tvguide.html?date=2025-01-21')
|
expect(url({ date })).toBe('https://www.antennapacific.gr/el/tvguide.html?date=2025-01-21')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can parse response', () => {
|
it('can parse response', () => {
|
||||||
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
|
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
|
||||||
let results = parser({ content, date })
|
let results = parser({ content, date })
|
||||||
results = results.map(p => {
|
results = results.map(p => {
|
||||||
p.start = p.start.toJSON()
|
p.start = p.start.toJSON()
|
||||||
p.stop = p.stop.toJSON()
|
p.stop = p.stop.toJSON()
|
||||||
return p
|
return p
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(results.length).toBe(17)
|
expect(results.length).toBe(17)
|
||||||
|
|
||||||
expect(results[0]).toMatchObject({
|
expect(results[0]).toMatchObject({
|
||||||
start: '2025-01-21T05:00:00.000Z',
|
start: '2025-01-21T05:00:00.000Z',
|
||||||
stop: '2025-01-21T06:00:00.000Z',
|
stop: '2025-01-21T06:00:00.000Z',
|
||||||
title: 'ANT1 NEWS - ΚΕΝΤΡΙΚΟ ΔΕΛΤΙΟ'
|
title: 'ANT1 NEWS - ΚΕΝΤΡΙΚΟ ΔΕΛΤΙΟ'
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(results[16]).toMatchObject({
|
expect(results[16]).toMatchObject({
|
||||||
start: '2025-01-22T02:45:00.000Z',
|
start: '2025-01-22T02:45:00.000Z',
|
||||||
stop: '2025-01-22T03:15:00.000Z',
|
stop: '2025-01-22T03:15:00.000Z',
|
||||||
title: 'ΚΑΛΗΜΕΡΑ ΕΛΛΑΔΑ'
|
title: 'ΚΑΛΗΜΕΡΑ ΕΛΛΑΔΑ'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can handle empty guide', () => {
|
it('can handle empty guide', () => {
|
||||||
const results = parser({
|
const results = parser({
|
||||||
date,
|
date,
|
||||||
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
|
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
|
||||||
})
|
})
|
||||||
expect(results).toMatchObject([])
|
expect(results).toMatchObject([])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,59 +1,59 @@
|
|||||||
const cheerio = require('cheerio')
|
const cheerio = require('cheerio')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require('dayjs/plugin/utc')
|
||||||
const timezone = require('dayjs/plugin/timezone')
|
const timezone = require('dayjs/plugin/timezone')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
|
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
site: 'antennasatellite.gr',
|
site: 'antennasatellite.gr',
|
||||||
days: 2,
|
days: 2,
|
||||||
url({ date }) {
|
url({ date }) {
|
||||||
return `https://www.antennasatellite.gr/el/tvguide.html?date=${date.format('YYYY-MM-DD')}`
|
return `https://www.antennasatellite.gr/el/tvguide.html?date=${date.format('YYYY-MM-DD')}`
|
||||||
},
|
},
|
||||||
parser({ content, date }) {
|
parser({ content, date }) {
|
||||||
const programs = []
|
const programs = []
|
||||||
const items = parseItems(content, date)
|
const items = parseItems(content, date)
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
const $item = cheerio.load(item)
|
const $item = cheerio.load(item)
|
||||||
const prev = programs[programs.length - 1]
|
const prev = programs[programs.length - 1]
|
||||||
let start = parseStart($item, date)
|
let start = parseStart($item, date)
|
||||||
if (prev) {
|
if (prev) {
|
||||||
if (start.isBefore(prev.start)) {
|
if (start.isBefore(prev.start)) {
|
||||||
start = start.add(1, 'd')
|
start = start.add(1, 'd')
|
||||||
date = date.add(1, 'd')
|
date = date.add(1, 'd')
|
||||||
}
|
}
|
||||||
prev.stop = start
|
prev.stop = start
|
||||||
}
|
}
|
||||||
const stop = start.add(30, 'm')
|
const stop = start.add(30, 'm')
|
||||||
programs.push({
|
programs.push({
|
||||||
title: parseTitle($item),
|
title: parseTitle($item),
|
||||||
start,
|
start,
|
||||||
stop
|
stop
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return programs
|
return programs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTitle($item) {
|
function parseTitle($item) {
|
||||||
return $item('.title').text().trim()
|
return $item('.title').text().trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseStart($item, date) {
|
function parseStart($item, date) {
|
||||||
const time = $item('dt.col-time').clone().children().remove().end().text().trim()
|
const time = $item('dt.col-time').clone().children().remove().end().text().trim()
|
||||||
|
|
||||||
return time
|
return time
|
||||||
? dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Athens')
|
? dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Athens')
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseItems(content) {
|
function parseItems(content) {
|
||||||
const $ = cheerio.load(content)
|
const $ = cheerio.load(content)
|
||||||
|
|
||||||
return $('dl.show').toArray()
|
return $('dl.show').toArray()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,46 @@
|
|||||||
const { parser, url } = require('./antennasatellite.gr.config.js')
|
const { parser, url } = require('./antennasatellite.gr.config.js')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require('dayjs/plugin/utc')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
|
|
||||||
const date = dayjs.utc('2025-01-21', 'YYYY-MM-DD').startOf('d')
|
const date = dayjs.utc('2025-01-21', 'YYYY-MM-DD').startOf('d')
|
||||||
|
|
||||||
it('can generate valid url', () => {
|
it('can generate valid url', () => {
|
||||||
expect(url({ date })).toBe('https://www.antennasatellite.gr/el/tvguide.html?date=2025-01-21')
|
expect(url({ date })).toBe('https://www.antennasatellite.gr/el/tvguide.html?date=2025-01-21')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can parse response', () => {
|
it('can parse response', () => {
|
||||||
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
|
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
|
||||||
let results = parser({ content, date })
|
let results = parser({ content, date })
|
||||||
results = results.map(p => {
|
results = results.map(p => {
|
||||||
p.start = p.start.toJSON()
|
p.start = p.start.toJSON()
|
||||||
p.stop = p.stop.toJSON()
|
p.stop = p.stop.toJSON()
|
||||||
return p
|
return p
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(results.length).toBe(16)
|
expect(results.length).toBe(16)
|
||||||
|
|
||||||
expect(results[0]).toMatchObject({
|
expect(results[0]).toMatchObject({
|
||||||
start: '2025-01-21T04:00:00.000Z',
|
start: '2025-01-21T04:00:00.000Z',
|
||||||
stop: '2025-01-21T04:40:00.000Z',
|
stop: '2025-01-21T04:40:00.000Z',
|
||||||
title: 'ANT1 NEWS'
|
title: 'ANT1 NEWS'
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(results[15]).toMatchObject({
|
expect(results[15]).toMatchObject({
|
||||||
start: '2025-01-22T00:50:00.000Z',
|
start: '2025-01-22T00:50:00.000Z',
|
||||||
stop: '2025-01-22T01:20:00.000Z',
|
stop: '2025-01-22T01:20:00.000Z',
|
||||||
title: 'ΤΟ ΠΡΩΙΝΟ'
|
title: 'ΤΟ ΠΡΩΙΝΟ'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can handle empty guide', () => {
|
it('can handle empty guide', () => {
|
||||||
const results = parser({
|
const results = parser({
|
||||||
date,
|
date,
|
||||||
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
|
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
|
||||||
})
|
})
|
||||||
expect(results).toMatchObject([])
|
expect(results).toMatchObject([])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<channels>
|
|
||||||
<channel site="arianaafgtv.com" lang="en" xmltv_id="ArianaAfghanistanInternationalTV.us" site_id="#">Ariana Afghanistan Television</channel>
|
|
||||||
</channels>
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
const cheerio = require('cheerio')
|
|
||||||
const dayjs = require('dayjs')
|
|
||||||
const utc = require('dayjs/plugin/utc')
|
|
||||||
const timezone = require('dayjs/plugin/timezone')
|
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
|
||||||
|
|
||||||
dayjs.extend(utc)
|
|
||||||
dayjs.extend(timezone)
|
|
||||||
dayjs.extend(customParseFormat)
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
site: 'arianaafgtv.com',
|
|
||||||
days: 2,
|
|
||||||
url: 'https://www.arianaafgtv.com/index.html',
|
|
||||||
parser({ content, date }) {
|
|
||||||
const programs = []
|
|
||||||
const items = parseItems(content, date)
|
|
||||||
items.forEach(item => {
|
|
||||||
const title = item.title
|
|
||||||
const start = parseStart(item, date)
|
|
||||||
const stop = parseStop(item, date)
|
|
||||||
programs.push({
|
|
||||||
title,
|
|
||||||
start,
|
|
||||||
stop
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return programs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseStop(item, date) {
|
|
||||||
const time = `${date.format('MM/DD/YYYY')} ${item.end.toUpperCase()}`
|
|
||||||
|
|
||||||
return dayjs.tz(time, 'MM/DD/YYYY hh:mm A', 'Asia/Kabul')
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseStart(item, date) {
|
|
||||||
const time = `${date.format('MM/DD/YYYY')} ${item.start.toUpperCase()}`
|
|
||||||
|
|
||||||
return dayjs.tz(time, 'MM/DD/YYYY hh:mm A', 'Asia/Kabul')
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseItems(content, date) {
|
|
||||||
const $ = cheerio.load(content)
|
|
||||||
const dayOfWeek = date.format('dddd')
|
|
||||||
const column = $('.H4')
|
|
||||||
.filter((i, el) => {
|
|
||||||
return $(el).text() === dayOfWeek
|
|
||||||
})
|
|
||||||
.first()
|
|
||||||
.parent()
|
|
||||||
|
|
||||||
const rows = column
|
|
||||||
.find('.Paragraph')
|
|
||||||
.map((i, el) => {
|
|
||||||
return $(el).html()
|
|
||||||
})
|
|
||||||
.toArray()
|
|
||||||
.map(r => (r === ' ' ? '|' : r))
|
|
||||||
.join(' ')
|
|
||||||
.split('|')
|
|
||||||
|
|
||||||
const items = []
|
|
||||||
rows.forEach(row => {
|
|
||||||
row = row.trim()
|
|
||||||
if (row) {
|
|
||||||
const found = row.match(/(\d+(|:\d+)(a|p)m-\d+(|:\d+)(a|p)m)/gi)
|
|
||||||
if (!found) return
|
|
||||||
const time = found[0]
|
|
||||||
let start = time.match(/(\d+(|:\d+)(a|p)m)-/i)[1]
|
|
||||||
start = dayjs(start.toUpperCase(), ['hh:mmA', 'h:mmA', 'hA']).format('hh:mm A')
|
|
||||||
let end = time.match(/-(\d+(|:\d+)(a|p)m)/i)[1]
|
|
||||||
end = dayjs(end.toUpperCase(), ['hh:mmA', 'h:mmA', 'hA']).format('hh:mm A')
|
|
||||||
const title = row.replace(time, '').replace(' ', '').trim()
|
|
||||||
items.push({ start, end, title })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# arianaafgtv.com
|
|
||||||
|
|
||||||
https://arianaafgtv.com/#ariana-afghanistan-television-tv-guide
|
|
||||||
|
|
||||||
### Download the guide
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run grab --- --site=arianaafgtv.com
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm test --- arianaafgtv.com
|
|
||||||
```
|
|
||||||
1
sites/arianatelevision.com/__data__/content.html
Normal file
1
sites/arianatelevision.com/__data__/content.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!DOCTYPE html><html><head></head><body><textarea data-jtrt-table-id="508" id="jtrt_table_settings_508" cols="30" rows="10">[[["Start","Saturday","Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","",""],["7:00","City Report","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","",""],["7:30","ICC T20 Highlights","Sport ","Sport ","Sport ","Sport ","Sport ","Sport ","",""],["15:00","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","",""],["6:30","Quran and Hadis ","Falah","Falah","Falah","Falah","Falah","Falah","",""],["","\\n","","","","","","","",""]]]</textarea></body></html>
|
||||||
1
sites/arianatelevision.com/__data__/no_content.html
Normal file
1
sites/arianatelevision.com/__data__/no_content.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!DOCTYPE html><html><head></head><body><textarea data-jtrt-table-id="508" id="jtrt_table_settings_508" cols="30" rows="10"></textarea></body></html>
|
||||||
@@ -1,60 +1,60 @@
|
|||||||
const cheerio = require('cheerio')
|
const cheerio = require('cheerio')
|
||||||
const { DateTime } = require('luxon')
|
const { DateTime } = require('luxon')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
site: 'arianatelevision.com',
|
site: 'arianatelevision.com',
|
||||||
days: 2,
|
days: 2,
|
||||||
url: 'https://www.arianatelevision.com/program-schedule/',
|
url: 'https://www.arianatelevision.com/program-schedule/',
|
||||||
parser({ content, date }) {
|
parser({ content, date }) {
|
||||||
const programs = []
|
const programs = []
|
||||||
const items = parseItems(content, date)
|
const items = parseItems(content, date)
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
const prev = programs[programs.length - 1]
|
const prev = programs[programs.length - 1]
|
||||||
let start = parseStart(item, date)
|
let start = parseStart(item, date)
|
||||||
if (prev) {
|
if (prev) {
|
||||||
if (start < prev.start) {
|
if (start < prev.start) {
|
||||||
start = start.plus({ days: 1 })
|
start = start.plus({ days: 1 })
|
||||||
date = date.add(1, 'd')
|
date = date.add(1, 'd')
|
||||||
}
|
}
|
||||||
prev.stop = start
|
prev.stop = start
|
||||||
}
|
}
|
||||||
const stop = start.plus({ minutes: 30 })
|
const stop = start.plus({ minutes: 30 })
|
||||||
programs.push({
|
programs.push({
|
||||||
title: item.title,
|
title: item.title,
|
||||||
start,
|
start,
|
||||||
stop
|
stop
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return programs
|
return programs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseStart(item, date) {
|
function parseStart(item, date) {
|
||||||
const time = `${date.format('YYYY-MM-DD')} ${item.start}`
|
const time = `${date.format('YYYY-MM-DD')} ${item.start}`
|
||||||
|
|
||||||
return DateTime.fromFormat(time, 'yyyy-MM-dd H:mm', { zone: 'Asia/Kabul' }).toUTC()
|
return DateTime.fromFormat(time, 'yyyy-MM-dd H:mm', { zone: 'Asia/Kabul' }).toUTC()
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseItems(content, date) {
|
function parseItems(content, date) {
|
||||||
const $ = cheerio.load(content)
|
const $ = cheerio.load(content)
|
||||||
const settings = $('#jtrt_table_settings_508').text()
|
const settings = $('#jtrt_table_settings_508').text()
|
||||||
if (!settings) return []
|
if (!settings) return []
|
||||||
const data = JSON.parse(settings)
|
const data = JSON.parse(settings)
|
||||||
if (!data || !Array.isArray(data)) return []
|
if (!data || !Array.isArray(data)) return []
|
||||||
|
|
||||||
let rows = data[0]
|
let rows = data[0]
|
||||||
rows.shift()
|
rows.shift()
|
||||||
const output = []
|
const output = []
|
||||||
rows.forEach(row => {
|
rows.forEach(row => {
|
||||||
let day = date.day() + 2
|
let day = date.day() + 2
|
||||||
if (day > 7) day = 1
|
if (day > 7) day = 1
|
||||||
if (!row[0] || !row[day]) return
|
if (!row[0] || !row[day]) return
|
||||||
output.push({
|
output.push({
|
||||||
start: row[0].trim(),
|
start: row[0].trim(),
|
||||||
title: row[day].trim()
|
title: row[day].trim()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +1,59 @@
|
|||||||
const { parser, url } = require('./arianatelevision.com.config.js')
|
const { parser, url } = require('./arianatelevision.com.config.js')
|
||||||
const dayjs = require('dayjs')
|
const fs = require('fs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const path = require('path')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const dayjs = require('dayjs')
|
||||||
dayjs.extend(customParseFormat)
|
const utc = require('dayjs/plugin/utc')
|
||||||
dayjs.extend(utc)
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
|
dayjs.extend(customParseFormat)
|
||||||
const date = dayjs.utc('2021-11-27', 'YYYY-MM-DD').startOf('d')
|
dayjs.extend(utc)
|
||||||
const channel = {
|
|
||||||
site_id: '#',
|
const date = dayjs.utc('2021-11-27', 'YYYY-MM-DD').startOf('d')
|
||||||
xmltv_id: 'ArianaTVNational.af'
|
const channel = {
|
||||||
}
|
site_id: '#',
|
||||||
|
xmltv_id: 'ArianaTVNational.af'
|
||||||
it('can generate valid url', () => {
|
}
|
||||||
expect(url).toBe('https://www.arianatelevision.com/program-schedule/')
|
|
||||||
})
|
it('can generate valid url', () => {
|
||||||
|
expect(url).toBe('https://www.arianatelevision.com/program-schedule/')
|
||||||
it('can parse response', () => {
|
})
|
||||||
const content =
|
|
||||||
'<!DOCTYPE html><html><head></head><body><textarea data-jtrt-table-id="508" id="jtrt_table_settings_508" cols="30" rows="10">[[["Start","Saturday","Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","",""],["7:00","City Report","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","",""],["7:30","ICC T20 Highlights","Sport ","Sport ","Sport ","Sport ","Sport ","Sport ","",""],["15:00","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","",""],["6:30","Quran and Hadis ","Falah","Falah","Falah","Falah","Falah","Falah","",""],["","\\n","","","","","","","",""]]]</textarea></body></html>'
|
it('can parse response', () => {
|
||||||
const result = parser({ content, date }).map(p => {
|
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
|
||||||
p.start = p.start.toJSON()
|
const result = parser({ content, date }).map(p => {
|
||||||
p.stop = p.stop.toJSON()
|
p.start = p.start.toJSON()
|
||||||
return p
|
p.stop = p.stop.toJSON()
|
||||||
})
|
return p
|
||||||
|
})
|
||||||
expect(result).toMatchObject([
|
|
||||||
{
|
expect(result).toMatchObject([
|
||||||
start: '2021-11-27T02:30:00.000Z',
|
{
|
||||||
stop: '2021-11-27T03:00:00.000Z',
|
start: '2021-11-27T02:30:00.000Z',
|
||||||
title: 'City Report'
|
stop: '2021-11-27T03:00:00.000Z',
|
||||||
},
|
title: 'City Report'
|
||||||
{
|
},
|
||||||
start: '2021-11-27T03:00:00.000Z',
|
{
|
||||||
stop: '2021-11-27T10:30:00.000Z',
|
start: '2021-11-27T03:00:00.000Z',
|
||||||
title: 'ICC T20 Highlights'
|
stop: '2021-11-27T10:30:00.000Z',
|
||||||
},
|
title: 'ICC T20 Highlights'
|
||||||
{
|
},
|
||||||
start: '2021-11-27T10:30:00.000Z',
|
{
|
||||||
stop: '2021-11-28T02:00:00.000Z',
|
start: '2021-11-27T10:30:00.000Z',
|
||||||
title: 'ICC T20 World Cup'
|
stop: '2021-11-28T02:00:00.000Z',
|
||||||
},
|
title: 'ICC T20 World Cup'
|
||||||
{
|
},
|
||||||
start: '2021-11-28T02:00:00.000Z',
|
{
|
||||||
stop: '2021-11-28T02:30:00.000Z',
|
start: '2021-11-28T02:00:00.000Z',
|
||||||
title: 'Quran and Hadis'
|
stop: '2021-11-28T02:30:00.000Z',
|
||||||
}
|
title: 'Quran and Hadis'
|
||||||
])
|
}
|
||||||
})
|
])
|
||||||
|
})
|
||||||
it('can handle empty guide', () => {
|
|
||||||
const result = parser({
|
it('can handle empty guide', () => {
|
||||||
date,
|
const result = parser({
|
||||||
channel,
|
date,
|
||||||
content:
|
channel,
|
||||||
'<!DOCTYPE html><html><head></head><body><textarea data-jtrt-table-id="508" id="jtrt_table_settings_508" cols="30" rows="10"></textarea></body></html>'
|
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
|
||||||
})
|
})
|
||||||
expect(result).toMatchObject([])
|
expect(result).toMatchObject([])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,163 +1,163 @@
|
|||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require('dayjs/plugin/utc')
|
||||||
const timezone = require('dayjs/plugin/timezone')
|
const timezone = require('dayjs/plugin/timezone')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
site: 'arirang.com',
|
site: 'arirang.com',
|
||||||
output: 'arirang.com.guide.xml',
|
output: 'arirang.com.guide.xml',
|
||||||
channels: 'arirang.com.channels.xml',
|
channels: 'arirang.com.channels.xml',
|
||||||
lang: 'en',
|
lang: 'en',
|
||||||
days: 7,
|
days: 7,
|
||||||
delay: 5000,
|
delay: 5000,
|
||||||
url: 'https://www.arirang.com/v1.0/open/external/proxy',
|
url: 'https://www.arirang.com/v1.0/open/external/proxy',
|
||||||
|
|
||||||
request: {
|
request: {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
cache: { ttl: 60 * 60 * 1000 },
|
cache: { ttl: 60 * 60 * 1000 },
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json, text/plain, */*',
|
Accept: 'application/json, text/plain, */*',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Origin: 'https://www.arirang.com',
|
Origin: 'https://www.arirang.com',
|
||||||
Referer: 'https://www.arirang.com/schedule',
|
Referer: 'https://www.arirang.com/schedule',
|
||||||
'User-Agent':
|
'User-Agent':
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36'
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36'
|
||||||
},
|
},
|
||||||
data: function (context) {
|
data: function (context) {
|
||||||
const { channel, date } = context
|
const { channel, date } = context
|
||||||
return {
|
return {
|
||||||
address: 'https://script.arirang.com/api/v1/bis/listScheduleV3.do',
|
address: 'https://script.arirang.com/api/v1/bis/listScheduleV3.do',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {},
|
headers: {},
|
||||||
body: {
|
body: {
|
||||||
data: {
|
data: {
|
||||||
dmParam: {
|
dmParam: {
|
||||||
chanId: channel.site_id,
|
chanId: channel.site_id,
|
||||||
broadYmd: dayjs.tz(date, 'Asia/Seoul').format('YYYYMMDD'),
|
broadYmd: dayjs.tz(date, 'Asia/Seoul').format('YYYYMMDD'),
|
||||||
planNo: '1'
|
planNo: '1'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
logo: function (context) {
|
logo: function (context) {
|
||||||
return context.channel.logo
|
return context.channel.logo
|
||||||
},
|
},
|
||||||
|
|
||||||
async parser(context) {
|
async parser(context) {
|
||||||
const programs = []
|
const programs = []
|
||||||
const items = parseItems(context.content)
|
const items = parseItems(context.content)
|
||||||
|
|
||||||
for (let item of items) {
|
for (let item of items) {
|
||||||
const programDetail = await parseProgramDetail(item)
|
const programDetail = await parseProgramDetail(item)
|
||||||
|
|
||||||
programs.push({
|
programs.push({
|
||||||
title: parseTitle(programDetail),
|
title: parseTitle(programDetail),
|
||||||
start: parseStart(item),
|
start: parseStart(item),
|
||||||
stop: parseStop(item),
|
stop: parseStop(item),
|
||||||
image: parseImage(programDetail),
|
image: parseImage(programDetail),
|
||||||
category: parseCategory(programDetail),
|
category: parseCategory(programDetail),
|
||||||
description: parseDescription(programDetail)
|
description: parseDescription(programDetail)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return programs
|
return programs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseItems(content) {
|
function parseItems(content) {
|
||||||
if (content != '') {
|
if (content != '') {
|
||||||
const data = JSON.parse(content)
|
const data = JSON.parse(content)
|
||||||
return !data || !data.responseBody || !Array.isArray(data.responseBody.dsSchWeek)
|
return !data || !data.responseBody || !Array.isArray(data.responseBody.dsSchWeek)
|
||||||
? []
|
? []
|
||||||
: data.responseBody.dsSchWeek
|
: data.responseBody.dsSchWeek
|
||||||
} else {
|
} else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseStart(item) {
|
function parseStart(item) {
|
||||||
return dayjs.tz(item.broadYmd + ' ' + item.broadHm, 'YYYYMMDD HHmm', 'Asia/Seoul')
|
return dayjs.tz(item.broadYmd + ' ' + item.broadHm, 'YYYYMMDD HHmm', 'Asia/Seoul')
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseStop(item) {
|
function parseStop(item) {
|
||||||
return dayjs
|
return dayjs
|
||||||
.tz(item.broadYmd + ' ' + item.broadHm, 'YYYYMMDD HHmm', 'Asia/Seoul')
|
.tz(item.broadYmd + ' ' + item.broadHm, 'YYYYMMDD HHmm', 'Asia/Seoul')
|
||||||
.add(item.broadRun, 'minute')
|
.add(item.broadRun, 'minute')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function parseProgramDetail(item) {
|
async function parseProgramDetail(item) {
|
||||||
return axios
|
return axios
|
||||||
.post(
|
.post(
|
||||||
'https://www.arirang.com/v1.0/open/program/detail',
|
'https://www.arirang.com/v1.0/open/program/detail',
|
||||||
{
|
{
|
||||||
bis_program_code: item.pgmCd
|
bis_program_code: item.pgmCd
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json, text/plain, */*',
|
Accept: 'application/json, text/plain, */*',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Origin: 'https://www.arirang.com',
|
Origin: 'https://www.arirang.com',
|
||||||
Referer: 'https://www.arirang.com/schedule',
|
Referer: 'https://www.arirang.com/schedule',
|
||||||
'User-Agent':
|
'User-Agent':
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36'
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36'
|
||||||
},
|
},
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
cache: { ttl: 60 * 1000 }
|
cache: { ttl: 60 * 1000 }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
// console.log('Retrieved program detail: bis_program_code ' + item.pgmCd)
|
// console.log('Retrieved program detail: bis_program_code ' + item.pgmCd)
|
||||||
return response.data
|
return response.data
|
||||||
})
|
})
|
||||||
.catch(function () {
|
.catch(function () {
|
||||||
// The provider/server may not have details on every single programs.
|
// The provider/server may not have details on every single programs.
|
||||||
// console.log('Unavailable program detail: bis_program_code ' + item.pgmCd)
|
// console.log('Unavailable program detail: bis_program_code ' + item.pgmCd)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTitle(programDetail) {
|
function parseTitle(programDetail) {
|
||||||
if (programDetail && programDetail.title && programDetail.title[0] && programDetail.title[0].text) {
|
if (programDetail && programDetail.title && programDetail.title[0] && programDetail.title[0].text) {
|
||||||
return programDetail.title[0].text
|
return programDetail.title[0].text
|
||||||
} else {
|
} else {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseImage(programDetail) {
|
function parseImage(programDetail) {
|
||||||
if (programDetail && programDetail.image && programDetail.image[0].url) {
|
if (programDetail && programDetail.image && programDetail.image[0].url) {
|
||||||
return programDetail.image[0].url
|
return programDetail.image[0].url
|
||||||
} else {
|
} else {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCategory(programDetail) {
|
function parseCategory(programDetail) {
|
||||||
if (programDetail && programDetail.category_Info && programDetail.category_Info[0].title) {
|
if (programDetail && programDetail.category_Info && programDetail.category_Info[0].title) {
|
||||||
return programDetail.category_Info[0].title
|
return programDetail.category_Info[0].title
|
||||||
} else {
|
} else {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDescription(programDetail) {
|
function parseDescription(programDetail) {
|
||||||
if (
|
if (
|
||||||
programDetail &&
|
programDetail &&
|
||||||
programDetail.content &&
|
programDetail.content &&
|
||||||
programDetail.content[0] &&
|
programDetail.content[0] &&
|
||||||
programDetail.content[0].text
|
programDetail.content[0].text
|
||||||
) {
|
) {
|
||||||
let description = programDetail.content[0].text
|
let description = programDetail.content[0].text
|
||||||
let regex = /(<([^>]+)>)/gi
|
let regex = /(<([^>]+)>)/gi
|
||||||
return description.replace(regex, '')
|
return description.replace(regex, '')
|
||||||
} else {
|
} else {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,72 +1,72 @@
|
|||||||
const { url, parser } = require('./arirang.com.config.js')
|
const { url, parser } = require('./arirang.com.config.js')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require('dayjs/plugin/utc')
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
|
|
||||||
jest.mock('axios')
|
jest.mock('axios')
|
||||||
|
|
||||||
const date = dayjs.tz('2025-04-20', 'Asia/Seoul').startOf('d')
|
const date = dayjs.tz('2025-04-20', 'Asia/Seoul').startOf('d')
|
||||||
const channel = {
|
const channel = {
|
||||||
xmltv_id: 'ArirangWorld.kr',
|
xmltv_id: 'ArirangWorld.kr',
|
||||||
site_id: 'CH_W',
|
site_id: 'CH_W',
|
||||||
name: 'Arirang World',
|
name: 'Arirang World',
|
||||||
lang: 'en',
|
lang: 'en',
|
||||||
logo: 'https://i.imgur.com/5Aoithj.png'
|
logo: 'https://i.imgur.com/5Aoithj.png'
|
||||||
}
|
}
|
||||||
const content = fs.readFileSync(path.resolve(__dirname, '__data__/schedule.json'), 'utf8')
|
const content = fs.readFileSync(path.resolve(__dirname, '__data__/schedule.json'), 'utf8')
|
||||||
const programDetail = fs.readFileSync(path.resolve(__dirname, '__data__/detail.json'), 'utf8')
|
const programDetail = fs.readFileSync(path.resolve(__dirname, '__data__/detail.json'), 'utf8')
|
||||||
const context = { channel: channel, content: content, date: date }
|
const context = { channel: channel, content: content, date: date }
|
||||||
|
|
||||||
it('can generate valid url', () => {
|
it('can generate valid url', () => {
|
||||||
expect(url).toBe('https://www.arirang.com/v1.0/open/external/proxy')
|
expect(url).toBe('https://www.arirang.com/v1.0/open/external/proxy')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can handle empty guide', async () => {
|
it('can handle empty guide', async () => {
|
||||||
const results = await parser({ channel: channel, content: '', date: date })
|
const results = await parser({ channel: channel, content: '', date: date })
|
||||||
expect(results).toMatchObject([])
|
expect(results).toMatchObject([])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can parse response', async () => {
|
it('can parse response', async () => {
|
||||||
axios.post.mockImplementation((url, data) => {
|
axios.post.mockImplementation((url, data) => {
|
||||||
if (
|
if (
|
||||||
url === 'https://www.arirang.com/v1.0/open/external/proxy' &&
|
url === 'https://www.arirang.com/v1.0/open/external/proxy' &&
|
||||||
JSON.stringify(data) ===
|
JSON.stringify(data) ===
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
address: 'https://script.arirang.com/api/v1/bis/listScheduleV3.do',
|
address: 'https://script.arirang.com/api/v1/bis/listScheduleV3.do',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {},
|
headers: {},
|
||||||
body: { data: { dmParam: { chanId: 'CH_W', broadYmd: '20250420', planNo: '1' } } }
|
body: { data: { dmParam: { chanId: 'CH_W', broadYmd: '20250420', planNo: '1' } } }
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
data: JSON.parse(content)
|
data: JSON.parse(content)
|
||||||
})
|
})
|
||||||
} else if (
|
} else if (
|
||||||
url === 'https://www.arirang.com/v1.0/open/program/detail' &&
|
url === 'https://www.arirang.com/v1.0/open/program/detail' &&
|
||||||
JSON.stringify(data) === JSON.stringify({ bis_program_code: '2025006T' })
|
JSON.stringify(data) === JSON.stringify({ bis_program_code: '2025006T' })
|
||||||
) {
|
) {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
data: JSON.parse(programDetail)
|
data: JSON.parse(programDetail)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
data: ''
|
data: ''
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const results = await parser(context)
|
const results = await parser(context)
|
||||||
|
|
||||||
expect(results[0]).toMatchObject({
|
expect(results[0]).toMatchObject({
|
||||||
title: 'Diplomat Archives: Hidden Stories',
|
title: 'Diplomat Archives: Hidden Stories',
|
||||||
start: dayjs.tz(date, 'Asia/Seoul'),
|
start: dayjs.tz(date, 'Asia/Seoul'),
|
||||||
stop: dayjs.tz(date, 'Asia/Seoul').add(30, 'minute'),
|
stop: dayjs.tz(date, 'Asia/Seoul').add(30, 'minute'),
|
||||||
image:
|
image:
|
||||||
'https://img.arirang.com/v1/AUTH_d52449c16d3b4bbca17d4fffd9fc44af/public/images/202504/2985531324875408146.jpg',
|
'https://img.arirang.com/v1/AUTH_d52449c16d3b4bbca17d4fffd9fc44af/public/images/202504/2985531324875408146.jpg',
|
||||||
description: 'As of April 2025, S. Korea has established diplomatic relations with a total of 194 countries.\nAmong them are countries that have had ties and exchanges with Korea for hundreds of years.\nWith such long-standing relationships with so many nations,\nmight there be fascinating hidden stories between Korea and the rest of the world that we don’t know yet? \n\n"Diplomat’s Archives: Hidden Stories" begins with this very question.\nTogether with foreign embassies in Korea, the series uncovers and sheds light on meaningful yet lesser-known stories between Korea and other countries.\nThrough this, we aim to reaffirm the deep friendships that have been built over time, highlight how countries are interconnected—bilaterally and multilaterally—\nand emphasize the importance of cooperation on the global stage today.',
|
description: 'As of April 2025, S. Korea has established diplomatic relations with a total of 194 countries.\nAmong them are countries that have had ties and exchanges with Korea for hundreds of years.\nWith such long-standing relationships with so many nations,\nmight there be fascinating hidden stories between Korea and the rest of the world that we don’t know yet? \n\n"Diplomat’s Archives: Hidden Stories" begins with this very question.\nTogether with foreign embassies in Korea, the series uncovers and sheds light on meaningful yet lesser-known stories between Korea and other countries.\nThrough this, we aim to reaffirm the deep friendships that have been built over time, highlight how countries are interconnected—bilaterally and multilaterally—\nand emphasize the importance of cooperation on the global stage today.',
|
||||||
category: 'Current Affairs'
|
category: 'Current Affairs'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
1
sites/artonline.tv/__data__/content.json
Normal file
1
sites/artonline.tv/__data__/content.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[{"id":158963,"eventid":null,"duration":"01:34:00","lang":"Arabic","title":"الراقصه و السياسي","description":"تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .","thumbnail":"/UploadImages/Channel/ARTAFLAM1/03/AlRaqesaWaAlSeyasi.jpg","image":"0","start_Time":"00:30","adddate":"3/4/2022 12:00:00 AM","repeat1":null,"iD_genre":0,"iD_Show_Type":0,"iD_Channel":77,"iD_country":0,"iD_rating":0,"end_time":"02:04","season_Number":0,"epoisode_Number":0,"hasCatchup":0,"cmsid":0,"containerID":0,"imagePath":"../../UploadImages/Channel/ARTAFLAM1/3/","youtube":"0","published_at":"0","directed_by":"0","composition":"0","cast":"0","timeShow":null,"short_description":"تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .","seOdescription":null,"tagseo":null,"channel_name":null,"pathimage":null,"pathThumbnail":null}]
|
||||||
@@ -1,70 +1,70 @@
|
|||||||
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0
|
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0
|
||||||
|
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
const timezone = require('dayjs/plugin/timezone')
|
const timezone = require('dayjs/plugin/timezone')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require('dayjs/plugin/utc')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
|
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
site: 'artonline.tv',
|
site: 'artonline.tv',
|
||||||
days: 2,
|
days: 2,
|
||||||
url: function ({ channel }) {
|
url: function ({ channel }) {
|
||||||
const [, site_id] = channel.site_id.split('#')
|
const [, site_id] = channel.site_id.split('#')
|
||||||
|
|
||||||
return `https://www.artonline.tv/Home/Tvlist${site_id}`
|
return `https://www.artonline.tv/Home/Tvlist${site_id}`
|
||||||
},
|
},
|
||||||
request: {
|
request: {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'content-type': 'application/x-www-form-urlencoded'
|
'content-type': 'application/x-www-form-urlencoded'
|
||||||
},
|
},
|
||||||
data: function ({ date }) {
|
data: function ({ date }) {
|
||||||
const diff = date.diff(dayjs.utc().startOf('d'), 'd')
|
const diff = date.diff(dayjs.utc().startOf('d'), 'd')
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
params.append('objId', diff)
|
params.append('objId', diff)
|
||||||
|
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
parser: function ({ content }) {
|
parser: function ({ content }) {
|
||||||
const programs = []
|
const programs = []
|
||||||
if (!content) return programs
|
if (!content) return programs
|
||||||
const items = JSON.parse(content)
|
const items = JSON.parse(content)
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
const image = parseImage(item)
|
const image = parseImage(item)
|
||||||
const start = parseStart(item)
|
const start = parseStart(item)
|
||||||
const duration = parseDuration(item)
|
const duration = parseDuration(item)
|
||||||
const stop = start.add(duration, 's')
|
const stop = start.add(duration, 's')
|
||||||
programs.push({
|
programs.push({
|
||||||
title: item.title,
|
title: item.title,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
image,
|
image,
|
||||||
start,
|
start,
|
||||||
stop
|
stop
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return programs
|
return programs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseStart(item) {
|
function parseStart(item) {
|
||||||
const [, M, D, YYYY] = item.adddate.match(/(\d+)\/(\d+)\/(\d+) /)
|
const [, M, D, YYYY] = item.adddate.match(/(\d+)\/(\d+)\/(\d+) /)
|
||||||
const [HH, mm] = item.start_Time.split(':')
|
const [HH, mm] = item.start_Time.split(':')
|
||||||
|
|
||||||
return dayjs.tz(`${YYYY}-${M}-${D}T${HH}:${mm}:00`, 'YYYY-M-DTHH:mm:ss', 'Asia/Riyadh')
|
return dayjs.tz(`${YYYY}-${M}-${D}T${HH}:${mm}:00`, 'YYYY-M-DTHH:mm:ss', 'Asia/Riyadh')
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDuration(item) {
|
function parseDuration(item) {
|
||||||
const [, HH, mm, ss] = item.duration.match(/(\d+):(\d+):(\d+)/)
|
const [, HH, mm, ss] = item.duration.match(/(\d+):(\d+):(\d+)/)
|
||||||
|
|
||||||
return parseInt(HH) * 3600 + parseInt(mm) * 60 + parseInt(ss)
|
return parseInt(HH) * 3600 + parseInt(mm) * 60 + parseInt(ss)
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseImage(item) {
|
function parseImage(item) {
|
||||||
return item.thumbnail ? `https://www.artonline.tv${item.thumbnail}` : null
|
return item.thumbnail ? `https://www.artonline.tv${item.thumbnail}` : null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +1,66 @@
|
|||||||
const { parser, url, request } = require('./artonline.tv.config.js')
|
const { parser, url, request } = require('./artonline.tv.config.js')
|
||||||
const dayjs = require('dayjs')
|
const fs = require('fs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const path = require('path')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const dayjs = require('dayjs')
|
||||||
dayjs.extend(customParseFormat)
|
const utc = require('dayjs/plugin/utc')
|
||||||
dayjs.extend(utc)
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
|
dayjs.extend(customParseFormat)
|
||||||
const channel = {
|
dayjs.extend(utc)
|
||||||
site_id: '#Aflam2',
|
|
||||||
xmltv_id: 'ARTAflam2.sa'
|
const channel = {
|
||||||
}
|
site_id: '#Aflam2',
|
||||||
|
xmltv_id: 'ARTAflam2.sa'
|
||||||
it('can generate valid url', () => {
|
}
|
||||||
expect(url({ channel })).toBe('https://www.artonline.tv/Home/TvlistAflam2')
|
|
||||||
})
|
it('can generate valid url', () => {
|
||||||
|
expect(url({ channel })).toBe('https://www.artonline.tv/Home/TvlistAflam2')
|
||||||
it('can generate valid request method', () => {
|
})
|
||||||
expect(request.method).toBe('POST')
|
|
||||||
})
|
it('can generate valid request method', () => {
|
||||||
|
expect(request.method).toBe('POST')
|
||||||
it('can generate valid request headers', () => {
|
})
|
||||||
expect(request.headers).toMatchObject({
|
|
||||||
'content-type': 'application/x-www-form-urlencoded'
|
it('can generate valid request headers', () => {
|
||||||
})
|
expect(request.headers).toMatchObject({
|
||||||
})
|
'content-type': 'application/x-www-form-urlencoded'
|
||||||
|
})
|
||||||
it('can generate valid request data for today', () => {
|
})
|
||||||
const date = dayjs.utc().startOf('d')
|
|
||||||
const data = request.data({ date })
|
it('can generate valid request data for today', () => {
|
||||||
expect(data.get('objId')).toBe('0')
|
const date = dayjs.utc().startOf('d')
|
||||||
})
|
const data = request.data({ date })
|
||||||
|
expect(data.get('objId')).toBe('0')
|
||||||
it('can generate valid request data for tomorrow', () => {
|
})
|
||||||
const date = dayjs.utc().startOf('d').add(1, 'd')
|
|
||||||
const data = request.data({ date })
|
it('can generate valid request data for tomorrow', () => {
|
||||||
expect(data.get('objId')).toBe('1')
|
const date = dayjs.utc().startOf('d').add(1, 'd')
|
||||||
})
|
const data = request.data({ date })
|
||||||
|
expect(data.get('objId')).toBe('1')
|
||||||
it('can parse response', () => {
|
})
|
||||||
const content =
|
|
||||||
'[{"id":158963,"eventid":null,"duration":"01:34:00","lang":"Arabic","title":"الراقصه و السياسي","description":"تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .","thumbnail":"/UploadImages/Channel/ARTAFLAM1/03/AlRaqesaWaAlSeyasi.jpg","image":"0","start_Time":"00:30","adddate":"3/4/2022 12:00:00 AM","repeat1":null,"iD_genre":0,"iD_Show_Type":0,"iD_Channel":77,"iD_country":0,"iD_rating":0,"end_time":"02:04","season_Number":0,"epoisode_Number":0,"hasCatchup":0,"cmsid":0,"containerID":0,"imagePath":"../../UploadImages/Channel/ARTAFLAM1/3/","youtube":"0","published_at":"0","directed_by":"0","composition":"0","cast":"0","timeShow":null,"short_description":"تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .","seOdescription":null,"tagseo":null,"channel_name":null,"pathimage":null,"pathThumbnail":null}]'
|
it('can parse response', () => {
|
||||||
const result = parser({ content }).map(p => {
|
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
|
||||||
p.start = p.start.toJSON()
|
const result = parser({ content }).map(p => {
|
||||||
p.stop = p.stop.toJSON()
|
p.start = p.start.toJSON()
|
||||||
return p
|
p.stop = p.stop.toJSON()
|
||||||
})
|
return p
|
||||||
|
})
|
||||||
expect(result).toMatchObject([
|
|
||||||
{
|
expect(result).toMatchObject([
|
||||||
start: '2022-03-03T21:30:00.000Z',
|
{
|
||||||
stop: '2022-03-03T23:04:00.000Z',
|
start: '2022-03-03T21:30:00.000Z',
|
||||||
title: 'الراقصه و السياسي',
|
stop: '2022-03-03T23:04:00.000Z',
|
||||||
description:
|
title: 'الراقصه و السياسي',
|
||||||
'تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .',
|
description:
|
||||||
image: 'https://www.artonline.tv/UploadImages/Channel/ARTAFLAM1/03/AlRaqesaWaAlSeyasi.jpg'
|
'تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .',
|
||||||
}
|
image: 'https://www.artonline.tv/UploadImages/Channel/ARTAFLAM1/03/AlRaqesaWaAlSeyasi.jpg'
|
||||||
])
|
}
|
||||||
})
|
])
|
||||||
|
})
|
||||||
it('can handle empty guide', () => {
|
|
||||||
const result = parser({
|
it('can handle empty guide', () => {
|
||||||
content: ''
|
const result = parser({
|
||||||
})
|
content: ''
|
||||||
expect(result).toMatchObject([])
|
})
|
||||||
})
|
expect(result).toMatchObject([])
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,86 +1,86 @@
|
|||||||
const cheerio = require('cheerio')
|
const cheerio = require('cheerio')
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const { DateTime } = require('luxon')
|
const { DateTime } = require('luxon')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
site: 'awilime.com',
|
site: 'awilime.com',
|
||||||
days: 2,
|
days: 2,
|
||||||
url({ channel, date }) {
|
url({ channel, date }) {
|
||||||
return `https://www.awilime.com/tv/napi_musor/${channel.site_id}/${date.format('YYYY_MM_DD')}`
|
return `https://www.awilime.com/tv/napi_musor/${channel.site_id}/${date.format('YYYY_MM_DD')}`
|
||||||
},
|
},
|
||||||
parser({ content, date }) {
|
parser({ content, date }) {
|
||||||
const programs = []
|
const programs = []
|
||||||
const items = parseItems(content)
|
const items = parseItems(content)
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
const prev = programs[programs.length - 1]
|
const prev = programs[programs.length - 1]
|
||||||
const $item = cheerio.load(item)
|
const $item = cheerio.load(item)
|
||||||
let start = parseStart($item, date)
|
let start = parseStart($item, date)
|
||||||
if (!start) return
|
if (!start) return
|
||||||
if (prev) {
|
if (prev) {
|
||||||
prev.stop = start
|
prev.stop = start
|
||||||
}
|
}
|
||||||
const stop = start.plus({ minute: 30 })
|
const stop = start.plus({ minute: 30 })
|
||||||
|
|
||||||
programs.push({
|
programs.push({
|
||||||
title: parseTitle($item),
|
title: parseTitle($item),
|
||||||
sub_title: parseSubTitle($item),
|
sub_title: parseSubTitle($item),
|
||||||
description: parseDescription($item),
|
description: parseDescription($item),
|
||||||
start,
|
start,
|
||||||
stop
|
stop
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return programs
|
return programs
|
||||||
},
|
},
|
||||||
async channels() {
|
async channels() {
|
||||||
const html = await axios
|
const html = await axios
|
||||||
.get('https://www.awilime.com/tv/napi_musor')
|
.get('https://www.awilime.com/tv/napi_musor')
|
||||||
.then(r => r.data)
|
.then(r => r.data)
|
||||||
.catch(console.log)
|
.catch(console.log)
|
||||||
const $ = cheerio.load(html)
|
const $ = cheerio.load(html)
|
||||||
const items = $('#body > div.tk > div > div').toArray()
|
const items = $('#body > div.tk > div > div').toArray()
|
||||||
|
|
||||||
const channels = []
|
const channels = []
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
const name = $(item).find('a').text().trim()
|
const name = $(item).find('a').text().trim()
|
||||||
const url = $(item).find('a').attr('href')
|
const url = $(item).find('a').attr('href')
|
||||||
const [, site_id] = url.match(/\/tv\/napi_musor\/(.*)/) || [null, null]
|
const [, site_id] = url.match(/\/tv\/napi_musor\/(.*)/) || [null, null]
|
||||||
if (!site_id) return
|
if (!site_id) return
|
||||||
if (channels.find(channel => channel.site_id === site_id)) return
|
if (channels.find(channel => channel.site_id === site_id)) return
|
||||||
|
|
||||||
channels.push({
|
channels.push({
|
||||||
lang: 'hu',
|
lang: 'hu',
|
||||||
site_id,
|
site_id,
|
||||||
name
|
name
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return channels
|
return channels
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTitle($item) {
|
function parseTitle($item) {
|
||||||
return $item('b > a').text().trim()
|
return $item('b > a').text().trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseSubTitle($item) {
|
function parseSubTitle($item) {
|
||||||
return $item('i').clone().children().remove('s').end().text().trim()
|
return $item('i').clone().children().remove('s').end().text().trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDescription($item) {
|
function parseDescription($item) {
|
||||||
return $item('p').text().trim()
|
return $item('p').text().trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseStart($item, date) {
|
function parseStart($item, date) {
|
||||||
let time = $item('b').clone().children().remove().end().text().trim()
|
let time = $item('b').clone().children().remove().end().text().trim()
|
||||||
if (!time || !/^\d/.test(time)) return null
|
if (!time || !/^\d/.test(time)) return null
|
||||||
time = `${date.format('YYYY-MM-DD')} ${time}`
|
time = `${date.format('YYYY-MM-DD')} ${time}`
|
||||||
|
|
||||||
return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Budapest' }).toUTC()
|
return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Budapest' }).toUTC()
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseItems(content) {
|
function parseItems(content) {
|
||||||
const $ = cheerio.load(content)
|
const $ = cheerio.load(content)
|
||||||
|
|
||||||
return $('#body > div.tdc > div.td2 > div').toArray()
|
return $('#body > div.tdc > div.td2 > div').toArray()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +1,49 @@
|
|||||||
const { parser, url } = require('./awilime.com.config.js')
|
const { parser, url } = require('./awilime.com.config.js')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require('dayjs/plugin/utc')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
|
|
||||||
const date = dayjs.utc('2024-06-26', 'YYYY-MM-DD').startOf('d')
|
const date = dayjs.utc('2024-06-26', 'YYYY-MM-DD').startOf('d')
|
||||||
const channel = {
|
const channel = {
|
||||||
site_id: 'budapest_europa_tv',
|
site_id: 'budapest_europa_tv',
|
||||||
xmltv_id: 'BudapestEuropaTelevizio.hu'
|
xmltv_id: 'BudapestEuropaTelevizio.hu'
|
||||||
}
|
}
|
||||||
|
|
||||||
it('can generate valid url', () => {
|
it('can generate valid url', () => {
|
||||||
expect(url({ channel, date })).toBe(
|
expect(url({ channel, date })).toBe(
|
||||||
'https://www.awilime.com/tv/napi_musor/budapest_europa_tv/2024_06_26'
|
'https://www.awilime.com/tv/napi_musor/budapest_europa_tv/2024_06_26'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can parse response', () => {
|
it('can parse response', () => {
|
||||||
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
|
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
|
||||||
const results = parser({ content, date }).map(p => {
|
const results = parser({ content, date }).map(p => {
|
||||||
p.start = p.start.toJSON()
|
p.start = p.start.toJSON()
|
||||||
p.stop = p.stop.toJSON()
|
p.stop = p.stop.toJSON()
|
||||||
return p
|
return p
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(results.length).toBe(15)
|
expect(results.length).toBe(15)
|
||||||
|
|
||||||
expect(results[3]).toMatchObject({
|
expect(results[3]).toMatchObject({
|
||||||
start: '2024-06-26T07:00:00.000Z',
|
start: '2024-06-26T07:00:00.000Z',
|
||||||
stop: '2024-06-26T08:00:00.000Z',
|
stop: '2024-06-26T08:00:00.000Z',
|
||||||
title: 'Ébredés',
|
title: 'Ébredés',
|
||||||
sub_title: 'Amerikai dokumentumfilm (2018)',
|
sub_title: 'Amerikai dokumentumfilm (2018)',
|
||||||
description: 'Balla Tibor misszionárius'
|
description: 'Balla Tibor misszionárius'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can handle empty guide', () => {
|
it('can handle empty guide', () => {
|
||||||
const result = parser({
|
const result = parser({
|
||||||
date,
|
date,
|
||||||
channel,
|
channel,
|
||||||
content:
|
content:
|
||||||
'<html><head><title>Object moved</title></head><body><h2>Object moved to <a href="/tv/napi_musor/budapest_europa_tv/2024_06_24">here</a>.</h2></body></html>'
|
'<html><head><title>Object moved</title></head><body><h2>Object moved to <a href="/tv/napi_musor/budapest_europa_tv/2024_06_24">here</a>.</h2></body></html>'
|
||||||
})
|
})
|
||||||
expect(result).toMatchObject([])
|
expect(result).toMatchObject([])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,110 +1,110 @@
|
|||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
const cheerio = require('cheerio')
|
const cheerio = require('cheerio')
|
||||||
const { DateTime } = require('luxon')
|
const { DateTime } = require('luxon')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
site: 'bein.com',
|
site: 'bein.com',
|
||||||
days: 2,
|
days: 2,
|
||||||
request: {
|
request: {
|
||||||
cache: {
|
cache: {
|
||||||
ttl: 60 * 60 * 1000 // 1 hour
|
ttl: 60 * 60 * 1000 // 1 hour
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
url: function ({ date, channel }) {
|
url: function ({ date, channel }) {
|
||||||
const [category] = channel.site_id.split('#')
|
const [category] = channel.site_id.split('#')
|
||||||
const postid = channel.lang === 'ar' ? '25344' : '25356'
|
const postid = channel.lang === 'ar' ? '25344' : '25356'
|
||||||
|
|
||||||
return `https://www.bein.com/${
|
return `https://www.bein.com/${
|
||||||
channel.lang
|
channel.lang
|
||||||
}/epg-ajax-template/?action=epg_fetch&category=${category}&cdate=${date.format(
|
}/epg-ajax-template/?action=epg_fetch&category=${category}&cdate=${date.format(
|
||||||
'YYYY-MM-DD'
|
'YYYY-MM-DD'
|
||||||
)}&language=${channel.lang.toUpperCase()}&loadindex=0&mins=00&offset=0&postid=${postid}&serviceidentity=bein.net`
|
)}&language=${channel.lang.toUpperCase()}&loadindex=0&mins=00&offset=0&postid=${postid}&serviceidentity=bein.net`
|
||||||
},
|
},
|
||||||
parser: function ({ content, channel, date }) {
|
parser: function ({ content, channel, date }) {
|
||||||
let programs = []
|
let programs = []
|
||||||
const items = parseItems(content, channel)
|
const items = parseItems(content, channel)
|
||||||
date = DateTime.fromMillis(date.valueOf()).minus({ days: 1 })
|
date = DateTime.fromMillis(date.valueOf()).minus({ days: 1 })
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
const $item = cheerio.load(item)
|
const $item = cheerio.load(item)
|
||||||
const title = parseTitle($item)
|
const title = parseTitle($item)
|
||||||
if (!title) return
|
if (!title) return
|
||||||
const category = parseCategory($item)
|
const category = parseCategory($item)
|
||||||
const prev = programs[programs.length - 1]
|
const prev = programs[programs.length - 1]
|
||||||
let start = parseTime($item, date)
|
let start = parseTime($item, date)
|
||||||
if (prev) {
|
if (prev) {
|
||||||
if (start < prev.start) {
|
if (start < prev.start) {
|
||||||
start = start.plus({ days: 1 })
|
start = start.plus({ days: 1 })
|
||||||
date = date.plus({ days: 1 })
|
date = date.plus({ days: 1 })
|
||||||
}
|
}
|
||||||
prev.stop = start
|
prev.stop = start
|
||||||
}
|
}
|
||||||
let stop = parseTime($item, start)
|
let stop = parseTime($item, start)
|
||||||
if (stop < start) {
|
if (stop < start) {
|
||||||
stop = stop.plus({ days: 1 })
|
stop = stop.plus({ days: 1 })
|
||||||
}
|
}
|
||||||
programs.push({
|
programs.push({
|
||||||
title,
|
title,
|
||||||
category,
|
category,
|
||||||
start,
|
start,
|
||||||
stop
|
stop
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return programs
|
return programs
|
||||||
},
|
},
|
||||||
async channels({ lang }) {
|
async channels({ lang }) {
|
||||||
const categories = ['entertainment', 'sports']
|
const categories = ['entertainment', 'sports']
|
||||||
|
|
||||||
let channels = []
|
let channels = []
|
||||||
for (let category of categories) {
|
for (let category of categories) {
|
||||||
const url = `https://www.bein.com/en/epg-ajax-template/?action=epg_fetch&offset=0&category=${category}&serviceidentity=bein.net&mins=00&cdate=${dayjs().format(
|
const url = `https://www.bein.com/en/epg-ajax-template/?action=epg_fetch&offset=0&category=${category}&serviceidentity=bein.net&mins=00&cdate=${dayjs().format(
|
||||||
'YYYY-MM-DD'
|
'YYYY-MM-DD'
|
||||||
)}&language=${lang.toUpperCase()}&postid=25356&loadindex=0`
|
)}&language=${lang.toUpperCase()}&postid=25356&loadindex=0`
|
||||||
const data = await axios
|
const data = await axios
|
||||||
.get(url)
|
.get(url)
|
||||||
.then(r => r.data)
|
.then(r => r.data)
|
||||||
.catch(console.log)
|
.catch(console.log)
|
||||||
|
|
||||||
const $ = cheerio.load(data)
|
const $ = cheerio.load(data)
|
||||||
$('.container-tvguide > div').each((i, el) => {
|
$('.container-tvguide > div').each((i, el) => {
|
||||||
const id = $(el).attr('id')
|
const id = $(el).attr('id')
|
||||||
if (!id || !/^channels_\d+/.test(id)) return
|
if (!id || !/^channels_\d+/.test(id)) return
|
||||||
const [, channelId] = id.split('_')
|
const [, channelId] = id.split('_')
|
||||||
|
|
||||||
channels.push({
|
channels.push({
|
||||||
lang,
|
lang,
|
||||||
site_id: `${category}#${channelId}`,
|
site_id: `${category}#${channelId}`,
|
||||||
name: channelId
|
name: channelId
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return channels
|
return channels
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTitle($item) {
|
function parseTitle($item) {
|
||||||
return $item('.title').text()
|
return $item('.title').text()
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCategory($item) {
|
function parseCategory($item) {
|
||||||
return $item('.format').text()
|
return $item('.format').text()
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTime($item, date) {
|
function parseTime($item, date) {
|
||||||
let [, time] = $item('.time')
|
let [, time] = $item('.time')
|
||||||
.text()
|
.text()
|
||||||
.match(/^(\d{2}:\d{2})/) || [null, null]
|
.match(/^(\d{2}:\d{2})/) || [null, null]
|
||||||
if (!time) return null
|
if (!time) return null
|
||||||
time = `${date.toFormat('yyyy-MM-dd')} ${time}`
|
time = `${date.toFormat('yyyy-MM-dd')} ${time}`
|
||||||
|
|
||||||
return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Qatar' }).toUTC()
|
return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Qatar' }).toUTC()
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseItems(content, channel) {
|
function parseItems(content, channel) {
|
||||||
const [, channelId] = channel.site_id.split('#')
|
const [, channelId] = channel.site_id.split('#')
|
||||||
const $ = cheerio.load(content)
|
const $ = cheerio.load(content)
|
||||||
|
|
||||||
return $(`#channels_${channelId} .slider > ul:first-child > li`).toArray()
|
return $(`#channels_${channelId} .slider > ul:first-child > li`).toArray()
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user