diff --git a/README.md b/README.md index c494b704..ec67ae0c 100644 --- a/README.md +++ b/README.md @@ -1,237 +1,237 @@ -# EPG [![update](https://github.com/iptv-org/epg/actions/workflows/update.yml/badge.svg)](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. - -## Table of contents - -- ✨ [Installation](#installation) -- 🚀 [Usage](#usage) -- 💫 [Update](#update) -- 🐋 [Docker](#docker) -- 📺 [Playlists](#playlists) -- 🗄 [Database](#database) -- 👨‍💻 [API](#api) -- 📚 [Resources](#resources) -- 💬 [Discussions](#discussions) -- 🛠 [Contribution](#contribution) -- 📄 [License](#license) - -## 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. - -After that open the [Console](https://en.wikipedia.org/wiki/Windows_Console) (or [Terminal]() if you have macOS) and type the following command: - -```sh -git clone --depth 1 -b master https://github.com/iptv-org/epg.git -``` - -Then navigate to the downloaded `epg` folder: - -```sh -cd epg -``` - -And install all the dependencies: - -```sh -npm install -``` - -## 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: - -```sh -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. - -You can also customize the behavior of the script using this options: - -```sh -Usage: npm run grab --- [options] - -Options: - -s, --site Name of the site to parse - -c, --channels Path to *.channels.xml file (required if the "--site" attribute is - not specified) - -o, --output Path to output file (default: "guide.xml") - -l, --lang Allows you to restrict downloading to channels in specified languages only (example: "en,id") - -t, --timeout Timeout for each request in milliseconds (default: 0) - -d, --delay Delay between request in milliseconds (default: 0) - -x, --proxy Use the specified proxy (example: "socks5://username:password@127.0.0.1:1234") - --days Number of days for which the program will be loaded (defaults to the value from the site config) - --maxConnections Number of concurrent requests (default: 1) - --gzip Specifies whether or not to create a compressed version of the guide (default: false) - --curl Display each request as CURL (default: false) -``` - -### Parallel downloading - -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 -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. - -### Use custom channel list - -Create an XML file and copy the descriptions of all the channels you need from the [/sites](sites) into it: - -```xml - - - Arirang TV - ... - -``` - -And then specify the path to that file via the `--channels` attribute: - -```sh -npm run grab --- --channels=path/to/custom.channels.xml -``` - -### 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. - -To start it, you only need to specify the necessary `grab` command and [cron expression](https://crontab.guru/): - -```sh -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. - -### 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: - -```sh -npx serve -``` - -After that, the guide will be available at the link: - -``` -http://localhost:3000/guide.xml -``` - -In addition it will be available to other devices on the same local network at the address: - -``` -http://:3000/guide.xml -``` - -For more info go to [serve](https://github.com/vercel/serve) documentation. - -## Update - -If you have downloaded the repository code according to the instructions above, then to update it will be enough to run the command: - -```sh -git pull -``` - -And then update all the dependencies: - -```sh -npm install -``` - -## Docker - -### Build an image - -```sh -docker build -t iptv-org/epg --no-cache . -``` - -### Create and run container - -```sh -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. - -From the outside, it will be available at this link: - -``` -http://localhost:3000/guide.xml -``` - -or - -``` -http://:3000/guide.xml -``` - -### Environment Variables - -To fine-tune the execution, you can pass environment variables to the container as follows: - -```sh -docker run \ --p 5000:3000 \ --v /path/to/channels.xml:/epg/channels.xml \ --e CRON_SCHEDULE="0 0,12 * * *" \ --e MAX_CONNECTIONS=10 \ --e GZIP=true \ --e CURL=true \ --e PROXY="socks5://127.0.0.1:1234" \ --e DAYS=14 \ --e TIMEOUT=5 \ --e DELAY=2 \ -iptv-org/epg -``` - -| Variable | Description | -| --------------- | ------------------------------------------------------------------------------------------------------------------ | -| CRON_SCHEDULE | A [cron expression](https://crontab.guru/) describing the schedule of the guide loadings (default: "0 0 \* \* \*") | -| MAX_CONNECTIONS | Limit on the number of concurrent requests (default: 1) | -| GZIP | Boolean value indicating whether to create a compressed version of the guide (default: false) | -| CURL | Display each request as CURL (default: false) | -| PROXY | Use the specified proxy | -| DAYS | Number of days for which the guide will be loaded (defaults to the value from the site config) | -| TIMEOUT | Timeout for each request in milliseconds (default: 0) | -| DELAY | Delay between request in milliseconds (default: 0) | - -## 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. - -## API - -The API documentation can be found in the [iptv-org/api](https://github.com/iptv-org/api) repository. - -## 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. - -## Discussions - -If you have a question or an idea, you can post it in the [Discussions](https://github.com/orgs/iptv-org/discussions) tab. - -## 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). - -And thank you to everyone who has already contributed! - -### Backers - - - -### Contributors - - - -## License - -[![CC0](http://mirrors.creativecommons.org/presskit/buttons/88x31/svg/cc-zero.svg)](LICENSE) +# EPG [![update](https://github.com/iptv-org/epg/actions/workflows/update.yml/badge.svg)](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. + +## Table of contents + +- ✨ [Installation](#installation) +- 🚀 [Usage](#usage) +- 💫 [Update](#update) +- 🐋 [Docker](#docker) +- 📺 [Playlists](#playlists) +- 🗄 [Database](#database) +- 👨‍💻 [API](#api) +- 📚 [Resources](#resources) +- 💬 [Discussions](#discussions) +- 🛠 [Contribution](#contribution) +- 📄 [License](#license) + +## 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. + +After that open the [Console](https://en.wikipedia.org/wiki/Windows_Console) (or [Terminal]() if you have macOS) and type the following command: + +```sh +git clone --depth 1 -b master https://github.com/iptv-org/epg.git +``` + +Then navigate to the downloaded `epg` folder: + +```sh +cd epg +``` + +And install all the dependencies: + +```sh +npm install +``` + +## 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: + +```sh +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. + +You can also customize the behavior of the script using this options: + +```sh +Usage: npm run grab --- [options] + +Options: + -s, --site Name of the site to parse + -c, --channels Path to *.channels.xml file (required if the "--site" attribute is + not specified) + -o, --output Path to output file (default: "guide.xml") + -l, --lang Allows you to restrict downloading to channels in specified languages only (example: "en,id") + -t, --timeout Timeout for each request in milliseconds (default: 0) + -d, --delay Delay between request in milliseconds (default: 0) + -x, --proxy Use the specified proxy (example: "socks5://username:password@127.0.0.1:1234") + --days Number of days for which the program will be loaded (defaults to the value from the site config) + --maxConnections Number of concurrent requests (default: 1) + --gzip Specifies whether or not to create a compressed version of the guide (default: false) + --curl Display each request as CURL (default: false) +``` + +### Parallel downloading + +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 +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. + +### Use custom channel list + +Create an XML file and copy the descriptions of all the channels you need from the [/sites](sites) into it: + +```xml + + + Arirang TV + ... + +``` + +And then specify the path to that file via the `--channels` attribute: + +```sh +npm run grab --- --channels=path/to/custom.channels.xml +``` + +### 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. + +To start it, you only need to specify the necessary `grab` command and [cron expression](https://crontab.guru/): + +```sh +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. + +### 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: + +```sh +npx serve +``` + +After that, the guide will be available at the link: + +``` +http://localhost:3000/guide.xml +``` + +In addition it will be available to other devices on the same local network at the address: + +``` +http://:3000/guide.xml +``` + +For more info go to [serve](https://github.com/vercel/serve) documentation. + +## Update + +If you have downloaded the repository code according to the instructions above, then to update it will be enough to run the command: + +```sh +git pull +``` + +And then update all the dependencies: + +```sh +npm install +``` + +## Docker + +### Build an image + +```sh +docker build -t iptv-org/epg --no-cache . +``` + +### Create and run container + +```sh +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. + +From the outside, it will be available at this link: + +``` +http://localhost:3000/guide.xml +``` + +or + +``` +http://:3000/guide.xml +``` + +### Environment Variables + +To fine-tune the execution, you can pass environment variables to the container as follows: + +```sh +docker run \ +-p 5000:3000 \ +-v /path/to/channels.xml:/epg/channels.xml \ +-e CRON_SCHEDULE="0 0,12 * * *" \ +-e MAX_CONNECTIONS=10 \ +-e GZIP=true \ +-e CURL=true \ +-e PROXY="socks5://127.0.0.1:1234" \ +-e DAYS=14 \ +-e TIMEOUT=5 \ +-e DELAY=2 \ +iptv-org/epg +``` + +| Variable | Description | +| --------------- | ------------------------------------------------------------------------------------------------------------------ | +| CRON_SCHEDULE | A [cron expression](https://crontab.guru/) describing the schedule of the guide loadings (default: "0 0 \* \* \*") | +| MAX_CONNECTIONS | Limit on the number of concurrent requests (default: 1) | +| GZIP | Boolean value indicating whether to create a compressed version of the guide (default: false) | +| CURL | Display each request as CURL (default: false) | +| PROXY | Use the specified proxy | +| DAYS | Number of days for which the guide will be loaded (defaults to the value from the site config) | +| TIMEOUT | Timeout for each request in milliseconds (default: 0) | +| DELAY | Delay between request in milliseconds (default: 0) | + +## 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. + +## API + +The API documentation can be found in the [iptv-org/api](https://github.com/iptv-org/api) repository. + +## 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. + +## Discussions + +If you have a question or an idea, you can post it in the [Discussions](https://github.com/orgs/iptv-org/discussions) tab. + +## 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). + +And thank you to everyone who has already contributed! + +### Backers + + + +### Contributors + + + +## License + +[![CC0](http://mirrors.creativecommons.org/presskit/buttons/88x31/svg/cc-zero.svg)](LICENSE) diff --git a/package-lock.json b/package-lock.json index 2eaa5365..94050c79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,10 @@ "@types/inquirer": "^9.0.8", "@types/jest": "^30.0.0", "@types/langs": "^2.0.5", + "@types/lodash.orderby": "^4.6.9", + "@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/numeral": "^2.0.5", @@ -57,6 +61,10 @@ "jest-offline": "^1.0.1", "langs": "^2.0.0", "libxml2-wasm": "^0.5.0", + "lodash.orderby": "^4.6.0", + "lodash.sortby": "^4.7.0", + "lodash.startcase": "^4.4.0", + "lodash.uniqby": "^4.7.0", "luxon": "^3.7.1", "mockdate": "^3.0.5", "nedb-promises": "^6.2.3", @@ -3306,6 +3314,48 @@ "integrity": "sha512-DIUKT4mkbTBxSrX6lmnQR888ObeFVVo1uNEqBH5/ddQHpnG4CA24DibpK7aO8QAcJEZUTcIx0F96TWuzVT9Z4g==", "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "license": "MIT" + }, + "node_modules/@types/lodash.orderby": { + "version": "4.6.9", + "resolved": "https://registry.npmjs.org/@types/lodash.orderby/-/lodash.orderby-4.6.9.tgz", + "integrity": "sha512-T9o2wkIJOmxXwVTPTmwJ59W6eTi2FseiLR369fxszG649Po/xe9vqFNhf/MtnvT5jrbDiyWKxPFPZbpSVK0SVQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/lodash.sortby": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/@types/lodash.sortby/-/lodash.sortby-4.7.9.tgz", + "integrity": "sha512-PDmjHnOlndLS59GofH0pnxIs+n9i4CWeXGErSB5JyNFHu2cmvW6mQOaUKjG8EDPkni14IgF8NsRW8bKvFzTm9A==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/lodash.startcase": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@types/lodash.startcase/-/lodash.startcase-4.4.9.tgz", + "integrity": "sha512-C0M4DlN1pnn2vEEhLHkTHxiRZ+3GlTegpoAEHHGXnuJkSOXyJMHGiSc+SLRzBlFZWHsBkixe6FqvEAEU04g14g==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/lodash.uniqby": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/@types/lodash.uniqby/-/lodash.uniqby-4.7.9.tgz", + "integrity": "sha512-rjrXji/seS6BZJRgXrU2h6FqxRVufsbq/HE0Tx0SdgbtlWr2YmD/M64BlYEYYlaMcpZwy32IYVkMfUMYlPuv0w==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/node": { "version": "24.1.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", @@ -8486,6 +8536,30 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "node_modules/lodash.orderby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.orderby/-/lodash.orderby-4.6.0.tgz", + "integrity": "sha512-T0rZxKmghOOf5YPnn8EY5iLYeWCpZq8G41FfqoVHH5QDTAFaghJRmAdLiadEDq+ztgM2q5PjA+Z1fOwGrLgmtg==", + "license": "MIT" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "license": "MIT" + }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "license": "MIT" + }, + "node_modules/lodash.uniqby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", + "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==", + "license": "MIT" + }, "node_modules/logform": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", @@ -13391,6 +13465,43 @@ "resolved": "https://registry.npmjs.org/@types/langs/-/langs-2.0.5.tgz", "integrity": "sha512-DIUKT4mkbTBxSrX6lmnQR888ObeFVVo1uNEqBH5/ddQHpnG4CA24DibpK7aO8QAcJEZUTcIx0F96TWuzVT9Z4g==" }, + "@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==" + }, + "@types/lodash.orderby": { + "version": "4.6.9", + "resolved": "https://registry.npmjs.org/@types/lodash.orderby/-/lodash.orderby-4.6.9.tgz", + "integrity": "sha512-T9o2wkIJOmxXwVTPTmwJ59W6eTi2FseiLR369fxszG649Po/xe9vqFNhf/MtnvT5jrbDiyWKxPFPZbpSVK0SVQ==", + "requires": { + "@types/lodash": "*" + } + }, + "@types/lodash.sortby": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/@types/lodash.sortby/-/lodash.sortby-4.7.9.tgz", + "integrity": "sha512-PDmjHnOlndLS59GofH0pnxIs+n9i4CWeXGErSB5JyNFHu2cmvW6mQOaUKjG8EDPkni14IgF8NsRW8bKvFzTm9A==", + "requires": { + "@types/lodash": "*" + } + }, + "@types/lodash.startcase": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@types/lodash.startcase/-/lodash.startcase-4.4.9.tgz", + "integrity": "sha512-C0M4DlN1pnn2vEEhLHkTHxiRZ+3GlTegpoAEHHGXnuJkSOXyJMHGiSc+SLRzBlFZWHsBkixe6FqvEAEU04g14g==", + "requires": { + "@types/lodash": "*" + } + }, + "@types/lodash.uniqby": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/@types/lodash.uniqby/-/lodash.uniqby-4.7.9.tgz", + "integrity": "sha512-rjrXji/seS6BZJRgXrU2h6FqxRVufsbq/HE0Tx0SdgbtlWr2YmD/M64BlYEYYlaMcpZwy32IYVkMfUMYlPuv0w==", + "requires": { + "@types/lodash": "*" + } + }, "@types/node": { "version": "24.1.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", @@ -16969,6 +17080,26 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "lodash.orderby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.orderby/-/lodash.orderby-4.6.0.tgz", + "integrity": "sha512-T0rZxKmghOOf5YPnn8EY5iLYeWCpZq8G41FfqoVHH5QDTAFaghJRmAdLiadEDq+ztgM2q5PjA+Z1fOwGrLgmtg==" + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" + }, + "lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==" + }, + "lodash.uniqby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", + "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==" + }, "logform": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", diff --git a/package.json b/package.json index d7235534..c8bed9f7 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,10 @@ "@types/inquirer": "^9.0.8", "@types/jest": "^30.0.0", "@types/langs": "^2.0.5", + "@types/lodash.orderby": "^4.6.9", + "@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/numeral": "^2.0.5", @@ -85,6 +89,10 @@ "jest-offline": "^1.0.1", "langs": "^2.0.0", "libxml2-wasm": "^0.5.0", + "lodash.orderby": "^4.6.0", + "lodash.sortby": "^4.7.0", + "lodash.startcase": "^4.4.0", + "lodash.uniqby": "^4.7.0", "luxon": "^3.7.1", "mockdate": "^3.0.5", "nedb-promises": "^6.2.3", diff --git a/scripts/commands/api/generate.ts b/scripts/commands/api/generate.ts index b0e078c4..8fd2f068 100644 --- a/scripts/commands/api/generate.ts +++ b/scripts/commands/api/generate.ts @@ -1,41 +1,41 @@ -import { Logger, Collection, Storage } from '@freearhey/core' -import { SITES_DIR, API_DIR } from '../../constants' -import { GuideChannel } from '../../models' -import { ChannelsParser } from '../../core' -import epgGrabber from 'epg-grabber' -import path from 'path' - -async function main() { - const logger = new Logger() - - logger.start('staring...') - - logger.info('loading channels...') - const sitesStorage = new Storage(SITES_DIR) - const parser = new ChannelsParser({ - storage: sitesStorage - }) - - const files: string[] = await sitesStorage.list('**/*.channels.xml') - - const channels = new Collection() - for (const filepath of files) { - const channelList = await parser.parse(filepath) - - channelList.channels.forEach((data: epgGrabber.Channel) => { - channels.add(new GuideChannel(data)) - }) - } - - logger.info(`found ${channels.count()} channel(s)`) - - const output = channels.map((channel: GuideChannel) => channel.toJSON()) - - const apiStorage = new Storage(API_DIR) - const outputFilename = 'guides.json' - await apiStorage.save('guides.json', output.toJSON()) - - logger.info(`saved to "${path.join(API_DIR, outputFilename)}"`) -} - -main() +import { Logger, Collection, Storage } from '@freearhey/core' +import { SITES_DIR, API_DIR } from '../../constants' +import { GuideChannel } from '../../models' +import { ChannelsParser } from '../../core' +import epgGrabber from 'epg-grabber' +import path from 'path' + +async function main() { + const logger = new Logger() + + logger.start('staring...') + + logger.info('loading channels...') + const sitesStorage = new Storage(SITES_DIR) + const parser = new ChannelsParser({ + storage: sitesStorage + }) + + const files: string[] = await sitesStorage.list('**/*.channels.xml') + + const channels = new Collection() + for (const filepath of files) { + const channelList = await parser.parse(filepath) + + channelList.channels.forEach((data: epgGrabber.Channel) => { + channels.add(new GuideChannel(data)) + }) + } + + logger.info(`found ${channels.count()} channel(s)`) + + const output = channels.map((channel: GuideChannel) => channel.toJSON()) + + const apiStorage = new Storage(API_DIR) + const outputFilename = 'guides.json' + await apiStorage.save('guides.json', output.toJSON()) + + logger.info(`saved to "${path.join(API_DIR, outputFilename)}"`) +} + +main() diff --git a/scripts/commands/api/load.ts b/scripts/commands/api/load.ts index 7a8f753d..0e5e2e07 100644 --- a/scripts/commands/api/load.ts +++ b/scripts/commands/api/load.ts @@ -1,25 +1,25 @@ -import { DATA_DIR } from '../../constants' -import { Storage } from '@freearhey/core' -import { DataLoader } from '../../core' - -async function main() { - const storage = new Storage(DATA_DIR) - const loader = new DataLoader({ storage }) - - await Promise.all([ - loader.download('blocklist.json'), - loader.download('categories.json'), - loader.download('channels.json'), - loader.download('countries.json'), - loader.download('languages.json'), - loader.download('regions.json'), - loader.download('subdivisions.json'), - loader.download('feeds.json'), - loader.download('timezones.json'), - loader.download('guides.json'), - loader.download('streams.json'), - loader.download('logos.json') - ]) -} - -main() +import { DATA_DIR } from '../../constants' +import { Storage } from '@freearhey/core' +import { DataLoader } from '../../core' + +async function main() { + const storage = new Storage(DATA_DIR) + const loader = new DataLoader({ storage }) + + await Promise.all([ + loader.download('blocklist.json'), + loader.download('categories.json'), + loader.download('channels.json'), + loader.download('countries.json'), + loader.download('languages.json'), + loader.download('regions.json'), + loader.download('subdivisions.json'), + loader.download('feeds.json'), + loader.download('timezones.json'), + loader.download('guides.json'), + loader.download('streams.json'), + loader.download('logos.json') + ]) +} + +main() diff --git a/scripts/commands/channels/lint.mts b/scripts/commands/channels/lint.mts index 72cb003c..e1f2a4f2 100644 --- a/scripts/commands/channels/lint.mts +++ b/scripts/commands/channels/lint.mts @@ -1,109 +1,109 @@ -import chalk from 'chalk' -import { program } from 'commander' -import { Storage, File } from '@freearhey/core' -import { XmlDocument, XsdValidator, XmlValidateError, ErrorDetail } from 'libxml2-wasm' - -const xsd = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -` - -program.argument('[filepath...]', 'Path to *.channels.xml files to check').parse(process.argv) - -async function main() { - const storage = new Storage() - - let errors: ErrorDetail[] = [] - - const files = program.args.length ? program.args : await storage.list('sites/**/*.channels.xml') - for (const filepath of files) { - const file = new File(filepath) - if (file.extension() !== 'xml') continue - - const xml = await storage.load(filepath) - - let localErrors: ErrorDetail[] = [] - - try { - const schema = XmlDocument.fromString(xsd) - const validator = XsdValidator.fromDoc(schema) - const doc = XmlDocument.fromString(xml) - - validator.validate(doc) - - schema.dispose() - validator.dispose() - doc.dispose() - } catch (_error) { - const error = _error as XmlValidateError - - localErrors = localErrors.concat(error.details) - } - - xml.split('\n').forEach((line: string, lineIndex: number) => { - const found = line.match(/='/) - if (found) { - const colIndex = found.index || 0 - localErrors.push({ - line: lineIndex + 1, - col: colIndex + 1, - message: 'Single quotes cannot be used in attributes' - }) - } - }) - - if (localErrors.length) { - console.log(`\n${chalk.underline(filepath)}`) - localErrors.forEach((error: ErrorDetail) => { - const position = `${error.line}:${error.col}` - console.log(` ${chalk.gray(position.padEnd(4, ' '))} ${error.message.trim()}`) - }) - - errors = errors.concat(localErrors) - } - } - - if (errors.length) { - console.log(chalk.red(`\n${errors.length} error(s)`)) - process.exit(1) - } -} - -main() +import chalk from 'chalk' +import { program } from 'commander' +import { Storage, File } from '@freearhey/core' +import { XmlDocument, XsdValidator, XmlValidateError, ErrorDetail } from 'libxml2-wasm' + +const xsd = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +` + +program.argument('[filepath...]', 'Path to *.channels.xml files to check').parse(process.argv) + +async function main() { + const storage = new Storage() + + let errors: ErrorDetail[] = [] + + const files = program.args.length ? program.args : await storage.list('sites/**/*.channels.xml') + for (const filepath of files) { + const file = new File(filepath) + if (file.extension() !== 'xml') continue + + const xml = await storage.load(filepath) + + let localErrors: ErrorDetail[] = [] + + try { + const schema = XmlDocument.fromString(xsd) + const validator = XsdValidator.fromDoc(schema) + const doc = XmlDocument.fromString(xml) + + validator.validate(doc) + + schema.dispose() + validator.dispose() + doc.dispose() + } catch (_error) { + const error = _error as XmlValidateError + + localErrors = localErrors.concat(error.details) + } + + xml.split('\n').forEach((line: string, lineIndex: number) => { + const found = line.match(/='/) + if (found) { + const colIndex = found.index || 0 + localErrors.push({ + line: lineIndex + 1, + col: colIndex + 1, + message: 'Single quotes cannot be used in attributes' + }) + } + }) + + if (localErrors.length) { + console.log(`\n${chalk.underline(filepath)}`) + localErrors.forEach((error: ErrorDetail) => { + const position = `${error.line}:${error.col}` + console.log(` ${chalk.gray(position.padEnd(4, ' '))} ${error.message.trim()}`) + }) + + errors = errors.concat(localErrors) + } + } + + if (errors.length) { + console.log(chalk.red(`\n${errors.length} error(s)`)) + process.exit(1) + } +} + +main() diff --git a/scripts/commands/sites/update.ts b/scripts/commands/sites/update.ts index 42db9acf..e1e1ef21 100644 --- a/scripts/commands/sites/update.ts +++ b/scripts/commands/sites/update.ts @@ -1,76 +1,76 @@ -import { IssueLoader, HTMLTable, ChannelsParser } from '../../core' -import { Logger, Storage, Collection } from '@freearhey/core' -import { ChannelList, Issue, Site } from '../../models' -import { SITES_DIR, ROOT_DIR } from '../../constants' -import { Channel } from 'epg-grabber' - -async function main() { - const logger = new Logger({ level: -999 }) - const issueLoader = new IssueLoader() - const sitesStorage = new Storage(SITES_DIR) - const sites = new Collection() - - logger.info('loading channels...') - const channelsParser = new ChannelsParser({ - storage: sitesStorage - }) - - logger.info('loading list of sites') - const folders = await sitesStorage.list('*/') - - logger.info('loading issues...') - const issues = await issueLoader.load() - - logger.info('putting the data together...') - const brokenGuideReports = issues.filter(issue => - issue.labels.find((label: string) => label === 'broken guide') - ) - for (const domain of folders) { - const filteredIssues = brokenGuideReports.filter( - (issue: Issue) => domain === issue.data.get('site') - ) - - const site = new Site({ - domain, - issues: filteredIssues - }) - - const files = await sitesStorage.list(`${domain}/*.channels.xml`) - for (const filepath of files) { - const channelList: ChannelList = await channelsParser.parse(filepath) - - site.totalChannels += channelList.channels.count() - site.markedChannels += channelList.channels - .filter((channel: Channel) => channel.xmltv_id) - .count() - } - - sites.add(site) - } - - logger.info('creating sites table...') - const tableData = new Collection() - sites.forEach((site: Site) => { - tableData.add([ - { value: `${site.domain}` }, - { value: site.totalChannels, align: 'right' }, - { value: site.markedChannels, align: 'right' }, - { value: site.getStatus().emoji, align: 'center' }, - { value: site.getIssues().all().join(', ') } - ]) - }) - - logger.info('updating sites.md...') - const table = new HTMLTable(tableData.all(), [ - { name: 'Site', align: 'left' }, - { name: 'Channels
(total / with xmltv-id)', colspan: 2, align: 'left' }, - { name: 'Status', align: 'left' }, - { name: 'Notes', align: 'left' } - ]) - const rootStorage = new Storage(ROOT_DIR) - const sitesTemplate = await new Storage().load('scripts/templates/_sites.md') - const sitesContent = sitesTemplate.replace('_TABLE_', table.toString()) - await rootStorage.save('SITES.md', sitesContent) -} - -main() +import { IssueLoader, HTMLTable, ChannelsParser } from '../../core' +import { Logger, Storage, Collection } from '@freearhey/core' +import { ChannelList, Issue, Site } from '../../models' +import { SITES_DIR, ROOT_DIR } from '../../constants' +import { Channel } from 'epg-grabber' + +async function main() { + const logger = new Logger({ level: -999 }) + const issueLoader = new IssueLoader() + const sitesStorage = new Storage(SITES_DIR) + const sites = new Collection() + + logger.info('loading channels...') + const channelsParser = new ChannelsParser({ + storage: sitesStorage + }) + + logger.info('loading list of sites') + const folders = await sitesStorage.list('*/') + + logger.info('loading issues...') + const issues = await issueLoader.load() + + logger.info('putting the data together...') + const brokenGuideReports = issues.filter(issue => + issue.labels.find((label: string) => label === 'broken guide') + ) + for (const domain of folders) { + const filteredIssues = brokenGuideReports.filter( + (issue: Issue) => domain === issue.data.get('site') + ) + + const site = new Site({ + domain, + issues: filteredIssues + }) + + const files = await sitesStorage.list(`${domain}/*.channels.xml`) + for (const filepath of files) { + const channelList: ChannelList = await channelsParser.parse(filepath) + + site.totalChannels += channelList.channels.count() + site.markedChannels += channelList.channels + .filter((channel: Channel) => channel.xmltv_id) + .count() + } + + sites.add(site) + } + + logger.info('creating sites table...') + const tableData = new Collection() + sites.forEach((site: Site) => { + tableData.add([ + { value: `${site.domain}` }, + { value: site.totalChannels, align: 'right' }, + { value: site.markedChannels, align: 'right' }, + { value: site.getStatus().emoji, align: 'center' }, + { value: site.getIssues().all().join(', ') } + ]) + }) + + logger.info('updating sites.md...') + const table = new HTMLTable(tableData.all(), [ + { name: 'Site', align: 'left' }, + { name: 'Channels
(total / with xmltv-id)', colspan: 2, align: 'left' }, + { name: 'Status', align: 'left' }, + { name: 'Notes', align: 'left' } + ]) + const rootStorage = new Storage(ROOT_DIR) + const sitesTemplate = await new Storage().load('scripts/templates/_sites.md') + const sitesContent = sitesTemplate.replace('_TABLE_', table.toString()) + await rootStorage.save('SITES.md', sitesContent) +} + +main() diff --git a/scripts/core/dataLoader.ts b/scripts/core/dataLoader.ts index 3d817977..9f4c4cfb 100644 --- a/scripts/core/dataLoader.ts +++ b/scripts/core/dataLoader.ts @@ -1,103 +1,103 @@ -import type { DataLoaderProps, DataLoaderData } from '../types/dataLoader' -import cliProgress, { MultiBar } from 'cli-progress' -import { Storage } from '@freearhey/core' -import { ApiClient } from './apiClient' -import numeral from 'numeral' - -export class DataLoader { - client: ApiClient - storage: Storage - progressBar: MultiBar - - constructor(props: DataLoaderProps) { - this.client = new ApiClient() - this.storage = props.storage - this.progressBar = new cliProgress.MultiBar({ - stopOnComplete: true, - hideCursor: true, - forceRedraw: true, - barsize: 36, - format(options, params, payload) { - const filename = payload.filename.padEnd(18, ' ') - const barsize = options.barsize || 40 - const percent = (params.progress * 100).toFixed(2) - const speed = payload.speed ? numeral(payload.speed).format('0.0 b') + '/s' : 'N/A' - const total = numeral(params.total).format('0.0 b') - const completeSize = Math.round(params.progress * barsize) - const incompleteSize = barsize - completeSize - const bar = - options.barCompleteString && options.barIncompleteString - ? options.barCompleteString.substr(0, completeSize) + - options.barGlue + - options.barIncompleteString.substr(0, incompleteSize) - : '-'.repeat(barsize) - - return `${filename} [${bar}] ${percent}% | ETA: ${params.eta}s | ${total} | ${speed}` - } - }) - } - - async load(): Promise { - const [ - countries, - regions, - subdivisions, - languages, - categories, - blocklist, - channels, - feeds, - timezones, - guides, - streams, - logos - ] = await Promise.all([ - this.storage.json('countries.json'), - this.storage.json('regions.json'), - this.storage.json('subdivisions.json'), - this.storage.json('languages.json'), - this.storage.json('categories.json'), - this.storage.json('blocklist.json'), - this.storage.json('channels.json'), - this.storage.json('feeds.json'), - this.storage.json('timezones.json'), - this.storage.json('guides.json'), - this.storage.json('streams.json'), - this.storage.json('logos.json') - ]) - - return { - countries, - regions, - subdivisions, - languages, - categories, - blocklist, - channels, - feeds, - timezones, - guides, - streams, - logos - } - } - - async download(filename: string) { - if (!this.storage || !this.progressBar) return - - const stream = await this.storage.createStream(filename) - const progressBar = this.progressBar.create(0, 0, { filename }) - - this.client - .get(filename, { - responseType: 'stream', - onDownloadProgress({ total, loaded, rate }) { - if (total) progressBar.setTotal(total) - progressBar.update(loaded, { speed: rate }) - } - }) - .then(response => { - response.data.pipe(stream) - }) - } -} +import type { DataLoaderProps, DataLoaderData } from '../types/dataLoader' +import cliProgress, { MultiBar } from 'cli-progress' +import { Storage } from '@freearhey/core' +import { ApiClient } from './apiClient' +import numeral from 'numeral' + +export class DataLoader { + client: ApiClient + storage: Storage + progressBar: MultiBar + + constructor(props: DataLoaderProps) { + this.client = new ApiClient() + this.storage = props.storage + this.progressBar = new cliProgress.MultiBar({ + stopOnComplete: true, + hideCursor: true, + forceRedraw: true, + barsize: 36, + format(options, params, payload) { + const filename = payload.filename.padEnd(18, ' ') + const barsize = options.barsize || 40 + const percent = (params.progress * 100).toFixed(2) + const speed = payload.speed ? numeral(payload.speed).format('0.0 b') + '/s' : 'N/A' + const total = numeral(params.total).format('0.0 b') + const completeSize = Math.round(params.progress * barsize) + const incompleteSize = barsize - completeSize + const bar = + options.barCompleteString && options.barIncompleteString + ? options.barCompleteString.substr(0, completeSize) + + options.barGlue + + options.barIncompleteString.substr(0, incompleteSize) + : '-'.repeat(barsize) + + return `${filename} [${bar}] ${percent}% | ETA: ${params.eta}s | ${total} | ${speed}` + } + }) + } + + async load(): Promise { + const [ + countries, + regions, + subdivisions, + languages, + categories, + blocklist, + channels, + feeds, + timezones, + guides, + streams, + logos + ] = await Promise.all([ + this.storage.json('countries.json'), + this.storage.json('regions.json'), + this.storage.json('subdivisions.json'), + this.storage.json('languages.json'), + this.storage.json('categories.json'), + this.storage.json('blocklist.json'), + this.storage.json('channels.json'), + this.storage.json('feeds.json'), + this.storage.json('timezones.json'), + this.storage.json('guides.json'), + this.storage.json('streams.json'), + this.storage.json('logos.json') + ]) + + return { + countries, + regions, + subdivisions, + languages, + categories, + blocklist, + channels, + feeds, + timezones, + guides, + streams, + logos + } + } + + async download(filename: string) { + if (!this.storage || !this.progressBar) return + + const stream = await this.storage.createStream(filename) + const progressBar = this.progressBar.create(0, 0, { filename }) + + this.client + .get(filename, { + responseType: 'stream', + onDownloadProgress({ total, loaded, rate }) { + if (total) progressBar.setTotal(total) + progressBar.update(loaded, { speed: rate }) + } + }) + .then(response => { + response.data.pipe(stream) + }) + } +} diff --git a/scripts/core/index.ts b/scripts/core/index.ts index 8d528fe7..8694174a 100644 --- a/scripts/core/index.ts +++ b/scripts/core/index.ts @@ -1,14 +1,14 @@ -export * from './apiClient' -export * from './channelsParser' -export * from './configLoader' -export * from './dataLoader' -export * from './dataProcessor' -export * from './grabber' -export * from './guideManager' -export * from './htmlTable' -export * from './issueLoader' -export * from './issueParser' -export * from './job' -export * from './proxyParser' -export * from './queue' -export * from './queueCreator' +export * from './apiClient' +export * from './channelsParser' +export * from './configLoader' +export * from './dataLoader' +export * from './dataProcessor' +export * from './grabber' +export * from './guideManager' +export * from './htmlTable' +export * from './issueLoader' +export * from './issueParser' +export * from './job' +export * from './proxyParser' +export * from './queue' +export * from './queueCreator' diff --git a/scripts/core/issueLoader.ts b/scripts/core/issueLoader.ts index eebd6c39..855f99e2 100644 --- a/scripts/core/issueLoader.ts +++ b/scripts/core/issueLoader.ts @@ -1,37 +1,37 @@ -import { restEndpointMethods } from '@octokit/plugin-rest-endpoint-methods' -import { paginateRest } from '@octokit/plugin-paginate-rest' -import { TESTING, OWNER, REPO } from '../constants' -import { Collection } from '@freearhey/core' -import { Octokit } from '@octokit/core' -import { IssueParser } from './' - -const CustomOctokit = Octokit.plugin(paginateRest, restEndpointMethods) -const octokit = new CustomOctokit() - -export class IssueLoader { - async load(props?: { labels: string[] | string }) { - let labels = '' - if (props && props.labels) { - labels = Array.isArray(props.labels) ? props.labels.join(',') : props.labels - } - let issues: object[] = [] - if (TESTING) { - issues = (await import('../../tests/__data__/input/sites_update/issues.mjs')).default - } else { - issues = await octokit.paginate(octokit.rest.issues.listForRepo, { - owner: OWNER, - repo: REPO, - per_page: 100, - labels, - state: 'open', - headers: { - 'X-GitHub-Api-Version': '2022-11-28' - } - }) - } - - const parser = new IssueParser() - - return new Collection(issues).map(parser.parse) - } -} +import { restEndpointMethods } from '@octokit/plugin-rest-endpoint-methods' +import { paginateRest } from '@octokit/plugin-paginate-rest' +import { TESTING, OWNER, REPO } from '../constants' +import { Collection } from '@freearhey/core' +import { Octokit } from '@octokit/core' +import { IssueParser } from './' + +const CustomOctokit = Octokit.plugin(paginateRest, restEndpointMethods) +const octokit = new CustomOctokit() + +export class IssueLoader { + async load(props?: { labels: string[] | string }) { + let labels = '' + if (props && props.labels) { + labels = Array.isArray(props.labels) ? props.labels.join(',') : props.labels + } + let issues: object[] = [] + if (TESTING) { + issues = (await import('../../tests/__data__/input/sites_update/issues.mjs')).default + } else { + issues = await octokit.paginate(octokit.rest.issues.listForRepo, { + owner: OWNER, + repo: REPO, + per_page: 100, + labels, + state: 'open', + headers: { + 'X-GitHub-Api-Version': '2022-11-28' + } + }) + } + + const parser = new IssueParser() + + return new Collection(issues).map(parser.parse) + } +} diff --git a/scripts/functions/functions.ts b/scripts/functions/functions.ts deleted file mode 100644 index f6e0bb63..00000000 --- a/scripts/functions/functions.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Sorts an array by the result of running each element through an iteratee function. - * Creates a shallow copy of the array before sorting to avoid mutating the original. - * - * @param {Array} arr - The array to sort - * @param {Function} fn - The iteratee function to compute sort values - * @returns {Array} A new sorted array - * - * @example - * const users = [{name: 'john', age: 30}, {name: 'jane', age: 25}]; - * sortBy(users, x => x.age); // [{name: 'jane', age: 25}, {name: 'john', age: 30}] - */ -export const sortBy = (arr: T[], fn: (item: T) => number | string): T[] => - [...arr].sort((a, b) => (fn(a) > fn(b) ? 1 : -1)) - -/** - * Sorts an array by multiple criteria with customizable sort orders. - * Supports ascending (default) and descending order for each criterion. - * - * @param {Array} arr - The array to sort - * @param {Array} fns - Array of iteratee functions to compute sort values - * @param {Array} orders - Array of sort orders ('asc' or 'desc'), defaults to all 'asc' - * @returns {Array} A new sorted array - * - * @example - * const users = [{name: 'john', age: 30}, {name: 'jane', age: 25}, {name: 'bob', age: 30}]; - * orderBy(users, [x => x.age, x => x.name], ['desc', 'asc']); - * // [{name: 'bob', age: 30}, {name: 'john', age: 30}, {name: 'jane', age: 25}] - */ -export const orderBy = ( - arr: unknown[], - fns: ((item: unknown) => string | number)[], - orders: string[] = [] -): unknown[] => - [...arr].sort((a, b) => - fns.reduce( - (acc, fn, i) => - acc || - ((orders[i] === 'desc' ? fn(b) > fn(a) : fn(a) > fn(b)) ? 1 : fn(a) === fn(b) ? 0 : -1), - 0 - ) - ) - -/** - * Creates a duplicate-free version of an array using an iteratee function to generate - * the criterion by which uniqueness is computed. Only the first occurrence of each - * element is kept. - * - * @param {Array} arr - The array to inspect - * @param {Function} fn - The iteratee function to compute uniqueness criterion - * @returns {Array} A new duplicate-free array - * - * @example - * const users = [{id: 1, name: 'john'}, {id: 2, name: 'jane'}, {id: 1, name: 'john'}]; - * uniqBy(users, x => x.id); // [{id: 1, name: 'john'}, {id: 2, name: 'jane'}] - */ -export const uniqBy = (arr: T[], fn: (item: T) => unknown): T[] => - arr.filter((item, index) => arr.findIndex(x => fn(x) === fn(item)) === index) - -/** - * Converts a string to start case (capitalizes the first letter of each word). - * Handles camelCase, snake_case, kebab-case, and regular spaces. - * - * @param {string} str - The string to convert - * @returns {string} The start case string - * - * @example - * startCase('hello_world'); // "Hello World" - * startCase('helloWorld'); // "Hello World" - * startCase('hello-world'); // "Hello World" - * startCase('hello world'); // "Hello World" - */ -export const startCase = (str: string): string => - str - .replace(/([a-z])([A-Z])/g, '$1 $2') // Split camelCase - .replace(/[_-]/g, ' ') // Replace underscores and hyphens with spaces - .replace(/\b\w/g, c => c.toUpperCase()) // Capitalize first letter of each word diff --git a/scripts/functions/index.ts b/scripts/functions/index.ts deleted file mode 100644 index 8c9cb76a..00000000 --- a/scripts/functions/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './functions' \ No newline at end of file diff --git a/scripts/models/guideChannel.ts b/scripts/models/guideChannel.ts index 92aca912..4876a89d 100644 --- a/scripts/models/guideChannel.ts +++ b/scripts/models/guideChannel.ts @@ -1,59 +1,59 @@ -import { Dictionary } from '@freearhey/core' -import epgGrabber from 'epg-grabber' -import { Feed, Channel } from '.' - -export class GuideChannel { - channelId?: string - channel?: Channel - feedId?: string - feed?: Feed - xmltvId?: string - languageCode?: string - siteId?: string - logoUrl?: string - siteDomain?: string - siteName?: string - - constructor(data: epgGrabber.Channel) { - const [channelId, feedId] = data.xmltv_id ? data.xmltv_id.split('@') : [undefined, undefined] - - this.channelId = channelId - this.feedId = feedId - this.xmltvId = data.xmltv_id - this.languageCode = data.lang - this.siteId = data.site_id - this.logoUrl = data.logo - this.siteDomain = data.site - this.siteName = data.name - } - - withChannel(channelsKeyById: Dictionary): this { - if (this.channelId) this.channel = channelsKeyById.get(this.channelId) - - return this - } - - withFeed(feedsKeyByStreamId: Dictionary): this { - if (this.feedId) this.feed = feedsKeyByStreamId.get(this.getStreamId()) - - return this - } - - getStreamId(): string { - if (!this.channelId) return '' - if (!this.feedId) return this.channelId - - return `${this.channelId}@${this.feedId}` - } - - toJSON() { - return { - channel: this.channelId || null, - feed: this.feedId || null, - site: this.siteDomain || '', - site_id: this.siteId || '', - site_name: this.siteName || '', - lang: this.languageCode || '' - } - } -} +import { Dictionary } from '@freearhey/core' +import epgGrabber from 'epg-grabber' +import { Feed, Channel } from '.' + +export class GuideChannel { + channelId?: string + channel?: Channel + feedId?: string + feed?: Feed + xmltvId?: string + languageCode?: string + siteId?: string + logoUrl?: string + siteDomain?: string + siteName?: string + + constructor(data: epgGrabber.Channel) { + const [channelId, feedId] = data.xmltv_id ? data.xmltv_id.split('@') : [undefined, undefined] + + this.channelId = channelId + this.feedId = feedId + this.xmltvId = data.xmltv_id + this.languageCode = data.lang + this.siteId = data.site_id + this.logoUrl = data.logo + this.siteDomain = data.site + this.siteName = data.name + } + + withChannel(channelsKeyById: Dictionary): this { + if (this.channelId) this.channel = channelsKeyById.get(this.channelId) + + return this + } + + withFeed(feedsKeyByStreamId: Dictionary): this { + if (this.feedId) this.feed = feedsKeyByStreamId.get(this.getStreamId()) + + return this + } + + getStreamId(): string { + if (!this.channelId) return '' + if (!this.feedId) return this.channelId + + return `${this.channelId}@${this.feedId}` + } + + toJSON() { + return { + channel: this.channelId || null, + feed: this.feedId || null, + site: this.siteDomain || '', + site_id: this.siteId || '', + site_name: this.siteName || '', + lang: this.languageCode || '' + } + } +} diff --git a/scripts/models/index.ts b/scripts/models/index.ts index 38ab2027..b97d859c 100644 --- a/scripts/models/index.ts +++ b/scripts/models/index.ts @@ -1,9 +1,9 @@ -export * from './channel' -export * from './feed' -export * from './guide' -export * from './guideChannel' -export * from './issue' -export * from './logo' -export * from './site' -export * from './stream' -export * from './channelList' +export * from './channel' +export * from './feed' +export * from './guide' +export * from './guideChannel' +export * from './issue' +export * from './logo' +export * from './site' +export * from './stream' +export * from './channelList' diff --git a/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.channels.xml b/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.channels.xml index 929c7a1c..0c3030c1 100644 --- a/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.channels.xml +++ b/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.channels.xml @@ -1,298 +1,298 @@ - - - LA CHAINE DU PERE NOEL - FRANCE 3 ALPES - FRANCE 3 ALSACE - FRANCE 3 AQUITAINE - FRANCE 3 AUVERGNE - FRANCE 3 NORMANDIE CAEN - FRANCE 3 BOURGOGNE - FRANCE 3 BRETAGNE - FRANCE 3 CENTRE - FRANCE 3 CHAMPAGNE ARDENNE - FRANCE 3 COTE D'AZUR - FRANCE 3 FRANCHE COMTE - FRANCE 3 NORMANDIE ROUEN - FRANCE 3 LANGUEDOC - FRANCE 3 LIMOUSIN - FRANCE 3 LORRAINE - FRANCE 3 MIDI-PYRENEES - FRANCE 3 NORD P. CALAIS - FRANCE 3 PARIS IDF - FRANCE 3 PAYS DE LA LOIRE - FRANCE 3 PICARDIE - FRANCE 3 POITOU CHARENTES - FRANCE 3 PROVENCE ALPES - FRANCE 3 RHONE ALPES - WARNER TV NEXT - BOOMERANG (VO) - TCM CINEMA (VO) - TF1 4K - NCI - TECH&CO - DISNEY CHANNEL +1 - TOP SANTE TV - CANAL+ LIGUE1 UBER EATS - M6 4K - FRANCE 24 Arabe - CANAL+FOOT - CANAL+SPORT360 - L'ESPRIT SORCIER TV - FRANCE 24 Espagnol - CARTOONITO - SQOOL TV - CANAL+BOX OFFICE - TVMONACO - DAZN 1 - TRACE URBAN - STAR ACADEMY, LE LIVE - RFM TV - TRACE CARIBBEAN - TRACE LATINA - TRACE VANILLA - CSTAR HITS FRANCE - MEN'S UP TV - SOUVENIRS FROM EARTH - PUBLIC SENAT 24/24 - B SMART - LA CHAINE METEO - SKYNEWS - AFRICA 24 - AL JAZEERA Arabic - MEDI 1 TV - TRT WORLD - CANAL 10 Guadeloupe - TAHITI NUI TELEVISION - TELE ANTILLES - MADRAS FM TV - TRAVEL CHANNEL - FOOD NETWORK - FOXNEWS - ANTENA 3 - STAR TVE - A3 SERIES - CANAL 24 HORAS - ALL FLAMENCO - TV3 CATALUNYA - ETB BASQUE - TV DE GALICIA - REAL MADRID TV - RTP 3 - TVI INTERNACIONAL - SIC NOTICIAS - SIC INTERNACIONAL - TV RECORD - TVI FICCAO - ALMA LUSA - A BOLA TV - CORREIO DA MANHA TV - RAI STORIA - RAI SCUOLA - MEDIASET ITALIA - AL ARABIYA - ALARABY TELEVISION - AL AOULA - CANAL ALGERIE - MBC - ROTANA CLASSIC - ROTANA CLIP - ENNAHAR TV - ECHOROUK TV - NESSMA EU - EL HIWAR ETTOUNSI - AL RESALAH - IQRAA - IQRAA INTERNATIONAL - SAMIRA TV - ROTANA MUSICA - ECHOROUK NEWS - ROTANA KHALIJIA - ROTANA CINEMA - ROTANA COMEDY - ROTANA DRAMA - EL BILAD TV - PANORAMA DRAMA - MBC DRAMA - MBC MASR - AL RAWDA - NTD TV - CCTV 4 - PHOENIX CNE - PHOENIX INFONEWS - CHINA MOVIE CHANNEL - CCTV DIVERTISSEMENT - ZHEJIANG INTERNATIONAL TV - SHANGHAI DRAGON TV - BEIJING TV - HUNAN WORLD TV - JIANGSU INTERNATIONAL TV - GRT GBA Satellite TV - GREAT WALL ELITE - RTS - 2STV - ORTM - RTI1 - CRTV - RTNC - TELE CONGO - ORTB - A+ - AFRICABLE - CANAL 2 INT. - TVT - RTG - TFM - TRACE AFRICA - TRACE GOSPEL - SEN TV - TRACE TERANGA - 2M MONDE - 6TER - AB1 - ACTION - AL JAZEERA Anglais - ANIMAUX - ARTE - AUTOMOTO, la chaine - BBC ENTERTAINMENT - BBC NEWS - BEIN SPORTS 1 - BEIN SPORTS 2 - BEIN SPORTS 3 - BEIN SPORTS MAX 10 - BEIN SPORTS MAX 4 - BEIN SPORTS MAX 5 - BEIN SPORTS MAX 6 - BEIN SPORTS MAX 7 - BEIN SPORTS MAX 8 - BEIN SPORTS MAX 9 - BET - BFM BUSINESS - BFM TV - BLOOMBERG EUROPE - BOOMERANG - BOOMERANG +1 - CANAL J - CANAL+ - CANAL+CINEMA(S) - CANAL+DOCS - CANAL+GRAND ECRAN - CANAL+kids - CANAL+SERIES - CANAL+SPORT - CHASSE PECHE - CHERIE 25 - CINE+CLASSIC - CINE+CLUB - CINE+EMOTION - CINE+FAMIZ - CINE+FRISSON - CINE+PREMIER - CLUBBING TV - CNBC - CNEWS - CNN INTERNATIONAL - COMEDIE+ - COMEDY CENTRAL - CRIME DISTRICT - CSTAR - DEMAIN - DISNEY CHANNEL - DISNEY JUNIOR - DEUTSCHE WELLE - EQUIDIA - EUROCHANNEL - EURONEWS Français - FASHIONTV PARIS - FRANCE 2 - FRANCE 24 Anglais - FRANCE 24 Français - FRANCE 3 - FRANCE 3 CORSE VIA STELLA - FRANCE 4 - FRANCE 5 - FRANCEINFO: - GAME ONE - GAME ONE +1 - GOLF CHANNEL - GULLI - HISTOIRE TV - I24NEWS - J-ONE - KTO - LCI - LCP 100% - LA CHAINE L'EQUIPE - LUCKY JACK - LUXE TV - M6 - M6MUSIC - MAISON ET TRAVAUX TV - MANGAS - MCM - MELODY - MELODY D'AFRIQUE - MEZZO - MEZZO LIVE - MGG TV - MTV - MTV HITS - MUSEUM TV - MY ZEN TV - NATIONAL GEOGRAPHIC - NATIONAL GEOGRAPHIC WILD - NHK WORLD - JAPAN - NICKELODEON - NICKELODEON JUNIOR - NICKELODEON +1 - NICKELODEON TEEN - NOLLYWOOD TV - NOVELAS TV - NRJ HITS - OCS PULP - OCS GEANTS - OCS MAX - OLYMPIA TV - PARAMOUNT CHANNEL - PARAMOUNT CHANNEL DECALE - PARIS PREMIERE - PIWI+ - PLANETE+ - PLANETE+AVENTURE - PLANETE+CRIME - POLAR+ - LCP/PS - RAI UNO - RAI DUE - RAI TRE - RAI NEWS 24 - RMC DECOUVERTE - RMC STORY - RTL9 - RTPI - SCIENCE & VIE TV - SERIE CLUB - SPORT EN FRANCE - STINGRAY CLASSICA - SUNU YEUF - T18 - TCM CINEMA - TELETOON+ - TELETOON +1 - TEVA - TF1 - TF1 +1 - TF1 SERIES FILMS - TFX - TIJI - TMC - TMC +1 - TOUTE L'HISTOIRE - TV5MONDE - TV BREIZH - TVE INTERNACIONAL - TV PITCHOUN - USHUAIA TV - VOXAFRICA - W9 - + + + LA CHAINE DU PERE NOEL + FRANCE 3 ALPES + FRANCE 3 ALSACE + FRANCE 3 AQUITAINE + FRANCE 3 AUVERGNE + FRANCE 3 NORMANDIE CAEN + FRANCE 3 BOURGOGNE + FRANCE 3 BRETAGNE + FRANCE 3 CENTRE + FRANCE 3 CHAMPAGNE ARDENNE + FRANCE 3 COTE D'AZUR + FRANCE 3 FRANCHE COMTE + FRANCE 3 NORMANDIE ROUEN + FRANCE 3 LANGUEDOC + FRANCE 3 LIMOUSIN + FRANCE 3 LORRAINE + FRANCE 3 MIDI-PYRENEES + FRANCE 3 NORD P. CALAIS + FRANCE 3 PARIS IDF + FRANCE 3 PAYS DE LA LOIRE + FRANCE 3 PICARDIE + FRANCE 3 POITOU CHARENTES + FRANCE 3 PROVENCE ALPES + FRANCE 3 RHONE ALPES + WARNER TV NEXT + BOOMERANG (VO) + TCM CINEMA (VO) + TF1 4K + NCI + TECH&CO + DISNEY CHANNEL +1 + TOP SANTE TV + CANAL+ LIGUE1 UBER EATS + M6 4K + FRANCE 24 Arabe + CANAL+FOOT + CANAL+SPORT360 + L'ESPRIT SORCIER TV + FRANCE 24 Espagnol + CARTOONITO + SQOOL TV + CANAL+BOX OFFICE + TVMONACO + DAZN 1 + TRACE URBAN + STAR ACADEMY, LE LIVE + RFM TV + TRACE CARIBBEAN + TRACE LATINA + TRACE VANILLA + CSTAR HITS FRANCE + MEN'S UP TV + SOUVENIRS FROM EARTH + PUBLIC SENAT 24/24 + B SMART + LA CHAINE METEO + SKYNEWS + AFRICA 24 + AL JAZEERA Arabic + MEDI 1 TV + TRT WORLD + CANAL 10 Guadeloupe + TAHITI NUI TELEVISION + TELE ANTILLES + MADRAS FM TV + TRAVEL CHANNEL + FOOD NETWORK + FOXNEWS + ANTENA 3 + STAR TVE + A3 SERIES + CANAL 24 HORAS + ALL FLAMENCO + TV3 CATALUNYA + ETB BASQUE + TV DE GALICIA + REAL MADRID TV + RTP 3 + TVI INTERNACIONAL + SIC NOTICIAS + SIC INTERNACIONAL + TV RECORD + TVI FICCAO + ALMA LUSA + A BOLA TV + CORREIO DA MANHA TV + RAI STORIA + RAI SCUOLA + MEDIASET ITALIA + AL ARABIYA + ALARABY TELEVISION + AL AOULA + CANAL ALGERIE + MBC + ROTANA CLASSIC + ROTANA CLIP + ENNAHAR TV + ECHOROUK TV + NESSMA EU + EL HIWAR ETTOUNSI + AL RESALAH + IQRAA + IQRAA INTERNATIONAL + SAMIRA TV + ROTANA MUSICA + ECHOROUK NEWS + ROTANA KHALIJIA + ROTANA CINEMA + ROTANA COMEDY + ROTANA DRAMA + EL BILAD TV + PANORAMA DRAMA + MBC DRAMA + MBC MASR + AL RAWDA + NTD TV + CCTV 4 + PHOENIX CNE + PHOENIX INFONEWS + CHINA MOVIE CHANNEL + CCTV DIVERTISSEMENT + ZHEJIANG INTERNATIONAL TV + SHANGHAI DRAGON TV + BEIJING TV + HUNAN WORLD TV + JIANGSU INTERNATIONAL TV + GRT GBA Satellite TV + GREAT WALL ELITE + RTS + 2STV + ORTM + RTI1 + CRTV + RTNC + TELE CONGO + ORTB + A+ + AFRICABLE + CANAL 2 INT. + TVT + RTG + TFM + TRACE AFRICA + TRACE GOSPEL + SEN TV + TRACE TERANGA + 2M MONDE + 6TER + AB1 + ACTION + AL JAZEERA Anglais + ANIMAUX + ARTE + AUTOMOTO, la chaine + BBC ENTERTAINMENT + BBC NEWS + BEIN SPORTS 1 + BEIN SPORTS 2 + BEIN SPORTS 3 + BEIN SPORTS MAX 10 + BEIN SPORTS MAX 4 + BEIN SPORTS MAX 5 + BEIN SPORTS MAX 6 + BEIN SPORTS MAX 7 + BEIN SPORTS MAX 8 + BEIN SPORTS MAX 9 + BET + BFM BUSINESS + BFM TV + BLOOMBERG EUROPE + BOOMERANG + BOOMERANG +1 + CANAL J + CANAL+ + CANAL+CINEMA(S) + CANAL+DOCS + CANAL+GRAND ECRAN + CANAL+kids + CANAL+SERIES + CANAL+SPORT + CHASSE PECHE + CHERIE 25 + CINE+CLASSIC + CINE+CLUB + CINE+EMOTION + CINE+FAMIZ + CINE+FRISSON + CINE+PREMIER + CLUBBING TV + CNBC + CNEWS + CNN INTERNATIONAL + COMEDIE+ + COMEDY CENTRAL + CRIME DISTRICT + CSTAR + DEMAIN + DISNEY CHANNEL + DISNEY JUNIOR + DEUTSCHE WELLE + EQUIDIA + EUROCHANNEL + EURONEWS Français + FASHIONTV PARIS + FRANCE 2 + FRANCE 24 Anglais + FRANCE 24 Français + FRANCE 3 + FRANCE 3 CORSE VIA STELLA + FRANCE 4 + FRANCE 5 + FRANCEINFO: + GAME ONE + GAME ONE +1 + GOLF CHANNEL + GULLI + HISTOIRE TV + I24NEWS + J-ONE + KTO + LCI + LCP 100% + LA CHAINE L'EQUIPE + LUCKY JACK + LUXE TV + M6 + M6MUSIC + MAISON ET TRAVAUX TV + MANGAS + MCM + MELODY + MELODY D'AFRIQUE + MEZZO + MEZZO LIVE + MGG TV + MTV + MTV HITS + MUSEUM TV + MY ZEN TV + NATIONAL GEOGRAPHIC + NATIONAL GEOGRAPHIC WILD + NHK WORLD - JAPAN + NICKELODEON + NICKELODEON JUNIOR + NICKELODEON +1 + NICKELODEON TEEN + NOLLYWOOD TV + NOVELAS TV + NRJ HITS + OCS PULP + OCS GEANTS + OCS MAX + OLYMPIA TV + PARAMOUNT CHANNEL + PARAMOUNT CHANNEL DECALE + PARIS PREMIERE + PIWI+ + PLANETE+ + PLANETE+AVENTURE + PLANETE+CRIME + POLAR+ + LCP/PS + RAI UNO + RAI DUE + RAI TRE + RAI NEWS 24 + RMC DECOUVERTE + RMC STORY + RTL9 + RTPI + SCIENCE & VIE TV + SERIE CLUB + SPORT EN FRANCE + STINGRAY CLASSICA + SUNU YEUF + T18 + TCM CINEMA + TELETOON+ + TELETOON +1 + TEVA + TF1 + TF1 +1 + TF1 SERIES FILMS + TFX + TIJI + TMC + TMC +1 + TOUTE L'HISTOIRE + TV5MONDE + TV BREIZH + TVE INTERNACIONAL + TV PITCHOUN + USHUAIA TV + VOXAFRICA + W9 + diff --git a/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.config.js b/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.config.js index 3ed3ebc4..80f232d2 100644 --- a/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.config.js +++ b/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.config.js @@ -1,79 +1,79 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'chaines-tv.orange.fr', - days: 2, - url({ channel, date }) { - return `https://rp-ott-mediation-tv.woopic.com/api-gw/live/v3/applications/STB4PC/programs?groupBy=channel&includeEmptyChannels=false&period=${date.valueOf()},${date - .add(1, 'd') - .valueOf()}&after=${channel.site_id}&limit=1` - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const start = parseStart(item) - const stop = parseStop(item, start) - programs.push({ - title: item.title, - subTitle: item.season?.serie?.title, - category: item.genreDetailed, - description: item.synopsis, - season: parseSeason(item), - episode: parseEpisode(item), - image: parseImage(item), - start: start.toJSON(), - stop: stop.toJSON() - }) - }) - - return programs - }, - async channels() { - const html = await axios - .get('https://chaines-tv.orange.fr/programme-tv?filtres=all') - .then(r => r.data) - .catch(console.log) - - const [, nuxtFunc] = html.match(/window\.__NUXT__=([^<]+)/) || [null, null] - const func = new Function(`"use strict";return ${nuxtFunc}`) - - const data = func() - const items = data.state.channels.channels - - return items.map(item => { - return { - lang: 'fr', - site_id: item.idEPG, - name: item.name - } - }) - } -} - -function parseImage(item) { - return item.covers && item.covers.length ? item.covers[0].url : null -} - -function parseStart(item) { - return dayjs.unix(item.diffusionDate) -} - -function parseStop(item, start) { - return start.add(item.duration, 's') -} - -function parseSeason(item) { - return item.season?.number -} - -function parseEpisode(item) { - return item.episodeNumber -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - - return data && data[channel.site_id] ? data[channel.site_id] : [] -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'chaines-tv.orange.fr', + days: 2, + url({ channel, date }) { + return `https://rp-ott-mediation-tv.woopic.com/api-gw/live/v3/applications/STB4PC/programs?groupBy=channel&includeEmptyChannels=false&period=${date.valueOf()},${date + .add(1, 'd') + .valueOf()}&after=${channel.site_id}&limit=1` + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const start = parseStart(item) + const stop = parseStop(item, start) + programs.push({ + title: item.title, + subTitle: item.season?.serie?.title, + category: item.genreDetailed, + description: item.synopsis, + season: parseSeason(item), + episode: parseEpisode(item), + image: parseImage(item), + start: start.toJSON(), + stop: stop.toJSON() + }) + }) + + return programs + }, + async channels() { + const html = await axios + .get('https://chaines-tv.orange.fr/programme-tv?filtres=all') + .then(r => r.data) + .catch(console.log) + + const [, nuxtFunc] = html.match(/window\.__NUXT__=([^<]+)/) || [null, null] + const func = new Function(`"use strict";return ${nuxtFunc}`) + + const data = func() + const items = data.state.channels.channels + + return items.map(item => { + return { + lang: 'fr', + site_id: item.idEPG, + name: item.name + } + }) + } +} + +function parseImage(item) { + return item.covers && item.covers.length ? item.covers[0].url : null +} + +function parseStart(item) { + return dayjs.unix(item.diffusionDate) +} + +function parseStop(item, start) { + return start.add(item.duration, 's') +} + +function parseSeason(item) { + return item.season?.number +} + +function parseEpisode(item) { + return item.episodeNumber +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + + return data && data[channel.site_id] ? data[channel.site_id] : [] +} diff --git a/sites/ctc.ru/ctc.ru.config.js b/sites/ctc.ru/ctc.ru.config.js index 773d9366..5dc41772 100644 --- a/sites/ctc.ru/ctc.ru.config.js +++ b/sites/ctc.ru/ctc.ru.config.js @@ -1,95 +1,95 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'ctc.ru', - days: 1, - url: ({ date }) => `https://ctc.ru/api/page/v2/programm/?date=${formatDate(date)}`, - parser({ content }) { - const programs = [] - const items = parseItems(content) - for (const item of items) { - programs.push({ - title: item.bubbleTitle, - // more like "films", "shows", "cartoons" - not a genre - category: item.bubbleSubTitle, - icons: parseIcons(item), - images: parseImages(item), - start: parseStart(item), - stop: parseStop(item), - // not sure if CTC uses this more like `premiere` but I don't have any - // additional info to use in the `premiere` field so I'm using this - // instead. - new: item.isPremiere ?? false, - url: item.bubbleUrl ? `https://ctc.ru${item.bubbleUrl}` : undefined, - rating: parseRating(item), - }) - } - - return programs - } -} - -function formatDate(date) { - return dayjs(date).format('DD-MM-YYYY') -} - -function parseIcons(item) { - const images = item.bubbleImage ?? [] - // biggest first - const sorted = images.sort((a, b) => b.height - a.height) - - return sorted.map((image) => ({ - src: image.url, - width: image.width, - height: image.height, - })) -} - -function parseImages(item) { - const images = item.trackImageUrl ?? [] - // biggest first - const sorted = images.sort((a, b) => b.height - a.height) - - // compile one image of each size since the content should be the same - const sizes = {} - for (const image of sorted) { - const maxRes = Math.max(image.width, image.height) - const item = { - type: 'backdrop', - // https://github.com/ektotv/xmltv/blob/801417b4b7aae38f13caa82d8bbfbed0a254ee5f/src/types.ts#L40-L48 - size: maxRes > 400 ? '3' : maxRes > 200 ? '2' : '1', - orient: image.height > image.width ? 'P' : 'L', - value: image.url, - } - if (sizes[item.size]) continue - sizes[item.size] = item - } - - // re-sort so the size 3 images are first - return Object - .values(sizes) - .sort((a, b) => Number(b.size) - Number(a.size)) -} - -function parseStart(item) { - return dayjs(item.startTime) -} - -function parseStop(item) { - return dayjs(item.endTime) -} - -function parseRating(item) { - if (item.ageLimit == null) return null - return { - // Not sure what the Russian system is actually called (if anything) - system: 'Russia', - value: `${item.ageLimit}+`, - } -} - -function parseItems(content) { - const node = JSON.parse(content).content.find(n => n.type === 'tv-program') - if (node) return node.widgets - return [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'ctc.ru', + days: 1, + url: ({ date }) => `https://ctc.ru/api/page/v2/programm/?date=${formatDate(date)}`, + parser({ content }) { + const programs = [] + const items = parseItems(content) + for (const item of items) { + programs.push({ + title: item.bubbleTitle, + // more like "films", "shows", "cartoons" - not a genre + category: item.bubbleSubTitle, + icons: parseIcons(item), + images: parseImages(item), + start: parseStart(item), + stop: parseStop(item), + // not sure if CTC uses this more like `premiere` but I don't have any + // additional info to use in the `premiere` field so I'm using this + // instead. + new: item.isPremiere ?? false, + url: item.bubbleUrl ? `https://ctc.ru${item.bubbleUrl}` : undefined, + rating: parseRating(item), + }) + } + + return programs + } +} + +function formatDate(date) { + return dayjs(date).format('DD-MM-YYYY') +} + +function parseIcons(item) { + const images = item.bubbleImage ?? [] + // biggest first + const sorted = images.sort((a, b) => b.height - a.height) + + return sorted.map((image) => ({ + src: image.url, + width: image.width, + height: image.height, + })) +} + +function parseImages(item) { + const images = item.trackImageUrl ?? [] + // biggest first + const sorted = images.sort((a, b) => b.height - a.height) + + // compile one image of each size since the content should be the same + const sizes = {} + for (const image of sorted) { + const maxRes = Math.max(image.width, image.height) + const item = { + type: 'backdrop', + // https://github.com/ektotv/xmltv/blob/801417b4b7aae38f13caa82d8bbfbed0a254ee5f/src/types.ts#L40-L48 + size: maxRes > 400 ? '3' : maxRes > 200 ? '2' : '1', + orient: image.height > image.width ? 'P' : 'L', + value: image.url, + } + if (sizes[item.size]) continue + sizes[item.size] = item + } + + // re-sort so the size 3 images are first + return Object + .values(sizes) + .sort((a, b) => Number(b.size) - Number(a.size)) +} + +function parseStart(item) { + return dayjs(item.startTime) +} + +function parseStop(item) { + return dayjs(item.endTime) +} + +function parseRating(item) { + if (item.ageLimit == null) return null + return { + // Not sure what the Russian system is actually called (if anything) + system: 'Russia', + value: `${item.ageLimit}+`, + } +} + +function parseItems(content) { + const node = JSON.parse(content).content.find(n => n.type === 'tv-program') + if (node) return node.widgets + return [] +} diff --git a/sites/ctc.ru/ctc.ru.test.js b/sites/ctc.ru/ctc.ru.test.js index 4e11cc64..529f39b2 100644 --- a/sites/ctc.ru/ctc.ru.test.js +++ b/sites/ctc.ru/ctc.ru.test.js @@ -1,91 +1,91 @@ -const { parser, url } = require('./ctc.ru.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2025-06-22') - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://ctc.ru/api/page/v2/programm/?date=22-06-2025') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2025-06-22T03:00:00.000Z', - stop: '2025-06-22T03:55:00.000Z', - title: 'Три кота', - category: 'Мультфильмы', - new: false, - url: 'https://ctc.ru/collections/multiki/', - icons: [ - { - src: 'https://mgf-static-ssl.ctc.ru/images/ctc-entity-dictionary-category/5/iconurl/web/5f9027fcba9ac-125x125.png', - width: 125, - height: 125, - }, - { - src: 'https://mgf-static-ssl.ctc.ru/images/ctc-entity-dictionary-category/5/iconurl/web/5f9027fc99587-60x60.png', - width: 60, - height: 60, - }, - ], - images: [ - { - type: 'backdrop', - size: '3', - orient: 'L', - value: 'https://mgf-static-ssl.ctc.ru/images/ctc-entity-project/1129/horizontalcover/web/66c319f0a31b5-1740x978.jpeg', - }, - { - type: 'backdrop', - size: '2', - orient: 'L', - value: 'https://mgf-static-ssl.ctc.ru/images/ctc-entity-project/1129/horizontalcover/web/66c319f20bac2-400x225.jpeg', - }, - { - type: 'backdrop', - size: '1', - orient: 'L', - value: 'https://mgf-static-ssl.ctc.ru/images/ctc-entity-project/1129/horizontalcover/web/66c319f244f92-150x84.jpeg', - }, - ], - rating: { - system: 'Russia', - value: '0+', - }, - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: JSON.stringify({ - isActive: true, - url: '/programm/', - header: [], - sidebar: [], - footer: [], - content: [], - seoTags: {}, - ogMarkup: {}, - userGeo: null, - userData: null, - meta: {}, - activeFrom: null, - activeTo: null, - type: 'tv-program-page', - }) - }) - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./ctc.ru.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2025-06-22') + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://ctc.ru/api/page/v2/programm/?date=22-06-2025') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2025-06-22T03:00:00.000Z', + stop: '2025-06-22T03:55:00.000Z', + title: 'Три кота', + category: 'Мультфильмы', + new: false, + url: 'https://ctc.ru/collections/multiki/', + icons: [ + { + src: 'https://mgf-static-ssl.ctc.ru/images/ctc-entity-dictionary-category/5/iconurl/web/5f9027fcba9ac-125x125.png', + width: 125, + height: 125, + }, + { + src: 'https://mgf-static-ssl.ctc.ru/images/ctc-entity-dictionary-category/5/iconurl/web/5f9027fc99587-60x60.png', + width: 60, + height: 60, + }, + ], + images: [ + { + type: 'backdrop', + size: '3', + orient: 'L', + value: 'https://mgf-static-ssl.ctc.ru/images/ctc-entity-project/1129/horizontalcover/web/66c319f0a31b5-1740x978.jpeg', + }, + { + type: 'backdrop', + size: '2', + orient: 'L', + value: 'https://mgf-static-ssl.ctc.ru/images/ctc-entity-project/1129/horizontalcover/web/66c319f20bac2-400x225.jpeg', + }, + { + type: 'backdrop', + size: '1', + orient: 'L', + value: 'https://mgf-static-ssl.ctc.ru/images/ctc-entity-project/1129/horizontalcover/web/66c319f244f92-150x84.jpeg', + }, + ], + rating: { + system: 'Russia', + value: '0+', + }, + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: JSON.stringify({ + isActive: true, + url: '/programm/', + header: [], + sidebar: [], + footer: [], + content: [], + seoTags: {}, + ogMarkup: {}, + userGeo: null, + userData: null, + meta: {}, + activeFrom: null, + activeTo: null, + type: 'tv-program-page', + }) + }) + expect(results).toMatchObject([]) +}) diff --git a/sites/derana.lk/derana.lk.config.js b/sites/derana.lk/derana.lk.config.js index 790de7ad..266bb725 100644 --- a/sites/derana.lk/derana.lk.config.js +++ b/sites/derana.lk/derana.lk.config.js @@ -3,7 +3,7 @@ const utc = require('dayjs/plugin/utc') const customParseFormat = require('dayjs/plugin/customParseFormat') const parseDuration = require('parse-duration').default const timezone = require('dayjs/plugin/timezone') -const { sortBy } = require('../../scripts/functions') +const sortBy = require('lodash.sortby') dayjs.extend(customParseFormat) dayjs.extend(utc) diff --git a/sites/dstv.com/dstv.com.config.js b/sites/dstv.com/dstv.com.config.js index 44c76111..3412a83d 100644 --- a/sites/dstv.com/dstv.com.config.js +++ b/sites/dstv.com/dstv.com.config.js @@ -3,7 +3,7 @@ const dayjs = require('dayjs') const utc = require('dayjs/plugin/utc') const timezone = require('dayjs/plugin/timezone') const customParseFormat = require('dayjs/plugin/customParseFormat') -const { uniqBy } = require('../../scripts/functions') +const uniqBy = require('lodash.uniqby') dayjs.extend(utc) dayjs.extend(timezone) diff --git a/sites/epgshare01.online/readme.md b/sites/epgshare01.online/readme.md index 7979cca3..8625d159 100644 --- a/sites/epgshare01.online/readme.md +++ b/sites/epgshare01.online/readme.md @@ -1,135 +1,135 @@ -# epgshare01.online - -https://epgshare01.online/epgshare01/ - -| Tag | -| ------------------- | -| `ALJAZEERA1` | -| `AR1` | -| `ASIANTELEVISION1` | -| `AU1` | -| `BA1` | -| `BE2` | -| `BEIN1` | -| `BG1` | -| `BR1` | -| `CA1` | -| `CH1` | -| `CL1` | -| `CO1` | -| `CR1` | -| `CY1` | -| `CZ1` | -| `DE1` | -| `DELUXEMUSIC1` | -| `DIRECTVSPORTS1` | -| `DISTROTV1` | -| `DK1` | -| `DO1` | -| `DRAFTKINGS1` | -| `DUMMY_CHANNELS` | -| `EC1` | -| `EG1` | -| `ES1` | -| `EUROSPORT1` | -| `FANDUEL1` | -| `FI1` | -| `FR1` | -| `GR1` | -| `HK1` | -| `HR1` | -| `HU1` | -| `ID1` | -| `IE1` | -| `IL1` | -| `IN4` | -| `IT1` | -| `JM1` | -| `JP1` | -| `JP2` | -| `KE1` | -| `KR1` | -| `MT1` | -| `MX1` | -| `MY1` | -| `NAUTICAL_CHANNEL1` | -| `NG1` | -| `NL1` | -| `NO1` | -| `NZ1` | -| `OPTUSSPORTS1` | -| `PA1` | -| `PAC-12` | -| `PE1` | -| `PH1` | -| `PH2` | -| `PK1` | -| `PL1` | -| `PLEX1` | -| `POWERNATION1` | -| `PT1` | -| `RAKUTEN_DE1` | -| `RAKUTEN_EN1` | -| `RAKUTEN_ES1` | -| `RAKUTEN_FR1` | -| `RAKUTEN_IT1` | -| `RAKUTEN_NL1` | -| `RAKUTEN_PL1` | -| `RALLY_TV1` | -| `RO1` | -| `RO2` | -| `SA1` | -| `SA2` | -| `SAMSUNG1` | -| `SE1` | -| `SG1` | -| `SK1` | -| `SPORTKLUB1` | -| `SSPORTPLUS1` | -| `SV1` | -| `TBNPLUS1` | -| `TENNIS1` | -| `THESPORTPLUS1` | -| `TR1` | -| `TR3` | -| `UK1` | -| `US1` | -| `US_LOCALS2` | -| `US_SPORTS1` | -| `UY1` | -| `VN1` | -| `VOA1` | -| `ZA1` | -| `viva-russia.ru` | - -### Download the guide - -Windows (Command Prompt): - -```sh -SET "NODE_OPTIONS=--max-old-space-size=6000" && npm run grab --- --channels=sites/epgshare01.online/epgshare01.online_.channels.xml -``` - -Windows (PowerShell): - -```sh -$env:NODE_OPTIONS="--max-old-space-size=6000"; npm run grab --- --channels=sites/epgshare01.online/epgshare01.online_.channels.xml -``` - -Linux and macOS: - -```sh -NODE_OPTIONS=--max-old-space-size=6000 npm run grab --- --channels=sites/epgshare01.online/epgshare01.online_.channels.xml -``` - -### Update channel list - -```sh -npm run channels:parse --- --config=./sites/epgshare01.online/epgshare01.online.config.js --output=./sites/epgshare01.online/epgshare01.online_.channels.xml --set=tag: -``` - -### Test - -```sh -npm test --- epgshare01.online -``` +# epgshare01.online + +https://epgshare01.online/epgshare01/ + +| Tag | +| ------------------- | +| `ALJAZEERA1` | +| `AR1` | +| `ASIANTELEVISION1` | +| `AU1` | +| `BA1` | +| `BE2` | +| `BEIN1` | +| `BG1` | +| `BR1` | +| `CA1` | +| `CH1` | +| `CL1` | +| `CO1` | +| `CR1` | +| `CY1` | +| `CZ1` | +| `DE1` | +| `DELUXEMUSIC1` | +| `DIRECTVSPORTS1` | +| `DISTROTV1` | +| `DK1` | +| `DO1` | +| `DRAFTKINGS1` | +| `DUMMY_CHANNELS` | +| `EC1` | +| `EG1` | +| `ES1` | +| `EUROSPORT1` | +| `FANDUEL1` | +| `FI1` | +| `FR1` | +| `GR1` | +| `HK1` | +| `HR1` | +| `HU1` | +| `ID1` | +| `IE1` | +| `IL1` | +| `IN4` | +| `IT1` | +| `JM1` | +| `JP1` | +| `JP2` | +| `KE1` | +| `KR1` | +| `MT1` | +| `MX1` | +| `MY1` | +| `NAUTICAL_CHANNEL1` | +| `NG1` | +| `NL1` | +| `NO1` | +| `NZ1` | +| `OPTUSSPORTS1` | +| `PA1` | +| `PAC-12` | +| `PE1` | +| `PH1` | +| `PH2` | +| `PK1` | +| `PL1` | +| `PLEX1` | +| `POWERNATION1` | +| `PT1` | +| `RAKUTEN_DE1` | +| `RAKUTEN_EN1` | +| `RAKUTEN_ES1` | +| `RAKUTEN_FR1` | +| `RAKUTEN_IT1` | +| `RAKUTEN_NL1` | +| `RAKUTEN_PL1` | +| `RALLY_TV1` | +| `RO1` | +| `RO2` | +| `SA1` | +| `SA2` | +| `SAMSUNG1` | +| `SE1` | +| `SG1` | +| `SK1` | +| `SPORTKLUB1` | +| `SSPORTPLUS1` | +| `SV1` | +| `TBNPLUS1` | +| `TENNIS1` | +| `THESPORTPLUS1` | +| `TR1` | +| `TR3` | +| `UK1` | +| `US1` | +| `US_LOCALS2` | +| `US_SPORTS1` | +| `UY1` | +| `VN1` | +| `VOA1` | +| `ZA1` | +| `viva-russia.ru` | + +### Download the guide + +Windows (Command Prompt): + +```sh +SET "NODE_OPTIONS=--max-old-space-size=6000" && npm run grab --- --channels=sites/epgshare01.online/epgshare01.online_.channels.xml +``` + +Windows (PowerShell): + +```sh +$env:NODE_OPTIONS="--max-old-space-size=6000"; npm run grab --- --channels=sites/epgshare01.online/epgshare01.online_.channels.xml +``` + +Linux and macOS: + +```sh +NODE_OPTIONS=--max-old-space-size=6000 npm run grab --- --channels=sites/epgshare01.online/epgshare01.online_.channels.xml +``` + +### Update channel list + +```sh +npm run channels:parse --- --config=./sites/epgshare01.online/epgshare01.online.config.js --output=./sites/epgshare01.online/epgshare01.online_.channels.xml --set=tag: +``` + +### Test + +```sh +npm test --- epgshare01.online +``` diff --git a/sites/guida.tv/guida.tv.config.js b/sites/guida.tv/guida.tv.config.js index 9e00f73f..810c53a2 100644 --- a/sites/guida.tv/guida.tv.config.js +++ b/sites/guida.tv/guida.tv.config.js @@ -4,7 +4,7 @@ const dayjs = require('dayjs') const utc = require('dayjs/plugin/utc') const timezone = require('dayjs/plugin/timezone') const customParseFormat = require('dayjs/plugin/customParseFormat') -const { uniqBy } = require('../../scripts/functions') +const uniqBy = require('lodash.uniqby') dayjs.extend(utc) dayjs.extend(timezone) diff --git a/sites/i.mjh.nz/i.mjh.nz_samsung.channels.xml b/sites/i.mjh.nz/i.mjh.nz_samsung.channels.xml index fc581f59..80c7ca37 100644 --- a/sites/i.mjh.nz/i.mjh.nz_samsung.channels.xml +++ b/sites/i.mjh.nz/i.mjh.nz_samsung.channels.xml @@ -1,1528 +1,1528 @@ - - - Deluxe Lounge HD - Focus TV - Netzkino - Tierwelt - Xplore - Hip Trips - just.fishing - Sportdigital Free - Just Cooking - SPIEGEL TV - GoldStar TV - Charlie - Grjngo - Tempora - auto motor und sport - MTV Pluto TV - Teen Nick - SpongeBob Schwammkopf - Ice Pilots - The Pet Collective - Actionfilme - Rakuten TV - Drama Filme - Rakuten TV - Nick Pluto TV - iCarly - MTV Teen Mom - FailArmy - Komödien - Rakuten TV - Dokumentarfilme - Rakuten TV - Pluto TV Serie - Pluto TV Paranormal - Pluto TV Animals - Pluto TV Food - Pluto TV Science - Pluto TV Sitcoms - Comedy Central Pluto TV - Pluto TV Movies - Comedy Central Made in Germany - MTV Catfish - People Are Awesome - Euronews Live - Travelxp - Yu-Gi-Oh! - Fantasja - Fabella - One Terra - Bergblick Free - Deluxe Deutschpop - Qwest TV - INFAST - Tastemade - Tennis Channel - Zee One - FIFA+ - CURIOSITY Now - SPIEGEL TV Konflikte - Strongman Champions League - Deutsches Kino - Rakuten TV - Bloomberg TV+ - MyTime Movie Network - wedo movies - Horse &amp; Country - Wilder Planet - Fashion TV - Top Filme - Rakuten TV - Moconomy - Heimatkino - INWONDER - XITE Hits - Crime Serien - Rakuten TV - Teletubbies - Comedy &amp; Shows - Craction TV - Moviedome - 100% Weihnachten - Rakuten TV - Moviedome Family - Clubbing TV - Crime Mix - Qello Concerts by Stingray - Stars in Gefahr - The Wicked Tuna Channel - Naruto - Spannung &amp; Emotionen - Filmgold - CNN - FRANCE 24 FAST - Kaminfeuer - Cine Mix - Red Bull TV - Sport 1 Motor - Out TV - Hell's Kitchen - Sallys Welt - Baywatch - More than Sports TV - Motorvision Classic - Motorvision - Terra Mater - X-MAS Mix - Deluxe Wintertime - CNN FAST - World Poker Tour - Aniverse - Deluxe Lounge HD - Focus TV - Netzkino - Tierwelt - Xplore - Hip Trips - just.fishing - Sportdigital Free - Just Cooking - SPIEGEL TV - GoldStar TV - Charlie - Grjngo - Tempora - auto motor und sport - MTV Pluto TV - Nick Pluto TV - Teen Nick - iCarly - Pluto TV Serie - Ice Pilots - MTV Teen Mom - The Pet Collective - WP - Drama Filme - Rakuten TV - Dokumentarfilme - Rakuten TV - SpongeBob Schwammkopf - Pluto TV Paranormal - FailArmy - Actionfilme - Rakuten TV - Komödien - Rakuten TV - Pluto TV Animals - Pluto TV Food - Pluto TV Science - Pluto TV Sitcoms - Comedy Central Pluto TV - Pluto TV Movies - Comedy Central Made in Germany - MTV Catfish - People Are Awesome - Travelxp - Fashion TV - Travelxp [FR] - Top Filme - Rakuten TV - Fabella - One Terra - Heimatkino - Bergblick Free - Deluxe Deutschpop - Qwest TV - INFAST - INWONDER - Tastemade - ADN [FR] - SportOutdoor.tv [IT] - FIFA+ - Strongman Champions League - Teletubbies - Risate all'italiana - Bloomberg TV+ - BelAir TV [FR] - Wellbeing TV [FR] - wedo movies - Craction TV - Wilder Planet - Moviedome Family - Horse &amp; Country - Moconomy - Tennis Channel - Deutsches Kino - Rakuten TV - Crime Serien - Rakuten TV - Euronews Live - Fantasja - 100% Weihnachten - Rakuten TV - Moviedome - Clubbing TV - Emotion'L [FR] - Qello Concerts by Stingray - Chefclub TV [FR] - Ciné Nanar [FR] - Motus [FR] - Spannung &amp; Emotionen - Comedy &amp; Shows - Scream'IN [FR] - Y'a que la vérité qui compte [FR] - Stars in Gefahr - Crime Mix - MyTime Movie Network - Trace Latina [FR] - Gong [FR] - Fréquence Novelas [FR] - Ciné Prime [FR] - Enquêtes de Choc [FR] - Filmgold - Homicide [FR] - Naruto - Destination Nature [FR] - Émotion [FR] - Le Figaro Live [FR] - The Wicked Tuna Channel - Echappées belles &amp; co [FR] - Grandi Documentari - Wedo Big Stories [IT] - Nature Time [FR] - Génération Sitcoms [FR] - Les filles d'à côté [FR] - Film Al Femminile - Wedo Movies [IT] - CNN - Le Tigre [FR] - Vialma [FR] - FRANCE 24 FAST - CURIOSITY Now - SPIEGEL TV Konflikte - Yu-Gi-Oh! - CNN FAST - Screen Green - Alerte à Malibu [FR] - Sport 1 Motor - Out TV - Sallys Welt - Baywatch - More than Sports TV - Motorvision Classic - Terra Mater - World Poker Tour - X-MAS Mix - Plus belle la vie [FR] - Culture Pub [FR] - Hell's Kitchen - Motorvision - Zee One - Deluxe Wintertime - Aniverse - Kaminfeuer - Deluxe Lounge HD - Focus TV - Netzkino - Xplore - Tierwelt - Hip Trips - just.fishing - Sportdigital Free - Nick Pluto TV - Teen Nick - iCarly - Pluto TV Serie - Pluto TV Paranormal - SPIEGEL TV Konflikte - Yu-Gi-Oh! - Zee One - FailArmy - The Pet Collective - Actionfilme - Rakuten TV - Komödien - Rakuten TV - Drama Filme - Rakuten TV - People Are Awesome - MTV Pluto TV - Ice Pilots - MTV Teen Mom - Terra Mater - Dokumentarfilme - Rakuten TV - SpongeBob Schwammkopf - Just Cooking - SPIEGEL TV - GoldStar TV - Charlie - Grjngo - Tempora - auto motor und sport - Comedy Central Pluto TV - Comedy Central Made in Germany - MTV Catfish - Pluto TV Animals - Pluto TV Science - Pluto TV Sitcoms - Pluto TV Movies - Euronews Live - BBC History - BBC Travel - BBC Food - Moconomy - Fantasja - Heimatkino - INFAST - INWONDER - Qwest TV - Tastemade - XITE Hits - Tennis Channel Deutschland - Pluto TV Food - CURIOSITY Now - Top Filme - Rakuten TV - Strongman Champions League - Deutsches Kino - Rakuten TV - Crime Serien - Rakuten TV - FIFA+ - Reuters - Fashion TV - Teletubbies - SuperToons TV - Bloomberg TV+ - Trace Urban - MyTime Movie Network - wedo movies - Craction TV - Moviedome - Moviedome Family - Wilder Planet - Fabella - One Terra - Bergblick Free - 100% Weihnachten - Rakuten TV - Horse &amp; Country - Travelxp - Deluxe Deutschpop - Clubbing TV - Crime Mix - Qello Concerts by Stingray - Cine Mix - Vevo Pop - Spannung &amp; Emotionen - Comedy &amp; Shows - Stars in Gefahr - Red Bull TV - Terra X - Love the Planet - duckTV - Vevo Schlager Pop - Filmgold - Hell's Kitchen - The Wicked Tuna Channel - CNN - FRANCE 24 FAST - Kaminfeuer - Screen Green - Sport 1 Motor - Out TV - Sallys Welt - Motorvision Classic - Motorvision - DEFA TV - XITE Rock On - X-MAS Mix - ZDF kocht! - Deluxe Wintertime - Naruto - CNN FAST - Baywatch - More than Sports TV - World Poker Tour - Bares für Rares - Aniverse - The Asylum - FilmRise Classic TV - NBC News NOW - Forensic Files - FilmRise Action - Game Show Central - FailArmy - Outside - Law&amp;Crime - Stingray Naturescape - The Red Green Channel - WeatherSpy - CINEVAULT: Classics - The Preview Channel - Dry Bar Comedy - The Design Network - MagellanTV Now - FilmRise True Crime - Unsolved Mysteries - FilmRise Western - People Are Awesome - Kidoodle.TV - MHz Now - FilmRise Free Movies - The Pet Collective - INFAST - InWonder - Haunt TV - Always Funny Videos - Baywatch - Moonbug - Court TV - IMPACT Wrestling - beIN SPORTS XTRA - Crime Time - Nosey - TED - Gusto TV - Deal or No Deal - Operation Repo - Alien Nation by DUST - Journy - Waypoint TV - XITE Just Chill - Hell's Kitchen - Dr. G: Medical Examiner - XITE 80s Flashback - XITE 90s Throwback - Circle - This Old House - XITE Rock On - 21 Jump Street - LOL! Network - Fireplace 4K - The Jack Hanna Channel - Xplore - Top Gear - NHRA TV - Cowboy Way - MotorTrend FAST TV - CBC News Explore - Radio-Canada INFO - RetroCrush - Origin Sports - Bob Ross - HappyKids - Documentary+ - Supermarket Sweep - The Biggest Loser - Tastemade Home - Strawberry Shortcake - Bon Appétit - Divorce Court - Architectural Digest - Vevo '70s - Vevo '80s - Vevo 2K - Vevo Retro Rock - Vevo R&amp;B - Comedy Dynamics - Midnight Pulp - Homeful - Hollywire - Midsomer Murders - Cops - The Weather Network - Bob the Builder - Vevo '90s - Vevo Country - Vevo Hip-Hop - Vevo Pop - LEGO Channel - Gravitas Movies - Tastemade - Antiques Roadshow - FIFA+ - Vevo 2010s - BBC Food - BBC Home &amp; Garden - World Poker Tour - MAVTV Select - POWERNATION - XITE Country's Finest - XITE Hits - XITE Icons - Baby Einstein - Cheaters - CBC Comedy - Highway to Heaven - XITE Only Love - batteryPOP - pocket.watch - XITE Country Today - Pluto TV Motor - Sparkle Movies - Popflix - Pluto TV Animals - Pluto TV Food - The Guardian - Fashion TV - Planet Knowledge - Action Movies - Rakuten TV - Comedy Movies - Rakuten TV - Documentaries - Rakuten TV - Filmstream - People Are Awesome - Euronews Live - The Pet Collective - FailArmy - INTROUBLE - INFAST - INWONDER - Drama Movies - Rakuten TV - Kidoodle.TV - Travelxp - GoUSA TV - 5 Cops - 5 Exploring Britain - Pluto TV Action - Pluto TV Movies - Pluto TV Drama - Pluto TV Crime - Pluto TV Inside - Pluto TV Romance - Pluto TV Classic TV - MotorRacing - Qwest TV - Real Stories - History Hit - Horse &amp; Country - Romance Movies - Rakuten TV - Revry - 100% Christmas - Rakuten TV - YAAAS! - SuperToons TV - Bloomberg TV+ - Homicide Hunter - wedo movies - MyTime Movie Network - Real Crime - Deluxe Lounge HD - Wonder - Reuters - Teletubbies - Pluto TV Conspiracy - Pluto TV Paranormal - Tennis Channel International - PBS History - MovieSphere - INWILD - Gusto TV - Beano TV - Catfish - TalkTV - Real Wild - Choppertown - Qello Concerts by Stingray - NBC News NOW - PGA Tour - Are We There Yet? - GB News - Bridezillas - Clubbing TV - Ketchup TV - Moviedome - Shades of Black - XITE Hits - Survivor - McLeod's Daughters - Viaplay Xtra - Project Runway - Wild Planet - Comedy Dynamics - Deal or No Deal US - Tastemade - World Drama by ITV Studios - Shorts - Origin Sports - Hell's Kitchen - WaterBear - Trace Urban - Horizons - Now 80s - The Wicked Tuna Channel - Haunt TV - GREAT! movies - GREAT! Christmas - Radical Docs - LOL! Network - Bloomberg Originals - Homes Under the Hammer - Great British Menu - Adventure Earth - Vevo '90s &amp; '00s - American Idol - America's Got Talent - Entertainment Hub - Mythbusters - The Jamie Oliver Channel - Toon Goggles - WildEarth - Real Life - The LEGO Channel - Come Dine With Me - Vevo Pop - Strongman Champions League - Top Movies - Rakuten TV - Festive Hub - POP - Love the Planet - Inside Crime - Mech+ - Grjngo - Western Movies - Baywatch - World War TV - FRANCE 24 FAST - Love Pets - pocket.watch - Real Series - Rakuten TV - True Crime UK - FIFA+ - So…Real - Super Anime - Dennis and Gnasher - Smurf TV - Fireplace - The Chat Show Channel - CNN FAST - Vevo '70s &amp; '80s - Vevo Hip Hop &amp; R&amp;B - Red Bull TV - Pointless UK: 'Powered by Banijay' - World Poker Tour - Wipeout Xtra Powered by Banijay - Gigs - UKTV Play Full Throttle - UKTV Play Heroes - UKTV Play Laughs - UKTV Play Uncovered - Challenge - Sky Mix - Earth Touch - Eggheads - Tiny POP - CNBC - DATELINE 24/7 - True Lives - Crime Network - CNN - Trace UK - Masterchef UK: 'Powered by Banijay' - Comedy Hub - Vevo Christmas - Mystery TV - Rabbids Invasion - Crime &amp; Justice - MAVTV Motorsports Network - Qwest TV - INFAST - INWONDER - INWILD - Real Stories - History Hit - Wonder - Tennis Channel - Republic TV - Xplore - Jack Hanna - Toon Goggles - Real Wild - Real Crime - Motorvision.TV - Hollywire - Magellan TV Now - Billiard TV - Boxing TV - FIFA+ - Gusto TV - Bloomberg TV - Bloomberg Originals - MAVTV Motorsports Network - GoUSA TV - Film Stream - WeatherSpy - The Pet Collective - FailArmy - People are Awesome - INTROUBLE - Real Life - Nosey - Don't Tell The Bride - Fashion TV - Tastemade - Republic Bharat - True Crime Now - Discovery - Mastiii - WildEarth - Discovery HD - Animal Planet - Animal Planet HD - TLC HD - Investigation Discovery - ID-HD - Eurosport - Eurosport HD - Discovery Science - Discovery Turbo - Maiboli - NDTV 24X7 - NDTV India - Aaj Tak - Good News Today - India Today - NDTV Profit - NDTV GoodTimes - Balle Balle - 9X Tashan - 9X Jalwa - BEANI TV - FIGHT TV - Heritage - South Station - Cook &amp; Bake - Korean TV - Hooray Rhymes - Zee News - TLC - Discovery Kids - Discovery Tamil - 9X Music - 9X Jhakaas - HD TRAVEL - BEST ACTION TV - Hollywood Desi - Dabangg - Pitaara - BABY FIRST - Zee 24 Taas - Zee 24 Ghanta - Zee Business - ABP News - ABP Ananda - ABP Majha - ABP Asmita - Times Now Navbharat - CNBC AWAAZ - CNBC TV18 - CNN News18 - News18 India - TV9 Gujarati - Dangal TV - Bhojpuri Cinema - Pogo - CNN - TV9 Kannada - TV9 Telugu - TV9 Marathi - TV9 Bangla - News18 Gujarati - Zee 24 Kalak - WION - Divya - Cartoon Network - The Movie Club - News9 Live - TV9 Bharatvarsh - PGA Tour - Enterr10 Bangla - Tu Cine - Home.Made.Nation - NEW KPOP - Bloomberg TV+ UHD - Documentary+ - Cheddar News - Sony Canal Competencias - Yahoo Finance - Estrella TV - The LEGO Channel - Kitchen Nightmares - Mixible - Fireplace 4K - PBS Digital Studios - XITE Just Chill - XITE Rock On - K-Stories by CJ ENM - Hell's Kitchen - REELZ Famous &amp; Infamous - Dr. G: Medical Examiner - XITE 90s Throwback - CINEVAULT: Classics - Telemundo Al Día - Million Dollar Listing - The Rotten Tomatoes Channel - Project Runway - WN San Francisco - All-Out Reality - Dabl - CBS Sports HQ - Dateline 24/7 - Holiday Movie Favorites by Lifetime - Ax Men - Crimes Cults Killers - Modern Marvels Presented by History - Torque - UnXplained Zone - Moonbug - CNN Headlines - ViX Novelas de romance - Sky News - Crime ThrillHer - Deal Zone - 21 Jump Street - XITE 80s Flashback - GOLFPASS - Ice Road Truckers - Perform - T2 - Antiques Roadshow - World’s Most Evil Killers - Shades of Black - This Old House Makers - LOL! Network - Fear Zone - The Jack Hanna Channel - ViX JaJaJa - ViX Villanos de Novela - ViX Grandes Parejas - TED - Rovr Pets - Top Gear - NBC Bay Area News - NHRA TV - Cowboy Way - Tastemade Home - MotorTrend FAST TV - MyTime Movie Network - Dance Moms - Duck Dynasty - CBC News International - Just for Laughs GAGS - Localish - Haunt TV - ABC7 Bay Area - Strawberry Shortcake - Bob the Builder - Jamie Oliver - The Price is Right - Telemundo California - Operation Repo - CNN RESUMEN - Home Refresh - Estrella Games - RetroCrush - Canela.TV - ABC News Live - Military Heroes - HappyKids - Supermarket Sweep - Bon Appétit - ALTER - Cold Case Files - Matched Married Meet - The Biggest Loser - BET Pluto TV - ION - ION Plus - Divorce Court - Bounce XL - Vevo '70s - Vevo '80s - MSG SportsZone - ALLBLK Gems - FOX Weather - Degrassi - Gravitas Movies - The Bob Ross Channel - DraftKings Network - Midnight Pulp - Dove Channel - Tastemade Travel - BUZZR - Homeful - The Walking Dead Universe - All Weddings WE tv - OuterSphere - Vevo Holiday - History &amp; Warfare Now - Origin Sports - Outdoor America - Shout! Factory - The Design Network - BBC Earth - Storage Wars: LA - I Survived… - World Poker Tour - Noticias Univision 24/7 - Aquí y Ahora - Zona TUDN - Rebelde - MAVTV Select - Ebony TV by Lionsgate - POWERNATION - Cine Retro - Galanes - Pequenos Gigantes - Como Dice el Dicho - Cine de Oro - CBS News Bay Area - Swamp People - Forged In Fire - FOX 2 San Francisco - Scripps News - Vevo '90s - Vevo Retro Rock - XITE Hits - XITE Icons - XITE Country Today - XITE Nuevo Latino - XITE Only Love - XITE Siempre Latino - XITE Country's Finest - batteryPOP - Sensical Jr - Barney and Friends - Rainbow Ruby - Rev and Roll - Sensical Makers - HooplaKidzTV - Sonic The Hedgehog - Stingray Hot Country - Stingray Remember the 80s - Stingray Flashback 70s - Stingray Today's Latin Pop - Stingray Hip Hop - Stingray Easy Listening - Stingray Spa - Teletubbies - Stingray Today's K-Pop - Stingray Romance Latino - Stingray Greatest Holiday Hits - Qello Concerts by Stingray - Stingray DJAZZ - ZenLIFE by Stingray - Cheaters - Stingray Holidayscapes - AMC en Español - Comedy Dynamics - Tastemade - Portlandia - All Reality WE tv - pocket.watch - Billiard TV - Ryan and Friends - FIFA+ - Pursuit UP - Bring It! - Mountain Men - MovieSphere - Las 3 Marías - At Home with Family Handyman - ACC Digital Network - Alone By History - Slugterra - Stingray Smooth Jazz - Stingray Nothin' But 90s - Stingray Classic Rock - TMZ - 4UV - Conan O'Brien TV - Vevo 2010s - Little Women: LA - Hallmark Movies &amp; More - XITE R&amp;B Classic Jams - Baby Einstein - Stingray Classica - XITE Celebrates - Always Funny Videos - America's Test Kitchen - Anime All day - Backstage - Baywatch - BBC Food - BBC Home &amp; Garden - beIN SPORTS XTRA - Bloomberg Originals - Brat TV - Cars - CBS News - Chicken Soup for the Soul - Cine Romantico - CINEVAULT: 80s - CINEVAULT: Westerns - Circle - Clarity 4K - Court TV - Crime 360 - Dallas Cowboys Cheer - Danger TV - Deal or No Deal - Drama Life - Dry Bar Comedy - Alien Nation by DUST - ElectricNOW - Estrella News - FailArmy - Family Ties - Fear Factor - FilmRise Action - FilmRise Free Movies - FilmRise Western - Forensic Files - FOX SOUL - fubo Sports Network - Game Show Central - Gusto TV - Highway to Heaven - Heartland - Hollywire - Hungry - IGN - IMPACT Wrestling - INFAST - InWonder - Journy - Kidoodle.TV - Law &amp; Crime - LiveNOW from FOX - Loupe 4K - Love &amp; Hip Hop - Love Nature 4K - Lucky Dog - Magellan TV Now - Maverick Black Cinema - MHz Now - Midsomer Murders - Pluto TV Pixel World - MTV Pluto TV - NBC LX Home - NBC News NOW - NEW KMOVIES - Newsmax TV - Nick Pluto TV - Nosey - Outside - Pac-12 Insider - Paramount Movie Channel - People Are Awesome - Pluto TV Fantastic - Pluto TV Westerns - Real America's Voice - Revry - RiffTrax - Samsung Wild Life - Xtreme Outdoor Presented by HISTORY - Sony Canal Comedias - Sony Canal Novelas - SportsGrid - Stadium - Stingray Naturescape - SURF NOW TV - TG Junior - The Asylum - The Challenge - The New Detectives - The Pet Collective - The Preview Channel - This Old House - Tiny House Nation - TODAY All Day - Toon Goggles - TV Land Drama - TV Land Sitcoms - TYT Network - Unidentified - Unsolved Mysteries - USA Today - Vevo 2K - Vevo Country - Vevo Hip-Hop - Vevo Latino - Vevo Pop - Vevo R&amp;B - VICE - Waypoint TV - WeatherSpy - Wild 'N Out - Wipeout Xtra - Xplore - ZooMoo - Top Gear en Español - Bloomberg Originals - Estilo y Vida – Rakuten TV - Pluto TV Cine Estelar - MTV Catfish - Comedia Made in Spain - MTV Cribs - Pluto TV Cocina - Pluto TV Animakids - The Pet Collective - Fashion TV - Comedias - Rakuten TV - Dramas - Rakuten TV - Documentales - Rakuten TV - People Are Awesome - Euronews en directo - Pluto TV Kids - Acción - Rakuten TV - INFAST - Qwest TV - MTV Originals - Doctor Who - BBC Drama - FailArmy - Yu-Gi-Oh! - ¡Hola! Play - Deluxe Lounge HD - MyTime Movie Network - Tastemade - Cortos - 100% Navidad - Rakuten TV - Caillou - Bloomberg TV+ - Los Asesinatos de Midsomer - Las Reglas Del Juego - iCarly - El Comisario - Encantador de Perros - Trace Sportstars - BelAir TV - Tu Cine - Trace Latina - Trace Urban - Nature Time - SuperToons TV - Bob Esponja - Rugrats - Surf Channel - Stormcast Novelas - Flash - WildEarth - Clubbing TV - Runtime - NBC News NOW - El Confidencial - Vive Kanal D Drama - Televisión Consciente - Trailers - Vevo Pop - Vevo Latino - Grjngo - Películas Del Oeste - Bigtime - Películas Gratis - Películas Top - Rakuten TV - Just For Laughs - FIFA+ - Crimen - Rakuten TV - Cines Verdi TV - Cine Feel Good – Verdi TV - Vivaldi TV - Ideas en 5 Minutos - MyPadel TV - Vivir con Perros - Vivir con Gatos - Pocoyó - La 1 - La 2 - Clan - 24H - duckTV - Love the Planet - Teen Vee - GoUSA TV - Cine Español - Rakuten TV - Películas Románticas - Rakuten TV - Ticker News - El País - Dark Matter TV - Todo Novelas - WaterBear - Teledeporte - Cine Friki - Runtime Romance - FRANCE 24 FAST - Baywatch – Los Vigilantes de la Playa - Corazón - Azteca Internacional - Travelxp - Mi chimenea - Rabbids : La invasion - Sol Música - Tennis Channel - World Poker Tour - Motorvision - Heidi &amp; Maya - Negocios TV - La Liga - DATELINE 24/7 - Vevo Navidad - Runtime Comedia - CNBC - Runtime Acción - Pitufo TV - CNN FAST - Cine Clásico - Vevo '70s &amp; '80s - Pluto TV Ciné - MTV Classics - Bob L'eponge - Fashion TV - Films d'action - Rakuten TV - Comédies - Rakuten TV - Films dramatiques - Rakuten TV - Documentaires - Rakuten TV - Pluto TV Kids Séries - MTV Catfish - iCarly - Pluto TV Polar - Euronews en direct - BBC Drama - Brefcinema - Deluxe Lounge HD - The Boat Show - Qwest TV - ADN - UniversCiné - MyTime Movie Network - Xilam TV - Doctor Who - Wild Side TV - Televisa TeleNovelas - Reuters - 100% Noël - Rakuten TV - Caillou - SuperToons TV - Stormcast Novelas - Bloomberg TV+ - Top Films - Rakuten TV - Juste Pour Rire - FIFA+ - Yu-Gi-Oh! - Pluto TV Cuisine - Tortues Ninja - Wellbeing TV - BelAir TV - People Are Awesome - FailArmy - Films Romantiques - Rakuten TV - Crime - Rakuten TV - Travelxp - Clubbing TV - Chefclub TV - Jellytoon - Scream'IN - Nature Time - Ciné Nanar - Gong - Homicide - Tahiti TV - Runtime - Emotion'L - Motus - Fréquence Novelas - Émotion - Y'a que la vérité qui compte - Le Figaro Live - Vevo Pop - Vevo Hip-Hop &amp; R&amp;B - Ciné Prime - Secret Story - MasterChef - Echappées belles &amp; co - MGG FAST - Love the Planet - Destination Nature - L'Equipe Live 1 - L'Equipe Live 2 - Génération Sitcoms - Alerte à Malibu - A prendre ou à laisser - Les 30 histoires - Trailers - Tele.Novela - Un Village Français - Les filles d'à côté - Enquêtes de Choc - Nashville Channel - Le Tigre - Vialma - Au coin de feu - Les Z'amours - Le meilleur d'Arthur - Qui veut gagner des millions ? - Charlotte aux Fraises - Vevo '90s &amp; '00s - Family Club - Les Lapins Crétins: Invasion - Maison &amp; Travaux - World Poker Tour - Motorvision - Drive TV - Allociné - Reportages by Spica - Le Meilleur de la TV Réalité - Les Anges - Popcorn - Motor Racing - L'effet papillon - Passion Novelas - Ciné Western - Degrassi - Top Santé - Plus belle la vie - Vevo Noël - Scènes de Crime - 100% Docs - Wasabi - Culture Pub - CNN FAST - Ardivision - Les secrets de nos régions - BBC Drama - Catfish - Super! iCarly - Pluto TV Real Life - Pluto TV Serie - Serie Teen - Pluto TV Film Romantici - Super! Pop - The Pet Collective - Fashion TV - Commedia - Rakuten TV - Drama - Rakuten TV - Documentari - Rakuten TV - People Are Awesome - Euronews in diretta - Motor1TV - Pluto TV Film - Film d'azione - Rakuten TV - Super! Spongebob - FailArmy - Canale Europa - Yamato Animation - Teletubbies - Deluxe Lounge HD - HorizonSports - Qwest TV - RADIO ITALIA TREND - Sportitalia - SuperToons TV - MONDO TV KIDS - SportOutdoor.tv - Bizzarro Movies - Bloomberg TV+ - Vivaldi TV - Ticker News - BelAir TV - Trace Urban - Trace Latina - WildEarth - Trailers - Televisa Telenovelas - 100% Natale - Rakuten TV - CG - CINEMA d'Autore - Cinema Segreto - 5-Minuti Creativi - Wellbeing TV - WaterBear - Clubbing TV - Italian Fishing TV - Doctor Who - Giornale Radio TV - WP - Dark Matter TV - Brindiamo! - Cmusic - Montagna! - Alta Tensione - Smile - Grandi Nomi - Explorer HD Channel - Top Film - Rakuten TV - Risate all'italiana - Romance - Rakuten TV - Rock TV - Vevo Pop - Pluto TV Film Commedia - Pluto TV Film Azione - Consulenze Illegali - TVPlay - Teen Vee - NBC News NOW - duckTV - Solocalcio - Love the Planet - Film Al Femminile - Wedo Movies - Velvet - Full Moon - Hip Hop TV - Pluto TV Reality - Grandi Documentari - Wedo Big Stories - Vevo '90s &amp; '00s - Move - Italian Active Lives - Serially Drama - Just For Laughs - Serie Crime - Rakuten TV - FIFA+ - Yu-Gi-Oh! - Serially Crime - House of Docs - FRANCE 24 FAST - Travel &amp; Living by DOVE - Baywatch - U-Trend - Travelxp - CineMadame - La 7 - Serie TV asiatiche d’azione by Liv TV - Lifestyle by LEI - Vevo '70s &amp; '80s - RBN - Cinema Excelsior - Le Vite Degli Altri - Andromeda - Autostop per il Cielo - Mutant X - Heidi &amp; Maya - GCTV (Global Champions TV) - CNBC - DATELINE 24/7 - Vevo Natale - East is East - Dolomiti Life TV - Grjngo- Film Western - The Boat Show - Caminetto - CNN FAST - World Poker Tour - Yu yu Hakusho - Film e Sorrisi - 현대홈쇼핑 - 현대홈쇼핑+Shop - MBC every1 어서와 한국은 처음이지? - 코코비TV - MBC every1 별순검 - Bloomberg TV+ UHD - Bloomberg Originals - SBS 런닝맨 - SBS 미운 우리 새끼 - SBS 순풍산부인과 - SBS 펜트하우스 - SBS 동상이몽2 - 너는 내 운명 - SBS 나는 솔로 - SBS 백종원의 골목식당 - SBS 정글의 법칙 - SBS 순간포착 세상에 이런일이 - SBS 패밀리가 떴다 - SBS 궁금한 이야기 Y - SBS 그것이 알고싶다 - SBS 생활의 달인 - MBC 무한도전 - MBC 돈꽃 - MBC 옷소매 붉은끝동 - 초록뱀미디어 K-STAR 골프 - SBS 편의점 샛별이 - SBS 불타는 청춘 - MBC 심야괴담회 - MBC 선을 넘는 녀석들 - SBS 스토브리그 - SBS TV 동물농장 - SBS 빽드 - MBC 나혼자산다 - MBC 지붕뚫고 하이킥 - 투니버스 뱀파이어소녀 달자 - tvN 대탈출3 - MBC 거침없이 하이킥 - 꽃보다 남자 - NEW MOVIES - OGN 스타리그 - 맛있는 녀석들 - TV CHOSUN 식객 허영만의 백반기행 - KBS Joy 무엇이든 물어보살 - World Billiards TV - tvN 응답하라 전 시즌 모아보기 - tvN 갯마을 차차차 - tvN 삼시세끼 산촌편 - Mnet 스트릿 우먼 파이터 - Mnet 스트릿 우먼 파이터 2 HOT CLIP - 심리 읽어드립니다 - tvN 호텔 델루나 - tvN 스트리트 푸드 파이터 1~2 - tvN 미스터 션샤인 - tvN 사랑의 불시착 - tvN 빈센조 - tvN 스물다섯 스물하나 - tvN 여신강림 - 로보카폴리 TV - 고독한 미식가 - 도라마코리아 - MBN 나는 자연인이다 - MBN 휴먼다큐 사노라면 - MBN 속풀이쇼 동치미 - MBN 돌싱글즈 - iHQ 돈쭐내러 왔습니다 - 전우치 - KBS Joy 연애의 참견 - 채널A 나만 믿고 따라와 도시어부 - 채널A 이제 만나러 갑니다 - E채널 토요일은 밥이 좋아 - KBS 개는 훌륭하다 - KBS 쌈마이웨이 - KBS 1박2일 시즌1 - tvN 작은 아씨들 - tvN 슬기로운 의사생활 시즌2 - JTBC 괴물 - JTBC 힘쎈여자 도봉순 - JTBC 효리네 민박 시즌1 - JTBC 골프 - JTBC 최강야구 - JTBC 비긴어게인 - Mnet 스트릿댄스 걸스 파이터 + Zㅏ때는 말이야 - MBC 안싸우면 다행이야 - MBC 나는 가수다 - ch.핑크퐁 - tvN 어쩌다 사장 전 시즌 몰아보기 - 사피엔스스튜디오 - 역사 읽어드립니다 - tvN 신서유기 6 - tvN 놀라운 토요일 하이라이트 - TV CHOSUN 국가가 부른다 - Animax 반지의 비밀일기 - 채널A 요즘 육아 금쪽같은 내새끼 - JTBC 크라임씬 - JTBC 뉴스 - JTBC 톡파원25시 - JTBC 품위있는 그녀 - tvN 환혼 - 연합뉴스TV - FIFA+ - 뽀요TV - 우리의식탁 - YTN - TV조선 빨간 풍선 - TV조선 골프왕 - essential; by Bugs - PLAYY 영화 - PLAYY 어워드특집 - tvN 유 퀴즈 온 더 블럭 HOT CLIP - 글로벌 뉴스 by LeadStory - PGA Tour - 씨네21+ - TV조선 미스터트롯 영웅의 탄생 - TED - + + + Deluxe Lounge HD + Focus TV + Netzkino + Tierwelt + Xplore + Hip Trips + just.fishing + Sportdigital Free + Just Cooking + SPIEGEL TV + GoldStar TV + Charlie + Grjngo + Tempora + auto motor und sport + MTV Pluto TV + Teen Nick + SpongeBob Schwammkopf + Ice Pilots + The Pet Collective + Actionfilme - Rakuten TV + Drama Filme - Rakuten TV + Nick Pluto TV + iCarly + MTV Teen Mom + FailArmy + Komödien - Rakuten TV + Dokumentarfilme - Rakuten TV + Pluto TV Serie + Pluto TV Paranormal + Pluto TV Animals + Pluto TV Food + Pluto TV Science + Pluto TV Sitcoms + Comedy Central Pluto TV + Pluto TV Movies + Comedy Central Made in Germany + MTV Catfish + People Are Awesome + Euronews Live + Travelxp + Yu-Gi-Oh! + Fantasja + Fabella + One Terra + Bergblick Free + Deluxe Deutschpop + Qwest TV + INFAST + Tastemade + Tennis Channel + Zee One + FIFA+ + CURIOSITY Now + SPIEGEL TV Konflikte + Strongman Champions League + Deutsches Kino - Rakuten TV + Bloomberg TV+ + MyTime Movie Network + wedo movies + Horse &amp; Country + Wilder Planet + Fashion TV + Top Filme - Rakuten TV + Moconomy + Heimatkino + INWONDER + XITE Hits + Crime Serien - Rakuten TV + Teletubbies + Comedy &amp; Shows + Craction TV + Moviedome + 100% Weihnachten - Rakuten TV + Moviedome Family + Clubbing TV + Crime Mix + Qello Concerts by Stingray + Stars in Gefahr + The Wicked Tuna Channel + Naruto + Spannung &amp; Emotionen + Filmgold + CNN + FRANCE 24 FAST + Kaminfeuer + Cine Mix + Red Bull TV + Sport 1 Motor + Out TV + Hell's Kitchen + Sallys Welt + Baywatch + More than Sports TV + Motorvision Classic + Motorvision + Terra Mater + X-MAS Mix + Deluxe Wintertime + CNN FAST + World Poker Tour + Aniverse + Deluxe Lounge HD + Focus TV + Netzkino + Tierwelt + Xplore + Hip Trips + just.fishing + Sportdigital Free + Just Cooking + SPIEGEL TV + GoldStar TV + Charlie + Grjngo + Tempora + auto motor und sport + MTV Pluto TV + Nick Pluto TV + Teen Nick + iCarly + Pluto TV Serie + Ice Pilots + MTV Teen Mom + The Pet Collective + WP + Drama Filme - Rakuten TV + Dokumentarfilme - Rakuten TV + SpongeBob Schwammkopf + Pluto TV Paranormal + FailArmy + Actionfilme - Rakuten TV + Komödien - Rakuten TV + Pluto TV Animals + Pluto TV Food + Pluto TV Science + Pluto TV Sitcoms + Comedy Central Pluto TV + Pluto TV Movies + Comedy Central Made in Germany + MTV Catfish + People Are Awesome + Travelxp + Fashion TV + Travelxp [FR] + Top Filme - Rakuten TV + Fabella + One Terra + Heimatkino + Bergblick Free + Deluxe Deutschpop + Qwest TV + INFAST + INWONDER + Tastemade + ADN [FR] + SportOutdoor.tv [IT] + FIFA+ + Strongman Champions League + Teletubbies + Risate all'italiana + Bloomberg TV+ + BelAir TV [FR] + Wellbeing TV [FR] + wedo movies + Craction TV + Wilder Planet + Moviedome Family + Horse &amp; Country + Moconomy + Tennis Channel + Deutsches Kino - Rakuten TV + Crime Serien - Rakuten TV + Euronews Live + Fantasja + 100% Weihnachten - Rakuten TV + Moviedome + Clubbing TV + Emotion'L [FR] + Qello Concerts by Stingray + Chefclub TV [FR] + Ciné Nanar [FR] + Motus [FR] + Spannung &amp; Emotionen + Comedy &amp; Shows + Scream'IN [FR] + Y'a que la vérité qui compte [FR] + Stars in Gefahr + Crime Mix + MyTime Movie Network + Trace Latina [FR] + Gong [FR] + Fréquence Novelas [FR] + Ciné Prime [FR] + Enquêtes de Choc [FR] + Filmgold + Homicide [FR] + Naruto + Destination Nature [FR] + Émotion [FR] + Le Figaro Live [FR] + The Wicked Tuna Channel + Echappées belles &amp; co [FR] + Grandi Documentari - Wedo Big Stories [IT] + Nature Time [FR] + Génération Sitcoms [FR] + Les filles d'à côté [FR] + Film Al Femminile - Wedo Movies [IT] + CNN + Le Tigre [FR] + Vialma [FR] + FRANCE 24 FAST + CURIOSITY Now + SPIEGEL TV Konflikte + Yu-Gi-Oh! + CNN FAST + Screen Green + Alerte à Malibu [FR] + Sport 1 Motor + Out TV + Sallys Welt + Baywatch + More than Sports TV + Motorvision Classic + Terra Mater + World Poker Tour + X-MAS Mix + Plus belle la vie [FR] + Culture Pub [FR] + Hell's Kitchen + Motorvision + Zee One + Deluxe Wintertime + Aniverse + Kaminfeuer + Deluxe Lounge HD + Focus TV + Netzkino + Xplore + Tierwelt + Hip Trips + just.fishing + Sportdigital Free + Nick Pluto TV + Teen Nick + iCarly + Pluto TV Serie + Pluto TV Paranormal + SPIEGEL TV Konflikte + Yu-Gi-Oh! + Zee One + FailArmy + The Pet Collective + Actionfilme - Rakuten TV + Komödien - Rakuten TV + Drama Filme - Rakuten TV + People Are Awesome + MTV Pluto TV + Ice Pilots + MTV Teen Mom + Terra Mater + Dokumentarfilme - Rakuten TV + SpongeBob Schwammkopf + Just Cooking + SPIEGEL TV + GoldStar TV + Charlie + Grjngo + Tempora + auto motor und sport + Comedy Central Pluto TV + Comedy Central Made in Germany + MTV Catfish + Pluto TV Animals + Pluto TV Science + Pluto TV Sitcoms + Pluto TV Movies + Euronews Live + BBC History + BBC Travel + BBC Food + Moconomy + Fantasja + Heimatkino + INFAST + INWONDER + Qwest TV + Tastemade + XITE Hits + Tennis Channel Deutschland + Pluto TV Food + CURIOSITY Now + Top Filme - Rakuten TV + Strongman Champions League + Deutsches Kino - Rakuten TV + Crime Serien - Rakuten TV + FIFA+ + Reuters + Fashion TV + Teletubbies + SuperToons TV + Bloomberg TV+ + Trace Urban + MyTime Movie Network + wedo movies + Craction TV + Moviedome + Moviedome Family + Wilder Planet + Fabella + One Terra + Bergblick Free + 100% Weihnachten - Rakuten TV + Horse &amp; Country + Travelxp + Deluxe Deutschpop + Clubbing TV + Crime Mix + Qello Concerts by Stingray + Cine Mix + Vevo Pop + Spannung &amp; Emotionen + Comedy &amp; Shows + Stars in Gefahr + Red Bull TV + Terra X + Love the Planet + duckTV + Vevo Schlager Pop + Filmgold + Hell's Kitchen + The Wicked Tuna Channel + CNN + FRANCE 24 FAST + Kaminfeuer + Screen Green + Sport 1 Motor + Out TV + Sallys Welt + Motorvision Classic + Motorvision + DEFA TV + XITE Rock On + X-MAS Mix + ZDF kocht! + Deluxe Wintertime + Naruto + CNN FAST + Baywatch + More than Sports TV + World Poker Tour + Bares für Rares + Aniverse + The Asylum + FilmRise Classic TV + NBC News NOW + Forensic Files + FilmRise Action + Game Show Central + FailArmy + Outside + Law&amp;Crime + Stingray Naturescape + The Red Green Channel + WeatherSpy + CINEVAULT: Classics + The Preview Channel + Dry Bar Comedy + The Design Network + MagellanTV Now + FilmRise True Crime + Unsolved Mysteries + FilmRise Western + People Are Awesome + Kidoodle.TV + MHz Now + FilmRise Free Movies + The Pet Collective + INFAST + InWonder + Haunt TV + Always Funny Videos + Baywatch + Moonbug + Court TV + IMPACT Wrestling + beIN SPORTS XTRA + Crime Time + Nosey + TED + Gusto TV + Deal or No Deal + Operation Repo + Alien Nation by DUST + Journy + Waypoint TV + XITE Just Chill + Hell's Kitchen + Dr. G: Medical Examiner + XITE 80s Flashback + XITE 90s Throwback + Circle + This Old House + XITE Rock On + 21 Jump Street + LOL! Network + Fireplace 4K + The Jack Hanna Channel + Xplore + Top Gear + NHRA TV + Cowboy Way + MotorTrend FAST TV + CBC News Explore + Radio-Canada INFO + RetroCrush + Origin Sports + Bob Ross + HappyKids + Documentary+ + Supermarket Sweep + The Biggest Loser + Tastemade Home + Strawberry Shortcake + Bon Appétit + Divorce Court + Architectural Digest + Vevo '70s + Vevo '80s + Vevo 2K + Vevo Retro Rock + Vevo R&amp;B + Comedy Dynamics + Midnight Pulp + Homeful + Hollywire + Midsomer Murders + Cops + The Weather Network + Bob the Builder + Vevo '90s + Vevo Country + Vevo Hip-Hop + Vevo Pop + LEGO Channel + Gravitas Movies + Tastemade + Antiques Roadshow + FIFA+ + Vevo 2010s + BBC Food + BBC Home &amp; Garden + World Poker Tour + MAVTV Select + POWERNATION + XITE Country's Finest + XITE Hits + XITE Icons + Baby Einstein + Cheaters + CBC Comedy + Highway to Heaven + XITE Only Love + batteryPOP + pocket.watch + XITE Country Today + Pluto TV Motor + Sparkle Movies + Popflix + Pluto TV Animals + Pluto TV Food + The Guardian + Fashion TV + Planet Knowledge + Action Movies - Rakuten TV + Comedy Movies - Rakuten TV + Documentaries - Rakuten TV + Filmstream + People Are Awesome + Euronews Live + The Pet Collective + FailArmy + INTROUBLE + INFAST + INWONDER + Drama Movies - Rakuten TV + Kidoodle.TV + Travelxp + GoUSA TV + 5 Cops + 5 Exploring Britain + Pluto TV Action + Pluto TV Movies + Pluto TV Drama + Pluto TV Crime + Pluto TV Inside + Pluto TV Romance + Pluto TV Classic TV + MotorRacing + Qwest TV + Real Stories + History Hit + Horse &amp; Country + Romance Movies - Rakuten TV + Revry + 100% Christmas - Rakuten TV + YAAAS! + SuperToons TV + Bloomberg TV+ + Homicide Hunter + wedo movies + MyTime Movie Network + Real Crime + Deluxe Lounge HD + Wonder + Reuters + Teletubbies + Pluto TV Conspiracy + Pluto TV Paranormal + Tennis Channel International + PBS History + MovieSphere + INWILD + Gusto TV + Beano TV + Catfish + TalkTV + Real Wild + Choppertown + Qello Concerts by Stingray + NBC News NOW + PGA Tour + Are We There Yet? + GB News + Bridezillas + Clubbing TV + Ketchup TV + Moviedome + Shades of Black + XITE Hits + Survivor + McLeod's Daughters + Viaplay Xtra + Project Runway + Wild Planet + Comedy Dynamics + Deal or No Deal US + Tastemade + World Drama by ITV Studios + Shorts + Origin Sports + Hell's Kitchen + WaterBear + Trace Urban + Horizons + Now 80s + The Wicked Tuna Channel + Haunt TV + GREAT! movies + GREAT! Christmas + Radical Docs + LOL! Network + Bloomberg Originals + Homes Under the Hammer + Great British Menu + Adventure Earth + Vevo '90s &amp; '00s + American Idol + America's Got Talent + Entertainment Hub + Mythbusters + The Jamie Oliver Channel + Toon Goggles + WildEarth + Real Life + The LEGO Channel + Come Dine With Me + Vevo Pop + Strongman Champions League + Top Movies - Rakuten TV + Festive Hub + POP + Love the Planet + Inside Crime + Mech+ + Grjngo - Western Movies + Baywatch + World War TV + FRANCE 24 FAST + Love Pets + pocket.watch + Real Series - Rakuten TV + True Crime UK + FIFA+ + So…Real + Super Anime + Dennis and Gnasher + Smurf TV + Fireplace + The Chat Show Channel + CNN FAST + Vevo '70s &amp; '80s + Vevo Hip Hop &amp; R&amp;B + Red Bull TV + Pointless UK: 'Powered by Banijay' + World Poker Tour + Wipeout Xtra Powered by Banijay + Gigs + UKTV Play Full Throttle + UKTV Play Heroes + UKTV Play Laughs + UKTV Play Uncovered + Challenge + Sky Mix + Earth Touch + Eggheads + Tiny POP + CNBC + DATELINE 24/7 + True Lives + Crime Network + CNN + Trace UK + Masterchef UK: 'Powered by Banijay' + Comedy Hub + Vevo Christmas + Mystery TV + Rabbids Invasion + Crime &amp; Justice + MAVTV Motorsports Network + Qwest TV + INFAST + INWONDER + INWILD + Real Stories + History Hit + Wonder + Tennis Channel + Republic TV + Xplore + Jack Hanna + Toon Goggles + Real Wild + Real Crime + Motorvision.TV + Hollywire + Magellan TV Now + Billiard TV + Boxing TV + FIFA+ + Gusto TV + Bloomberg TV + Bloomberg Originals + MAVTV Motorsports Network + GoUSA TV + Film Stream + WeatherSpy + The Pet Collective + FailArmy + People are Awesome + INTROUBLE + Real Life + Nosey + Don't Tell The Bride + Fashion TV + Tastemade + Republic Bharat + True Crime Now + Discovery + Mastiii + WildEarth + Discovery HD + Animal Planet + Animal Planet HD + TLC HD + Investigation Discovery + ID-HD + Eurosport + Eurosport HD + Discovery Science + Discovery Turbo + Maiboli + NDTV 24X7 + NDTV India + Aaj Tak + Good News Today + India Today + NDTV Profit + NDTV GoodTimes + Balle Balle + 9X Tashan + 9X Jalwa + BEANI TV + FIGHT TV + Heritage + South Station + Cook &amp; Bake + Korean TV + Hooray Rhymes + Zee News + TLC + Discovery Kids + Discovery Tamil + 9X Music + 9X Jhakaas + HD TRAVEL + BEST ACTION TV + Hollywood Desi + Dabangg + Pitaara + BABY FIRST + Zee 24 Taas + Zee 24 Ghanta + Zee Business + ABP News + ABP Ananda + ABP Majha + ABP Asmita + Times Now Navbharat + CNBC AWAAZ + CNBC TV18 + CNN News18 + News18 India + TV9 Gujarati + Dangal TV + Bhojpuri Cinema + Pogo + CNN + TV9 Kannada + TV9 Telugu + TV9 Marathi + TV9 Bangla + News18 Gujarati + Zee 24 Kalak + WION + Divya + Cartoon Network + The Movie Club + News9 Live + TV9 Bharatvarsh + PGA Tour + Enterr10 Bangla + Tu Cine + Home.Made.Nation + NEW KPOP + Bloomberg TV+ UHD + Documentary+ + Cheddar News + Sony Canal Competencias + Yahoo Finance + Estrella TV + The LEGO Channel + Kitchen Nightmares + Mixible + Fireplace 4K + PBS Digital Studios + XITE Just Chill + XITE Rock On + K-Stories by CJ ENM + Hell's Kitchen + REELZ Famous &amp; Infamous + Dr. G: Medical Examiner + XITE 90s Throwback + CINEVAULT: Classics + Telemundo Al Día + Million Dollar Listing + The Rotten Tomatoes Channel + Project Runway + WN San Francisco + All-Out Reality + Dabl + CBS Sports HQ + Dateline 24/7 + Holiday Movie Favorites by Lifetime + Ax Men + Crimes Cults Killers + Modern Marvels Presented by History + Torque + UnXplained Zone + Moonbug + CNN Headlines + ViX Novelas de romance + Sky News + Crime ThrillHer + Deal Zone + 21 Jump Street + XITE 80s Flashback + GOLFPASS + Ice Road Truckers + Perform + T2 + Antiques Roadshow + World’s Most Evil Killers + Shades of Black + This Old House Makers + LOL! Network + Fear Zone + The Jack Hanna Channel + ViX JaJaJa + ViX Villanos de Novela + ViX Grandes Parejas + TED + Rovr Pets + Top Gear + NBC Bay Area News + NHRA TV + Cowboy Way + Tastemade Home + MotorTrend FAST TV + MyTime Movie Network + Dance Moms + Duck Dynasty + CBC News International + Just for Laughs GAGS + Localish + Haunt TV + ABC7 Bay Area + Strawberry Shortcake + Bob the Builder + Jamie Oliver + The Price is Right + Telemundo California + Operation Repo + CNN RESUMEN + Home Refresh + Estrella Games + RetroCrush + Canela.TV + ABC News Live + Military Heroes + HappyKids + Supermarket Sweep + Bon Appétit + ALTER + Cold Case Files + Matched Married Meet + The Biggest Loser + BET Pluto TV + ION + ION Plus + Divorce Court + Bounce XL + Vevo '70s + Vevo '80s + MSG SportsZone + ALLBLK Gems + FOX Weather + Degrassi + Gravitas Movies + The Bob Ross Channel + DraftKings Network + Midnight Pulp + Dove Channel + Tastemade Travel + BUZZR + Homeful + The Walking Dead Universe + All Weddings WE tv + OuterSphere + Vevo Holiday + History &amp; Warfare Now + Origin Sports + Outdoor America + Shout! Factory + The Design Network + BBC Earth + Storage Wars: LA + I Survived… + World Poker Tour + Noticias Univision 24/7 + Aquí y Ahora + Zona TUDN + Rebelde + MAVTV Select + Ebony TV by Lionsgate + POWERNATION + Cine Retro + Galanes + Pequenos Gigantes + Como Dice el Dicho + Cine de Oro + CBS News Bay Area + Swamp People + Forged In Fire + FOX 2 San Francisco + Scripps News + Vevo '90s + Vevo Retro Rock + XITE Hits + XITE Icons + XITE Country Today + XITE Nuevo Latino + XITE Only Love + XITE Siempre Latino + XITE Country's Finest + batteryPOP + Sensical Jr + Barney and Friends + Rainbow Ruby + Rev and Roll + Sensical Makers + HooplaKidzTV + Sonic The Hedgehog + Stingray Hot Country + Stingray Remember the 80s + Stingray Flashback 70s + Stingray Today's Latin Pop + Stingray Hip Hop + Stingray Easy Listening + Stingray Spa + Teletubbies + Stingray Today's K-Pop + Stingray Romance Latino + Stingray Greatest Holiday Hits + Qello Concerts by Stingray + Stingray DJAZZ + ZenLIFE by Stingray + Cheaters + Stingray Holidayscapes + AMC en Español + Comedy Dynamics + Tastemade + Portlandia + All Reality WE tv + pocket.watch + Billiard TV + Ryan and Friends + FIFA+ + Pursuit UP + Bring It! + Mountain Men + MovieSphere + Las 3 Marías + At Home with Family Handyman + ACC Digital Network + Alone By History + Slugterra + Stingray Smooth Jazz + Stingray Nothin' But 90s + Stingray Classic Rock + TMZ + 4UV + Conan O'Brien TV + Vevo 2010s + Little Women: LA + Hallmark Movies &amp; More + XITE R&amp;B Classic Jams + Baby Einstein + Stingray Classica + XITE Celebrates + Always Funny Videos + America's Test Kitchen + Anime All day + Backstage + Baywatch + BBC Food + BBC Home &amp; Garden + beIN SPORTS XTRA + Bloomberg Originals + Brat TV + Cars + CBS News + Chicken Soup for the Soul + Cine Romantico + CINEVAULT: 80s + CINEVAULT: Westerns + Circle + Clarity 4K + Court TV + Crime 360 + Dallas Cowboys Cheer + Danger TV + Deal or No Deal + Drama Life + Dry Bar Comedy + Alien Nation by DUST + ElectricNOW + Estrella News + FailArmy + Family Ties + Fear Factor + FilmRise Action + FilmRise Free Movies + FilmRise Western + Forensic Files + FOX SOUL + fubo Sports Network + Game Show Central + Gusto TV + Highway to Heaven + Heartland + Hollywire + Hungry + IGN + IMPACT Wrestling + INFAST + InWonder + Journy + Kidoodle.TV + Law &amp; Crime + LiveNOW from FOX + Loupe 4K + Love &amp; Hip Hop + Love Nature 4K + Lucky Dog + Magellan TV Now + Maverick Black Cinema + MHz Now + Midsomer Murders + Pluto TV Pixel World + MTV Pluto TV + NBC LX Home + NBC News NOW + NEW KMOVIES + Newsmax TV + Nick Pluto TV + Nosey + Outside + Pac-12 Insider + Paramount Movie Channel + People Are Awesome + Pluto TV Fantastic + Pluto TV Westerns + Real America's Voice + Revry + RiffTrax + Samsung Wild Life + Xtreme Outdoor Presented by HISTORY + Sony Canal Comedias + Sony Canal Novelas + SportsGrid + Stadium + Stingray Naturescape + SURF NOW TV + TG Junior + The Asylum + The Challenge + The New Detectives + The Pet Collective + The Preview Channel + This Old House + Tiny House Nation + TODAY All Day + Toon Goggles + TV Land Drama + TV Land Sitcoms + TYT Network + Unidentified + Unsolved Mysteries + USA Today + Vevo 2K + Vevo Country + Vevo Hip-Hop + Vevo Latino + Vevo Pop + Vevo R&amp;B + VICE + Waypoint TV + WeatherSpy + Wild 'N Out + Wipeout Xtra + Xplore + ZooMoo + Top Gear en Español + Bloomberg Originals + Estilo y Vida – Rakuten TV + Pluto TV Cine Estelar + MTV Catfish + Comedia Made in Spain + MTV Cribs + Pluto TV Cocina + Pluto TV Animakids + The Pet Collective + Fashion TV + Comedias - Rakuten TV + Dramas - Rakuten TV + Documentales - Rakuten TV + People Are Awesome + Euronews en directo + Pluto TV Kids + Acción - Rakuten TV + INFAST + Qwest TV + MTV Originals + Doctor Who + BBC Drama + FailArmy + Yu-Gi-Oh! + ¡Hola! Play + Deluxe Lounge HD + MyTime Movie Network + Tastemade + Cortos + 100% Navidad - Rakuten TV + Caillou + Bloomberg TV+ + Los Asesinatos de Midsomer + Las Reglas Del Juego + iCarly + El Comisario + Encantador de Perros + Trace Sportstars + BelAir TV + Tu Cine + Trace Latina + Trace Urban + Nature Time + SuperToons TV + Bob Esponja + Rugrats + Surf Channel + Stormcast Novelas + Flash + WildEarth + Clubbing TV + Runtime + NBC News NOW + El Confidencial + Vive Kanal D Drama + Televisión Consciente + Trailers + Vevo Pop + Vevo Latino + Grjngo - Películas Del Oeste + Bigtime - Películas Gratis + Películas Top - Rakuten TV + Just For Laughs + FIFA+ + Crimen - Rakuten TV + Cines Verdi TV + Cine Feel Good – Verdi TV + Vivaldi TV + Ideas en 5 Minutos + MyPadel TV + Vivir con Perros + Vivir con Gatos + Pocoyó + La 1 + La 2 + Clan + 24H + duckTV + Love the Planet + Teen Vee + GoUSA TV + Cine Español - Rakuten TV + Películas Románticas - Rakuten TV + Ticker News + El País + Dark Matter TV + Todo Novelas + WaterBear + Teledeporte + Cine Friki + Runtime Romance + FRANCE 24 FAST + Baywatch – Los Vigilantes de la Playa + Corazón + Azteca Internacional + Travelxp + Mi chimenea + Rabbids : La invasion + Sol Música + Tennis Channel + World Poker Tour + Motorvision + Heidi &amp; Maya + Negocios TV + La Liga + DATELINE 24/7 + Vevo Navidad + Runtime Comedia + CNBC + Runtime Acción + Pitufo TV + CNN FAST + Cine Clásico + Vevo '70s &amp; '80s + Pluto TV Ciné + MTV Classics + Bob L'eponge + Fashion TV + Films d'action - Rakuten TV + Comédies - Rakuten TV + Films dramatiques - Rakuten TV + Documentaires - Rakuten TV + Pluto TV Kids Séries + MTV Catfish + iCarly + Pluto TV Polar + Euronews en direct + BBC Drama + Brefcinema + Deluxe Lounge HD + The Boat Show + Qwest TV + ADN + UniversCiné + MyTime Movie Network + Xilam TV + Doctor Who + Wild Side TV + Televisa TeleNovelas + Reuters + 100% Noël - Rakuten TV + Caillou + SuperToons TV + Stormcast Novelas + Bloomberg TV+ + Top Films - Rakuten TV + Juste Pour Rire + FIFA+ + Yu-Gi-Oh! + Pluto TV Cuisine + Tortues Ninja + Wellbeing TV + BelAir TV + People Are Awesome + FailArmy + Films Romantiques - Rakuten TV + Crime - Rakuten TV + Travelxp + Clubbing TV + Chefclub TV + Jellytoon + Scream'IN + Nature Time + Ciné Nanar + Gong + Homicide + Tahiti TV + Runtime + Emotion'L + Motus + Fréquence Novelas + Émotion + Y'a que la vérité qui compte + Le Figaro Live + Vevo Pop + Vevo Hip-Hop &amp; R&amp;B + Ciné Prime + Secret Story + MasterChef + Echappées belles &amp; co + MGG FAST + Love the Planet + Destination Nature + L'Equipe Live 1 + L'Equipe Live 2 + Génération Sitcoms + Alerte à Malibu + A prendre ou à laisser + Les 30 histoires + Trailers + Tele.Novela + Un Village Français + Les filles d'à côté + Enquêtes de Choc + Nashville Channel + Le Tigre + Vialma + Au coin de feu + Les Z'amours + Le meilleur d'Arthur + Qui veut gagner des millions ? + Charlotte aux Fraises + Vevo '90s &amp; '00s + Family Club + Les Lapins Crétins: Invasion + Maison &amp; Travaux + World Poker Tour + Motorvision + Drive TV + Allociné + Reportages by Spica + Le Meilleur de la TV Réalité + Les Anges + Popcorn + Motor Racing + L'effet papillon + Passion Novelas + Ciné Western + Degrassi + Top Santé + Plus belle la vie + Vevo Noël + Scènes de Crime + 100% Docs + Wasabi + Culture Pub + CNN FAST + Ardivision + Les secrets de nos régions + BBC Drama + Catfish + Super! iCarly + Pluto TV Real Life + Pluto TV Serie + Serie Teen + Pluto TV Film Romantici + Super! Pop + The Pet Collective + Fashion TV + Commedia - Rakuten TV + Drama - Rakuten TV + Documentari - Rakuten TV + People Are Awesome + Euronews in diretta + Motor1TV + Pluto TV Film + Film d'azione - Rakuten TV + Super! Spongebob + FailArmy + Canale Europa + Yamato Animation + Teletubbies + Deluxe Lounge HD + HorizonSports + Qwest TV + RADIO ITALIA TREND + Sportitalia + SuperToons TV + MONDO TV KIDS + SportOutdoor.tv + Bizzarro Movies + Bloomberg TV+ + Vivaldi TV + Ticker News + BelAir TV + Trace Urban + Trace Latina + WildEarth + Trailers + Televisa Telenovelas + 100% Natale - Rakuten TV + CG - CINEMA d'Autore + Cinema Segreto + 5-Minuti Creativi + Wellbeing TV + WaterBear + Clubbing TV + Italian Fishing TV + Doctor Who + Giornale Radio TV + WP + Dark Matter TV + Brindiamo! + Cmusic + Montagna! + Alta Tensione + Smile + Grandi Nomi + Explorer HD Channel + Top Film - Rakuten TV + Risate all'italiana + Romance - Rakuten TV + Rock TV + Vevo Pop + Pluto TV Film Commedia + Pluto TV Film Azione + Consulenze Illegali + TVPlay + Teen Vee + NBC News NOW + duckTV + Solocalcio + Love the Planet + Film Al Femminile - Wedo Movies + Velvet + Full Moon + Hip Hop TV + Pluto TV Reality + Grandi Documentari - Wedo Big Stories + Vevo '90s &amp; '00s + Move - Italian Active Lives + Serially Drama + Just For Laughs + Serie Crime - Rakuten TV + FIFA+ + Yu-Gi-Oh! + Serially Crime + House of Docs + FRANCE 24 FAST + Travel &amp; Living by DOVE + Baywatch + U-Trend + Travelxp + CineMadame + La 7 + Serie TV asiatiche d’azione by Liv TV + Lifestyle by LEI + Vevo '70s &amp; '80s + RBN + Cinema Excelsior + Le Vite Degli Altri + Andromeda + Autostop per il Cielo + Mutant X + Heidi &amp; Maya + GCTV (Global Champions TV) + CNBC + DATELINE 24/7 + Vevo Natale + East is East + Dolomiti Life TV + Grjngo- Film Western + The Boat Show + Caminetto + CNN FAST + World Poker Tour + Yu yu Hakusho + Film e Sorrisi + 현대홈쇼핑 + 현대홈쇼핑+Shop + MBC every1 어서와 한국은 처음이지? + 코코비TV + MBC every1 별순검 + Bloomberg TV+ UHD + Bloomberg Originals + SBS 런닝맨 + SBS 미운 우리 새끼 + SBS 순풍산부인과 + SBS 펜트하우스 + SBS 동상이몽2 - 너는 내 운명 + SBS 나는 솔로 + SBS 백종원의 골목식당 + SBS 정글의 법칙 + SBS 순간포착 세상에 이런일이 + SBS 패밀리가 떴다 + SBS 궁금한 이야기 Y + SBS 그것이 알고싶다 + SBS 생활의 달인 + MBC 무한도전 + MBC 돈꽃 + MBC 옷소매 붉은끝동 + 초록뱀미디어 K-STAR 골프 + SBS 편의점 샛별이 + SBS 불타는 청춘 + MBC 심야괴담회 + MBC 선을 넘는 녀석들 + SBS 스토브리그 + SBS TV 동물농장 + SBS 빽드 + MBC 나혼자산다 + MBC 지붕뚫고 하이킥 + 투니버스 뱀파이어소녀 달자 + tvN 대탈출3 + MBC 거침없이 하이킥 + 꽃보다 남자 + NEW MOVIES + OGN 스타리그 + 맛있는 녀석들 + TV CHOSUN 식객 허영만의 백반기행 + KBS Joy 무엇이든 물어보살 + World Billiards TV + tvN 응답하라 전 시즌 모아보기 + tvN 갯마을 차차차 + tvN 삼시세끼 산촌편 + Mnet 스트릿 우먼 파이터 + Mnet 스트릿 우먼 파이터 2 HOT CLIP + 심리 읽어드립니다 + tvN 호텔 델루나 + tvN 스트리트 푸드 파이터 1~2 + tvN 미스터 션샤인 + tvN 사랑의 불시착 + tvN 빈센조 + tvN 스물다섯 스물하나 + tvN 여신강림 + 로보카폴리 TV + 고독한 미식가 + 도라마코리아 + MBN 나는 자연인이다 + MBN 휴먼다큐 사노라면 + MBN 속풀이쇼 동치미 + MBN 돌싱글즈 + iHQ 돈쭐내러 왔습니다 + 전우치 + KBS Joy 연애의 참견 + 채널A 나만 믿고 따라와 도시어부 + 채널A 이제 만나러 갑니다 + E채널 토요일은 밥이 좋아 + KBS 개는 훌륭하다 + KBS 쌈마이웨이 + KBS 1박2일 시즌1 + tvN 작은 아씨들 + tvN 슬기로운 의사생활 시즌2 + JTBC 괴물 + JTBC 힘쎈여자 도봉순 + JTBC 효리네 민박 시즌1 + JTBC 골프 + JTBC 최강야구 + JTBC 비긴어게인 + Mnet 스트릿댄스 걸스 파이터 + Zㅏ때는 말이야 + MBC 안싸우면 다행이야 + MBC 나는 가수다 + ch.핑크퐁 + tvN 어쩌다 사장 전 시즌 몰아보기 + 사피엔스스튜디오 + 역사 읽어드립니다 + tvN 신서유기 6 + tvN 놀라운 토요일 하이라이트 + TV CHOSUN 국가가 부른다 + Animax 반지의 비밀일기 + 채널A 요즘 육아 금쪽같은 내새끼 + JTBC 크라임씬 + JTBC 뉴스 + JTBC 톡파원25시 + JTBC 품위있는 그녀 + tvN 환혼 + 연합뉴스TV + FIFA+ + 뽀요TV + 우리의식탁 + YTN + TV조선 빨간 풍선 + TV조선 골프왕 + essential; by Bugs + PLAYY 영화 + PLAYY 어워드특집 + tvN 유 퀴즈 온 더 블럭 HOT CLIP + 글로벌 뉴스 by LeadStory + PGA Tour + 씨네21+ + TV조선 미스터트롯 영웅의 탄생 + TED + diff --git a/sites/mojmaxtv.hrvatskitelekom.hr/mojmaxtv.hrvatskitelekom.hr.config.js b/sites/mojmaxtv.hrvatskitelekom.hr/mojmaxtv.hrvatskitelekom.hr.config.js index db27786b..bbad1e6c 100644 --- a/sites/mojmaxtv.hrvatskitelekom.hr/mojmaxtv.hrvatskitelekom.hr.config.js +++ b/sites/mojmaxtv.hrvatskitelekom.hr/mojmaxtv.hrvatskitelekom.hr.config.js @@ -2,7 +2,7 @@ const doFetch = require('@ntlab/sfetch') const axios = require('axios') const dayjs = require('dayjs') const crypto = require('crypto') -const { sortBy } = require('../../scripts/functions') +const sortBy = require('lodash.sortby') // API Configuration Constants const NATCO_CODE = 'hr' diff --git a/sites/mtel.ba/mtel.ba.config.js b/sites/mtel.ba/mtel.ba.config.js index 13b4d26f..6ade976d 100644 --- a/sites/mtel.ba/mtel.ba.config.js +++ b/sites/mtel.ba/mtel.ba.config.js @@ -3,7 +3,7 @@ const dayjs = require('dayjs') const utc = require('dayjs/plugin/utc') const timezone = require('dayjs/plugin/timezone') const customParseFormat = require('dayjs/plugin/customParseFormat') -const { sortBy } = require('../../scripts/functions') +const sortBy = require('lodash.sortby') dayjs.extend(utc) dayjs.extend(timezone) diff --git a/sites/ontvtonight.com/ontvtonight.com.config.js b/sites/ontvtonight.com/ontvtonight.com.config.js index eac558a9..e4003ad1 100644 --- a/sites/ontvtonight.com/ontvtonight.com.config.js +++ b/sites/ontvtonight.com/ontvtonight.com.config.js @@ -4,7 +4,7 @@ const dayjs = require('dayjs') const utc = require('dayjs/plugin/utc') const timezone = require('dayjs/plugin/timezone') const customParseFormat = require('dayjs/plugin/customParseFormat') -const { uniqBy } = require('../../scripts/functions') +const uniqBy = require('lodash.uniqby') dayjs.extend(utc) dayjs.extend(timezone) diff --git a/sites/programme-tv.net/programme-tv.net.config.js b/sites/programme-tv.net/programme-tv.net.config.js index bee7936f..4b3919d7 100644 --- a/sites/programme-tv.net/programme-tv.net.config.js +++ b/sites/programme-tv.net/programme-tv.net.config.js @@ -1,127 +1,127 @@ -const durationParser = require('parse-duration').default -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: 'programme-tv.net', - days: 2, - request: { - headers: { - cookie: 'authId=b7154156fe4fb8acdb6f38e1207c6231' - } - }, - url: function ({ date, channel }) { - return `https://www.programme-tv.net/programme/chaine/${date.format('YYYY-MM-DD')}/programme-${ - channel.site_id - }.html` - }, - parser: function ({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const title = parseTitle($item) - const subTitle = parseSubtitle($item) - const image = parseImage($item) - const category = parseCategory($item) - const start = parseStart($item, date) - const duration = parseDuration($item) - const stop = start.add(duration, 'ms') - - programs.push({ title, subTitle, image, category, start, stop }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - const data = await axios - .get( - `https://www.programme-tv.net/_esi/channel-list/${dayjs().format( - 'YYYY-MM-DD' - )}/?bouquet=perso&modal=0`, - { - headers: { - cookie: 'authId=b7154156fe4fb8acdb6f38e1207c6231' - } - } - ) - .then(r => r.data) - .catch(console.error) - - let channels = [] - - const $ = cheerio.load(data) - $('.channelList-listItemsLink').each((i, el) => { - const name = $(el).attr('title') - const url = $(el).attr('href') - const [, site_id] = url.match(/\/programme-(.*)\.html$/i) - - channels.push({ - lang: 'fr', - site_id, - name - }) - }) - - return channels - } -} - -function parseStart($item, date) { - let time = $item('.mainBroadcastCard-startingHour').first().text().trim() - time = `${date.format('MM/DD/YYYY')} ${time.replace('h', ':')}` - - return dayjs.tz(time, 'MM/DD/YYYY HH:mm', 'Europe/Paris') -} - -function parseDuration($item) { - const duration = $item('.mainBroadcastCard-durationContent').first().text().trim() - - return durationParser(duration) -} - -function parseImage($item) { - const img = $item('.mainBroadcastCard-imageContent').first().find('img') - const value = img.attr('srcset') || img.data('srcset') - - let url = null - - if (value) { - const sources = value.split(',').map(s => s.trim()) - for (const source of sources) { - const [src, descriptor] = source.split(/\s+/) - if (descriptor === '128w') { - url = src.replace('128x180', '960x540') - break - } - } - } - - return url -} - -function parseCategory($item) { - return $item('.mainBroadcastCard-format').first().text().trim() -} - -function parseTitle($item) { - return $item('.mainBroadcastCard-title').first().text().trim() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.mainBroadcastCard').toArray() -} - -function parseSubtitle($item) { - return $item('.mainBroadcastCard-subtitle').text().trim() || null +const durationParser = require('parse-duration').default +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: 'programme-tv.net', + days: 2, + request: { + headers: { + cookie: 'authId=b7154156fe4fb8acdb6f38e1207c6231' + } + }, + url: function ({ date, channel }) { + return `https://www.programme-tv.net/programme/chaine/${date.format('YYYY-MM-DD')}/programme-${ + channel.site_id + }.html` + }, + parser: function ({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const title = parseTitle($item) + const subTitle = parseSubtitle($item) + const image = parseImage($item) + const category = parseCategory($item) + const start = parseStart($item, date) + const duration = parseDuration($item) + const stop = start.add(duration, 'ms') + + programs.push({ title, subTitle, image, category, start, stop }) + }) + + return programs + }, + async channels() { + const axios = require('axios') + const data = await axios + .get( + `https://www.programme-tv.net/_esi/channel-list/${dayjs().format( + 'YYYY-MM-DD' + )}/?bouquet=perso&modal=0`, + { + headers: { + cookie: 'authId=b7154156fe4fb8acdb6f38e1207c6231' + } + } + ) + .then(r => r.data) + .catch(console.error) + + let channels = [] + + const $ = cheerio.load(data) + $('.channelList-listItemsLink').each((i, el) => { + const name = $(el).attr('title') + const url = $(el).attr('href') + const [, site_id] = url.match(/\/programme-(.*)\.html$/i) + + channels.push({ + lang: 'fr', + site_id, + name + }) + }) + + return channels + } +} + +function parseStart($item, date) { + let time = $item('.mainBroadcastCard-startingHour').first().text().trim() + time = `${date.format('MM/DD/YYYY')} ${time.replace('h', ':')}` + + return dayjs.tz(time, 'MM/DD/YYYY HH:mm', 'Europe/Paris') +} + +function parseDuration($item) { + const duration = $item('.mainBroadcastCard-durationContent').first().text().trim() + + return durationParser(duration) +} + +function parseImage($item) { + const img = $item('.mainBroadcastCard-imageContent').first().find('img') + const value = img.attr('srcset') || img.data('srcset') + + let url = null + + if (value) { + const sources = value.split(',').map(s => s.trim()) + for (const source of sources) { + const [src, descriptor] = source.split(/\s+/) + if (descriptor === '128w') { + url = src.replace('128x180', '960x540') + break + } + } + } + + return url +} + +function parseCategory($item) { + return $item('.mainBroadcastCard-format').first().text().trim() +} + +function parseTitle($item) { + return $item('.mainBroadcastCard-title').first().text().trim() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.mainBroadcastCard').toArray() +} + +function parseSubtitle($item) { + return $item('.mainBroadcastCard-subtitle').text().trim() || null } \ No newline at end of file diff --git a/sites/reportv.com.ar/reportv.com.ar.config.js b/sites/reportv.com.ar/reportv.com.ar.config.js index cfe6141e..0016fd8d 100644 --- a/sites/reportv.com.ar/reportv.com.ar.config.js +++ b/sites/reportv.com.ar/reportv.com.ar.config.js @@ -5,7 +5,7 @@ const cheerio = require('cheerio') const utc = require('dayjs/plugin/utc') const timezone = require('dayjs/plugin/timezone') const customParseFormat = require('dayjs/plugin/customParseFormat') -const { startCase } = require('../../scripts/functions') +const startCase = require('lodash.startcase') dayjs.extend(utc) dayjs.extend(timezone) diff --git a/sites/sky.com/sky.com.config.js b/sites/sky.com/sky.com.config.js index 291ecdfc..bb33b6c2 100644 --- a/sites/sky.com/sky.com.config.js +++ b/sites/sky.com/sky.com.config.js @@ -3,7 +3,7 @@ const dayjs = require('dayjs') const utc = require('dayjs/plugin/utc') const doFetch = require('@ntlab/sfetch') const debug = require('debug')('site:sky.com') -const { sortBy } = require('../../scripts/functions') +const sortBy = require('lodash.sortby') dayjs.extend(utc) diff --git a/sites/streamingtvguides.com/streamingtvguides.com.config.js b/sites/streamingtvguides.com/streamingtvguides.com.config.js index ccff2d3f..75f0f976 100644 --- a/sites/streamingtvguides.com/streamingtvguides.com.config.js +++ b/sites/streamingtvguides.com/streamingtvguides.com.config.js @@ -2,7 +2,8 @@ const cheerio = require('cheerio') const dayjs = require('dayjs') const customParseFormat = require('dayjs/plugin/customParseFormat') const timezone = require('dayjs/plugin/timezone') -const { sortBy, uniqBy } = require('../../scripts/functions') +const sortBy = require('lodash.sortby') +const uniqBy = require('lodash.uniqby') dayjs.extend(customParseFormat) dayjs.extend(timezone) diff --git a/sites/tivie.id/tivie.id.config.js b/sites/tivie.id/tivie.id.config.js index ed3bda76..ec4862f8 100644 --- a/sites/tivie.id/tivie.id.config.js +++ b/sites/tivie.id/tivie.id.config.js @@ -1,141 +1,141 @@ -const axios = require('axios') -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') -const doFetch = require('@ntlab/sfetch') -const debug = require('debug')('site:tivie.id') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -doFetch.setDebugger(debug) - -const tz = 'Asia/Jakarta' - -module.exports = { - site: 'tivie.id', - days: 2, - url({ channel, date }) { - return `https://tivie.id/channel/${channel.site_id}/${date.format('YYYYMMDD')}` - }, - async parser({ content, date }) { - const programs = [] - if (content) { - const $ = cheerio.load(content) - const items = $('ul[x-data] > li[id*="event-"] > div.w-full') - .toArray() - .map(item => { - const $item = $(item) - const time = $item.find('div:nth-child(1) span:nth-child(1)') - const info = $item.find('div:nth-child(2) h5') - const detail = info.find('a') - const p = { - start: dayjs.tz(`${date.format('YYYY-MM-DD')} ${time.html()}`, 'YYYY-MM-DD HH:mm', tz) - } - if (detail.length) { - const subtitle = detail.find('div') - p.title = parseText(subtitle.length ? subtitle : detail) - p.url = detail.attr('href') - } else { - p.title = parseText(info) - } - if (p.title) { - const [, , season, episode] = p.title.match(/( S(\d+))?, Ep\. (\d+)/) || [ - null, - null, - null, - null - ] - if (season) { - p.season = parseInt(season) - } - if (episode) { - p.episode = parseInt(episode) - } - } - return p - }) - // fetch detailed guide if necessary - const queues = items - .filter(i => i.url) - .map(i => { - const url = i.url - delete i.url - return { i, url } - }) - if (queues.length) { - await doFetch(queues, (queue, res) => { - const $ = cheerio.load(res) - const img = $('#main-content > div > div:nth-child(1) img') - const info = $('#main-content > div > div:nth-child(2)') - const title = parseText(info.find('h2:nth-child(2)')) - if (!queue.i.title.startsWith(title) && !queue.i.title.startsWith('LIVE ')) { - queue.i.subTitle = parseText(info.find('h2:nth-child(2)')) - } - const desc1 = parseText(info.find('div[class=""]:nth-child(3)')) - const desc2 = parseText(info.find('div[class=""]:nth-child(4)')) - if (desc2 == '') { - queue.i.description = desc1.replace('TiViE.id | ', '') - } else { - queue.i.description = desc2.replace('TiViE.id | ', '') - queue.i.date = parseText(info.find('h2:nth-child(3)')) - } - queue.i.categories = parseText(info.find('div[class=""]:nth-child(1)')).split(', ') - queue.i.image = img.length ? img.attr('src') : null - }) - } - // fill start-stop - for (let i = 0; i < items.length; i++) { - if (i < items.length - 1) { - items[i].stop = items[i + 1].start - } else { - items[i].stop = dayjs.tz( - `${date.add(1, 'd').format('YYYY-MM-DD')} 00:00`, - 'YYYY-MM-DD HH:mm', - tz - ) - } - } - // add programs - programs.push(...items) - } - - return programs - }, - async channels({ lang = 'id' }) { - const result = await axios - .get('https://tivie.id/channel') - .then(response => response.data) - .catch(console.error) - - const $ = cheerio.load(result) - const items = $('ul[x-data] li[x-data] div header h2 a').toArray() - const channels = items.map(item => { - const $item = $(item) - const url = $item.attr('href') - return { - lang, - site_id: url.substr(url.lastIndexOf('/') + 1), - name: $item.find('strong').text() - } - }) - - return channels - } -} - -function parseText($item) { - let text = $item.text().replace(/\t/g, '').replace(/\n/g, ' ').trim() - while (true) { - if (text.match(/\s\s/)) { - text = text.replace(/\s\s/g, ' ') - continue - } - break - } - - return text -} +const axios = require('axios') +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') +const doFetch = require('@ntlab/sfetch') +const debug = require('debug')('site:tivie.id') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +doFetch.setDebugger(debug) + +const tz = 'Asia/Jakarta' + +module.exports = { + site: 'tivie.id', + days: 2, + url({ channel, date }) { + return `https://tivie.id/channel/${channel.site_id}/${date.format('YYYYMMDD')}` + }, + async parser({ content, date }) { + const programs = [] + if (content) { + const $ = cheerio.load(content) + const items = $('ul[x-data] > li[id*="event-"] > div.w-full') + .toArray() + .map(item => { + const $item = $(item) + const time = $item.find('div:nth-child(1) span:nth-child(1)') + const info = $item.find('div:nth-child(2) h5') + const detail = info.find('a') + const p = { + start: dayjs.tz(`${date.format('YYYY-MM-DD')} ${time.html()}`, 'YYYY-MM-DD HH:mm', tz) + } + if (detail.length) { + const subtitle = detail.find('div') + p.title = parseText(subtitle.length ? subtitle : detail) + p.url = detail.attr('href') + } else { + p.title = parseText(info) + } + if (p.title) { + const [, , season, episode] = p.title.match(/( S(\d+))?, Ep\. (\d+)/) || [ + null, + null, + null, + null + ] + if (season) { + p.season = parseInt(season) + } + if (episode) { + p.episode = parseInt(episode) + } + } + return p + }) + // fetch detailed guide if necessary + const queues = items + .filter(i => i.url) + .map(i => { + const url = i.url + delete i.url + return { i, url } + }) + if (queues.length) { + await doFetch(queues, (queue, res) => { + const $ = cheerio.load(res) + const img = $('#main-content > div > div:nth-child(1) img') + const info = $('#main-content > div > div:nth-child(2)') + const title = parseText(info.find('h2:nth-child(2)')) + if (!queue.i.title.startsWith(title) && !queue.i.title.startsWith('LIVE ')) { + queue.i.subTitle = parseText(info.find('h2:nth-child(2)')) + } + const desc1 = parseText(info.find('div[class=""]:nth-child(3)')) + const desc2 = parseText(info.find('div[class=""]:nth-child(4)')) + if (desc2 == '') { + queue.i.description = desc1.replace('TiViE.id | ', '') + } else { + queue.i.description = desc2.replace('TiViE.id | ', '') + queue.i.date = parseText(info.find('h2:nth-child(3)')) + } + queue.i.categories = parseText(info.find('div[class=""]:nth-child(1)')).split(', ') + queue.i.image = img.length ? img.attr('src') : null + }) + } + // fill start-stop + for (let i = 0; i < items.length; i++) { + if (i < items.length - 1) { + items[i].stop = items[i + 1].start + } else { + items[i].stop = dayjs.tz( + `${date.add(1, 'd').format('YYYY-MM-DD')} 00:00`, + 'YYYY-MM-DD HH:mm', + tz + ) + } + } + // add programs + programs.push(...items) + } + + return programs + }, + async channels({ lang = 'id' }) { + const result = await axios + .get('https://tivie.id/channel') + .then(response => response.data) + .catch(console.error) + + const $ = cheerio.load(result) + const items = $('ul[x-data] li[x-data] div header h2 a').toArray() + const channels = items.map(item => { + const $item = $(item) + const url = $item.attr('href') + return { + lang, + site_id: url.substr(url.lastIndexOf('/') + 1), + name: $item.find('strong').text() + } + }) + + return channels + } +} + +function parseText($item) { + let text = $item.text().replace(/\t/g, '').replace(/\n/g, ' ').trim() + while (true) { + if (text.match(/\s\s/)) { + text = text.replace(/\s\s/g, ' ') + continue + } + break + } + + return text +} diff --git a/sites/tv.mail.ru/tv.mail.ru.config.js b/sites/tv.mail.ru/tv.mail.ru.config.js index 5b5bb99c..ec046c40 100644 --- a/sites/tv.mail.ru/tv.mail.ru.config.js +++ b/sites/tv.mail.ru/tv.mail.ru.config.js @@ -1,6 +1,6 @@ const { DateTime } = require('luxon') const axios = require('axios') -const { uniqBy } = require('../../scripts/functions') +const uniqBy = require('lodash.uniqby') module.exports = { site: 'tv.mail.ru', diff --git a/sites/tv.sfr.fr/tv.sfr.fr.channels.xml b/sites/tv.sfr.fr/tv.sfr.fr.channels.xml index bc19ca9a..27e8cae8 100644 --- a/sites/tv.sfr.fr/tv.sfr.fr.channels.xml +++ b/sites/tv.sfr.fr/tv.sfr.fr.channels.xml @@ -1,492 +1,492 @@ - - - TF1 - France 2 - France 3 - France 4 - France 5 - M6 - Arte - LCP-Public Sénat - W9 - TMC - TFX - Gulli - BFM TV - CNews - LCI - franceinfo: - CStar - T18 - NOVO19 - TF1 Séries-Films - L'équipe - 6ter - RMC Story - RMC Découverte - Chérie 25 - i24 News - AFTER FOOT TV - BFM Business - TECH&CO - RMC Sport 1 - RMC Sport Live 2 - BFM MARSEILLE PROVENCE - BFM Lyon - RMC Talk Info - Discovery Channel - TLC - Discovery Investigation - BFM Grands Reportages - France 24 - Euronews Fra - RMC Alerte Secours - 13ème rue - Syfy - E! Entertainment - WARNER TV - MTV - MCM - AB1 - SERIE CLUB - Game One - Game One+1 - Warner TV Next - J-One - BET - Comedy Central - Netflix - Prime Vidéo - Disney+ - Paris Première - Téva - RTL9 - TV Breizh - TV5 Monde - TF1 4K - France 2 UHD - CANAL+ SPORT 360 - CANAL+ FOOT - CANAL+ BOX OFFICE - CANAL+ GRAND ECRAN - CANAL+ DOCS - CANAL+ KIDS - BFM2 - LCP-AN 24/24 - Public Sénat 24/24 - LA CHAINE METEO - RMC Sport Access - RMC Mecanic - beIN SPORTS 1 - beIN SPORTS 2 - beIN SPORTS 3 - DAZN 1 - Equidia - MGG TV - Auto Moto - Journal du Golf TV - Sport en France - OLPLAY - EUROSPORT 1 HD - EUROSPORT 2 HD - OCS - CINE+ frisson - CINE+ émotion - CINE+ family - CINE+ festival - CINE+ classic - DISNEY CHANNEL - DISNEY CHANNEL +1 - Paramount Network - Paramount Network Décalé - TCM Cinéma - Action - INSOMNIA - RMC WOW - RMC Mystère - J'irai dormir chez vous - Ushuaia TV - TREK - Crime District - Marmiton TV - Histoire TV - Toute l'histoire - KTO - Animaux - Chasse et Pêche - Science et Vie TV - Luxe TV - Fashion TV - Men's Up TV - Astrocenter.tv - My Zen TV - MUSEUM TV - Museum TV 4K - EXPLORE - Le Figaro TV - SEASONS - KITCHEN MANIA - Top Santé TV - Maison & Travaux TV - Auto Plus TV - Nickelodeon Junior - Boomerang - Boomerang+1 - Tiji - DREAMWORKS - Nickelodeon - Nickelodeon+1 - Canal J - Cartoon Network - Nickelodeon Teen - Cartoonito - Trace Vanilla - Mangas - Lucky Jack - MTV Hits - M6 Music - RFM TV - NRJ Hits - Trace Latina - Mezzo - Mezzo Live - Melody TV - Trace Urban - Trace Toca - TRACE CARIBBEAN - Trace Gospel - Melody d'Afrique - BFM Grand Lille - BFM Grand Littoral - BFM NICE COTE D'AZUR - BFM TOULON VAR - BFM DICI ALPES DU SUD - BFM DICI HAUTE-PROVENCE - BFM ALSACE - BFM Normandie - vià30 - vià31 - vià34 - vià66 - beIN SPORTS MAX 4 - beIN SPORTS MAX 5 - beIN SPORTS MAX 6 - beIN SPORTS MAX 7 - beIN SPORTS MAX 8 - beIN SPORTS MAX 9 - beIN SPORTS MAX 10 - GOLF+ HD - CANAL+LIVE 1 - CANAL+LIVE 2 - CANAL+LIVE 3 - CANAL+LIVE 4 - CANAL+LIVE 5 - CANAL+LIVE 6 - CANAL+LIVE 7 - RMC Sport Live 3 - RMC Sport Live 4 - XXL - Dorcel TV - Dorcel XXX - Private TV - Hustler TV - VIXEN - Pink TV / Pink X - Man-X - Union TV - DORCEL TV AFRICA - MEN TV - PENTHOUSE HD - Das Erste - SPORT 1 - France 3 Alpes - France 3 Alsace - France 3 Aquitaine - France 3 Auvergne - France 3 Basse-Normandie - France 3 Bourgogne - France 3 Bretagne - France 3 Centre - France 3 Champagne-Ardenne - France 3 via Stella - France 3 Côte d'Azur - France 3 Franche-Comté - France 3 Haute-Normandie - France 3 Languedoc - France 3 Limousin - France 3 Lorraine - France 3 Midi-Pyrénées - France 3 Nord-Pas-de-Calais - France 3 Paris IDF - France 3 Pays de la Loire - France 3 Picardie - France 3 Poitou-Charentes - France 3 Provence-Alpes - France 3 Rhône-Alpes - France 3 Nouvelle Aquitaine - France 3 - Corse - 20 Minutes TV - Télé Bocal - vià93 - Figaro TV IDF - TV78 - Lyon Capitale TV - Télé Grenoble - TL7 Saint Etienne - 8 Mont-Blanc - ILTV - ASTV - Wéo Picardie - Wéo TV, La voix du nord - CRESPIN TELEVISION - Vià MATÉLÉ - 7A Limoges - TV7 Bordeaux - TVPI (TV Biarritz) - ETB1 - ETB2 - ETB3 - KANALDUDE - Grosbliederstroff - TV2COM - TRESSANGE TV - NA TV - Canal 32 - vià Mirabelle - Puissance TV - Télé Schiltigheim - TVMonaco - Mosaik Cristal - TV8 Moselle-Est - vià Vosges - Cannes Lérins TV - Maritima TV - MAURIENNE TV - Angers Télé - Télé Nantes - TV Vendée - LMtv Sarthe - Tébéo - TV Rennes 35 - Val de Loire TV - Zouk TV - Télé Paese - CNN International - BBC NEWS - France 24 ENG - CNBC Europe - Bloomberg - Al Jazeera English - i24 News Anglais - NHK WORLD-JAPAN - Sky News - TGCOM24 - i24 News Arabe - France 24 ARA - Al Jazeera - Medi 1 TV - Al Arabiya - Echorouk TV - Ennahar TV - SIC Noticias - Canal 11 - Rai News 24 - France 24 Espanol - 24 Horas - TVE - DW-TV - WELT - TVN24 - Record News - CGTN - Africa 24 - Canal 2 - V+ - Porto Canal - Local Visao - Benfica TV - A BOLA TV - TV Record - RTP 3 - TVI Internacional - SIC Internacional - Canal Q - RTPI - Rai Uno - Rai Due - Rai Tre - RAI SCUOLA - RAI STORIA - Mediaset Italia - REAL MADRID TV - STAR TVE - Antena 3 - Atres Series - ALL FLAMENCO - TVG EUROPA - TV3 Catalunya - etb basque - TELE MADRID - ANDALUCIA TV - Boomerang Anglais - FilmBox Arthouse - TCM Anglais - TinyTeen - Lang Lab - Lingo Toons - DocuBox HD - FashionBox HD - Pro Sieben - N-TV - RTL Television - RTL2 - Sat 1 - Super RTL - SWR - Vox - ZDF - KABEL EINS - KIKA - RTL NITRO TV - Arte Allemande - 3 SAT - VIVA - iTVN - iTVN Extra - TVP Polonia - Armenia 1 - Antenna 1 - ART CINEMA - ART AFLAM 1 - ART AFLAM 2 - AL HEKAYAT 1 - AL HEKAYAT 2 - TV Romania International - Bahia - Télé Maroc - Samira TV - Canal Algérie - A3 - Beur TV - 2M Maroc - Al Aoula - Arryadia - Assadissa - El Hiwar Ettounsi - Watania 2 - Tunisia 1 - Asharq News - Berbère Jeunesse - Berbère Musique - Berbère TV - Sky News Arabia - CHADA TV - Rotana Comedy - Syria TV - Al Resalah - Alaraby 1 - Echorouk News - El Bilad TV - Dizi - Alaraby 2 - Rotana Aflam+ - Rotana Cinéma+ FR - Rotana Music - Rotana Drama - Rotana Kids - Rotana Cinema - Rotana Classic - Nessma - DMC - Fix et Foxy - Carthage+ - DMC Drama - Iqraa TV - Iqraa International - Al Majd Holy Quran - Al Maghribia - Arrabiâ - Al Jadeed - LBC Sat - Lana TV - NBN - OTV - Murr TV - Rotana M+ - Dubaï TV - ON TV - HANNIBAL TV - Panorama Drama - Panorama Film - Al Masriya - The Israeli Network - Jordan Satellite Channel - Euro Star - Euro D - Habertürk - BEIN MOVIES - SHOW MAX - Show Turk - ATV Avrupa - KANAL 7 AVRUPA - TV8 International - Saudi Channel 1 - A+ - ORTB - NOVELAS - CRTV - Equinoxe TV - CHERIFLA TV - ORTC - TV Congo - Maboke TV - RTNC - NCI - RTI 1 - CDIRECT - Gabon 1ère - RTG - TVM - ORTM - Nollywood TV Epic - Nollywood TV - Pulaagu - Trace Africa - Vox Africa - 2STV - RTS 1 - SEN TV - TFM - SUNU YEUF - Beijing TV - CCTV YULE - CCTV-4 - China Movie Channel (CMC) - Hunan TV - JSBC International - Phoenix CNE - Phoenix Infonews - Shangaï Dragon TV - Great Wall Elite - ZTV World (Zhejiang Star TV) - GRT GBA Satellite TV - CGTN-Français - NTD - KBS World - Colors - Utsav Bharat - Rishtey - Utsav Plus - Zee TV - NHK World Premium - B4U Movies - Geo News - Geo TV - Sony Max - Las Estrellas - Distrito Comedia - De pelicula - RMS - Telehit - tlnovelas + + + TF1 + France 2 + France 3 + France 4 + France 5 + M6 + Arte + LCP-Public Sénat + W9 + TMC + TFX + Gulli + BFM TV + CNews + LCI + franceinfo: + CStar + T18 + NOVO19 + TF1 Séries-Films + L'équipe + 6ter + RMC Story + RMC Découverte + Chérie 25 + i24 News + AFTER FOOT TV + BFM Business + TECH&CO + RMC Sport 1 + RMC Sport Live 2 + BFM MARSEILLE PROVENCE + BFM Lyon + RMC Talk Info + Discovery Channel + TLC + Discovery Investigation + BFM Grands Reportages + France 24 + Euronews Fra + RMC Alerte Secours + 13ème rue + Syfy + E! Entertainment + WARNER TV + MTV + MCM + AB1 + SERIE CLUB + Game One + Game One+1 + Warner TV Next + J-One + BET + Comedy Central + Netflix + Prime Vidéo + Disney+ + Paris Première + Téva + RTL9 + TV Breizh + TV5 Monde + TF1 4K + France 2 UHD + CANAL+ SPORT 360 + CANAL+ FOOT + CANAL+ BOX OFFICE + CANAL+ GRAND ECRAN + CANAL+ DOCS + CANAL+ KIDS + BFM2 + LCP-AN 24/24 + Public Sénat 24/24 + LA CHAINE METEO + RMC Sport Access + RMC Mecanic + beIN SPORTS 1 + beIN SPORTS 2 + beIN SPORTS 3 + DAZN 1 + Equidia + MGG TV + Auto Moto + Journal du Golf TV + Sport en France + OLPLAY + EUROSPORT 1 HD + EUROSPORT 2 HD + OCS + CINE+ frisson + CINE+ émotion + CINE+ family + CINE+ festival + CINE+ classic + DISNEY CHANNEL + DISNEY CHANNEL +1 + Paramount Network + Paramount Network Décalé + TCM Cinéma + Action + INSOMNIA + RMC WOW + RMC Mystère + J'irai dormir chez vous + Ushuaia TV + TREK + Crime District + Marmiton TV + Histoire TV + Toute l'histoire + KTO + Animaux + Chasse et Pêche + Science et Vie TV + Luxe TV + Fashion TV + Men's Up TV + Astrocenter.tv + My Zen TV + MUSEUM TV + Museum TV 4K + EXPLORE + Le Figaro TV + SEASONS + KITCHEN MANIA + Top Santé TV + Maison & Travaux TV + Auto Plus TV + Nickelodeon Junior + Boomerang + Boomerang+1 + Tiji + DREAMWORKS + Nickelodeon + Nickelodeon+1 + Canal J + Cartoon Network + Nickelodeon Teen + Cartoonito + Trace Vanilla + Mangas + Lucky Jack + MTV Hits + M6 Music + RFM TV + NRJ Hits + Trace Latina + Mezzo + Mezzo Live + Melody TV + Trace Urban + Trace Toca + TRACE CARIBBEAN + Trace Gospel + Melody d'Afrique + BFM Grand Lille + BFM Grand Littoral + BFM NICE COTE D'AZUR + BFM TOULON VAR + BFM DICI ALPES DU SUD + BFM DICI HAUTE-PROVENCE + BFM ALSACE + BFM Normandie + vià30 + vià31 + vià34 + vià66 + beIN SPORTS MAX 4 + beIN SPORTS MAX 5 + beIN SPORTS MAX 6 + beIN SPORTS MAX 7 + beIN SPORTS MAX 8 + beIN SPORTS MAX 9 + beIN SPORTS MAX 10 + GOLF+ HD + CANAL+LIVE 1 + CANAL+LIVE 2 + CANAL+LIVE 3 + CANAL+LIVE 4 + CANAL+LIVE 5 + CANAL+LIVE 6 + CANAL+LIVE 7 + RMC Sport Live 3 + RMC Sport Live 4 + XXL + Dorcel TV + Dorcel XXX + Private TV + Hustler TV + VIXEN + Pink TV / Pink X + Man-X + Union TV + DORCEL TV AFRICA + MEN TV + PENTHOUSE HD + Das Erste + SPORT 1 + France 3 Alpes + France 3 Alsace + France 3 Aquitaine + France 3 Auvergne + France 3 Basse-Normandie + France 3 Bourgogne + France 3 Bretagne + France 3 Centre + France 3 Champagne-Ardenne + France 3 via Stella + France 3 Côte d'Azur + France 3 Franche-Comté + France 3 Haute-Normandie + France 3 Languedoc + France 3 Limousin + France 3 Lorraine + France 3 Midi-Pyrénées + France 3 Nord-Pas-de-Calais + France 3 Paris IDF + France 3 Pays de la Loire + France 3 Picardie + France 3 Poitou-Charentes + France 3 Provence-Alpes + France 3 Rhône-Alpes + France 3 Nouvelle Aquitaine + France 3 - Corse + 20 Minutes TV + Télé Bocal + vià93 + Figaro TV IDF + TV78 + Lyon Capitale TV + Télé Grenoble + TL7 Saint Etienne + 8 Mont-Blanc + ILTV + ASTV + Wéo Picardie + Wéo TV, La voix du nord + CRESPIN TELEVISION + Vià MATÉLÉ + 7A Limoges + TV7 Bordeaux + TVPI (TV Biarritz) + ETB1 + ETB2 + ETB3 + KANALDUDE + Grosbliederstroff + TV2COM + TRESSANGE TV + NA TV + Canal 32 + vià Mirabelle + Puissance TV + Télé Schiltigheim + TVMonaco + Mosaik Cristal + TV8 Moselle-Est + vià Vosges + Cannes Lérins TV + Maritima TV + MAURIENNE TV + Angers Télé + Télé Nantes + TV Vendée + LMtv Sarthe + Tébéo + TV Rennes 35 + Val de Loire TV + Zouk TV + Télé Paese + CNN International + BBC NEWS + France 24 ENG + CNBC Europe + Bloomberg + Al Jazeera English + i24 News Anglais + NHK WORLD-JAPAN + Sky News + TGCOM24 + i24 News Arabe + France 24 ARA + Al Jazeera + Medi 1 TV + Al Arabiya + Echorouk TV + Ennahar TV + SIC Noticias + Canal 11 + Rai News 24 + France 24 Espanol + 24 Horas + TVE + DW-TV + WELT + TVN24 + Record News + CGTN + Africa 24 + Canal 2 + V+ + Porto Canal + Local Visao + Benfica TV + A BOLA TV + TV Record + RTP 3 + TVI Internacional + SIC Internacional + Canal Q + RTPI + Rai Uno + Rai Due + Rai Tre + RAI SCUOLA + RAI STORIA + Mediaset Italia + REAL MADRID TV + STAR TVE + Antena 3 + Atres Series + ALL FLAMENCO + TVG EUROPA + TV3 Catalunya + etb basque + TELE MADRID + ANDALUCIA TV + Boomerang Anglais + FilmBox Arthouse + TCM Anglais + TinyTeen + Lang Lab + Lingo Toons + DocuBox HD + FashionBox HD + Pro Sieben + N-TV + RTL Television + RTL2 + Sat 1 + Super RTL + SWR + Vox + ZDF + KABEL EINS + KIKA + RTL NITRO TV + Arte Allemande + 3 SAT + VIVA + iTVN + iTVN Extra + TVP Polonia + Armenia 1 + Antenna 1 + ART CINEMA + ART AFLAM 1 + ART AFLAM 2 + AL HEKAYAT 1 + AL HEKAYAT 2 + TV Romania International + Bahia + Télé Maroc + Samira TV + Canal Algérie + A3 + Beur TV + 2M Maroc + Al Aoula + Arryadia + Assadissa + El Hiwar Ettounsi + Watania 2 + Tunisia 1 + Asharq News + Berbère Jeunesse + Berbère Musique + Berbère TV + Sky News Arabia + CHADA TV + Rotana Comedy + Syria TV + Al Resalah + Alaraby 1 + Echorouk News + El Bilad TV + Dizi + Alaraby 2 + Rotana Aflam+ + Rotana Cinéma+ FR + Rotana Music + Rotana Drama + Rotana Kids + Rotana Cinema + Rotana Classic + Nessma + DMC + Fix et Foxy + Carthage+ + DMC Drama + Iqraa TV + Iqraa International + Al Majd Holy Quran + Al Maghribia + Arrabiâ + Al Jadeed + LBC Sat + Lana TV + NBN + OTV + Murr TV + Rotana M+ + Dubaï TV + ON TV + HANNIBAL TV + Panorama Drama + Panorama Film + Al Masriya + The Israeli Network + Jordan Satellite Channel + Euro Star + Euro D + Habertürk + BEIN MOVIES + SHOW MAX + Show Turk + ATV Avrupa + KANAL 7 AVRUPA + TV8 International + Saudi Channel 1 + A+ + ORTB + NOVELAS + CRTV + Equinoxe TV + CHERIFLA TV + ORTC + TV Congo + Maboke TV + RTNC + NCI + RTI 1 + CDIRECT + Gabon 1ère + RTG + TVM + ORTM + Nollywood TV Epic + Nollywood TV + Pulaagu + Trace Africa + Vox Africa + 2STV + RTS 1 + SEN TV + TFM + SUNU YEUF + Beijing TV + CCTV YULE + CCTV-4 + China Movie Channel (CMC) + Hunan TV + JSBC International + Phoenix CNE + Phoenix Infonews + Shangaï Dragon TV + Great Wall Elite + ZTV World (Zhejiang Star TV) + GRT GBA Satellite TV + CGTN-Français + NTD + KBS World + Colors + Utsav Bharat + Rishtey + Utsav Plus + Zee TV + NHK World Premium + B4U Movies + Geo News + Geo TV + Sony Max + Las Estrellas + Distrito Comedia + De pelicula + RMS + Telehit + tlnovelas \ No newline at end of file diff --git a/sites/tv.sfr.fr/tv.sfr.fr.config.js b/sites/tv.sfr.fr/tv.sfr.fr.config.js index f6c2b64d..c14b7a8a 100644 --- a/sites/tv.sfr.fr/tv.sfr.fr.config.js +++ b/sites/tv.sfr.fr/tv.sfr.fr.config.js @@ -1,65 +1,65 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'tv.sfr.fr', - days: 2, - url({ date }) { - return `https://static-cdn.tv.sfr.net/data/epg/gen8/guide_web_${date.format('YYYYMMDD')}.json` - }, - request: { - maxContentLength: 20 * 1024 * 1024, // 20Mb - cache: { - ttl: 24 * 60 * 60 * 1000 // 1 day - } - }, - parser({ content, channel }) { - let programs = [] - let items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - start: dayjs(item.startDate), - stop: dayjs(item.endDate), - title: item.title, - subTitle: item.subTitle || null, - category: item.genre, - description: item.longSynopsis, - images: item.images.map(img => img.url), - season: item.seasonNumber || null, - episode: item.episodeNumber || null - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://api.sfr.fr/service-channel/api/rest/v2/channels') - .then(r => r.data) - .catch(console.error) - - let channels = {} - Object.values(data.data.chaines).forEach(channel => { - if (!channels[channel.epg_id]) { - channels[channel.epg_id] = { - lang: 'fr', - site_id: channel.epg_id, - name: channel.nom_chaine - } - } - }) - - return Object.values(channels) - } -} - -function parseItems(content, channel) { - try { - const data = JSON.parse(content) - if (!data || !data.epg || !Array.isArray(data.epg[channel.site_id])) return [] - - return data.epg[channel.site_id] - } catch { - return [] - } -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'tv.sfr.fr', + days: 2, + url({ date }) { + return `https://static-cdn.tv.sfr.net/data/epg/gen8/guide_web_${date.format('YYYYMMDD')}.json` + }, + request: { + maxContentLength: 20 * 1024 * 1024, // 20Mb + cache: { + ttl: 24 * 60 * 60 * 1000 // 1 day + } + }, + parser({ content, channel }) { + let programs = [] + let items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + start: dayjs(item.startDate), + stop: dayjs(item.endDate), + title: item.title, + subTitle: item.subTitle || null, + category: item.genre, + description: item.longSynopsis, + images: item.images.map(img => img.url), + season: item.seasonNumber || null, + episode: item.episodeNumber || null + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://api.sfr.fr/service-channel/api/rest/v2/channels') + .then(r => r.data) + .catch(console.error) + + let channels = {} + Object.values(data.data.chaines).forEach(channel => { + if (!channels[channel.epg_id]) { + channels[channel.epg_id] = { + lang: 'fr', + site_id: channel.epg_id, + name: channel.nom_chaine + } + } + }) + + return Object.values(channels) + } +} + +function parseItems(content, channel) { + try { + const data = JSON.parse(content) + if (!data || !data.epg || !Array.isArray(data.epg[channel.site_id])) return [] + + return data.epg[channel.site_id] + } catch { + return [] + } +} diff --git a/sites/tv.sfr.fr/tv.sfr.fr.test.js b/sites/tv.sfr.fr/tv.sfr.fr.test.js index cca7f6b5..29630f9b 100644 --- a/sites/tv.sfr.fr/tv.sfr.fr.test.js +++ b/sites/tv.sfr.fr/tv.sfr.fr.test.js @@ -1,71 +1,71 @@ -const { parser, url } = require('./tv.sfr.fr.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2025-01-18', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '192', - xmltv_id: 'TF1.fr' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://static-cdn.tv.sfr.net/data/epg/gen8/guide_web_20250118.json' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') - let results = parser({ content, channel }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(23) - expect(results[0]).toMatchObject({ - start: '2025-01-18T02:05:00.000Z', - stop: '2025-01-18T05:00:00.000Z', - title: 'Programmes de la nuit', - subTitle: null, - category: 'Programme indéterminé', - description: 'Retrouvez tous vos programmes de nuit.', - images: [ - 'http://static-cdn.tv.sfr.net/data/img/pl/3/6/9/5757963.jpg', - 'http://static-cdn.tv.sfr.net/data/img/pl/5/0/8/7616805.jpg' - ], - season: null, - episode: null - }) - expect(results[22]).toMatchObject({ - start: '2025-01-18T22:40:00.000Z', - stop: '2025-01-19T00:00:00.000Z', - title: 'Star Academy', - subTitle: 'Retour au château', - category: 'Téléréalité', - description: - "C'est en direct du plateau que Nikos Aliagas revient sur les prestations des différents académiciens en compagnie du corps professoral. L'occasion de revenir en détails sur le déroulement du prime avec les aspects positifs mais également les éléments sur lesquels les élèves doivent progresser pour espérer faire la différence sur cette fin d'aventure.", - images: [ - 'http://static-cdn.tv.sfr.net/data/img/pl/1/0/0/9517001.jpg', - 'http://static-cdn.tv.sfr.net/data/img/pl/6/7/2/9992276.jpg', - 'http://static-cdn.tv.sfr.net/data/img/pl/9/0/1/9985109.jpg', - 'http://static-cdn.tv.sfr.net/data/img/pl/6/7/5/9491576.jpg' - ], - season: 12, - episode: 15 - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: '' - }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./tv.sfr.fr.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2025-01-18', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '192', + xmltv_id: 'TF1.fr' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://static-cdn.tv.sfr.net/data/epg/gen8/guide_web_20250118.json' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') + let results = parser({ content, channel }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(23) + expect(results[0]).toMatchObject({ + start: '2025-01-18T02:05:00.000Z', + stop: '2025-01-18T05:00:00.000Z', + title: 'Programmes de la nuit', + subTitle: null, + category: 'Programme indéterminé', + description: 'Retrouvez tous vos programmes de nuit.', + images: [ + 'http://static-cdn.tv.sfr.net/data/img/pl/3/6/9/5757963.jpg', + 'http://static-cdn.tv.sfr.net/data/img/pl/5/0/8/7616805.jpg' + ], + season: null, + episode: null + }) + expect(results[22]).toMatchObject({ + start: '2025-01-18T22:40:00.000Z', + stop: '2025-01-19T00:00:00.000Z', + title: 'Star Academy', + subTitle: 'Retour au château', + category: 'Téléréalité', + description: + "C'est en direct du plateau que Nikos Aliagas revient sur les prestations des différents académiciens en compagnie du corps professoral. L'occasion de revenir en détails sur le déroulement du prime avec les aspects positifs mais également les éléments sur lesquels les élèves doivent progresser pour espérer faire la différence sur cette fin d'aventure.", + images: [ + 'http://static-cdn.tv.sfr.net/data/img/pl/1/0/0/9517001.jpg', + 'http://static-cdn.tv.sfr.net/data/img/pl/6/7/2/9992276.jpg', + 'http://static-cdn.tv.sfr.net/data/img/pl/9/0/1/9985109.jpg', + 'http://static-cdn.tv.sfr.net/data/img/pl/6/7/5/9491576.jpg' + ], + season: 12, + episode: 15 + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: '' + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/tvcesoir.fr/tvcesoir.fr.config.js b/sites/tvcesoir.fr/tvcesoir.fr.config.js index 1a9855bf..5a063677 100644 --- a/sites/tvcesoir.fr/tvcesoir.fr.config.js +++ b/sites/tvcesoir.fr/tvcesoir.fr.config.js @@ -3,7 +3,7 @@ const dayjs = require('dayjs') const utc = require('dayjs/plugin/utc') const timezone = require('dayjs/plugin/timezone') const customParseFormat = require('dayjs/plugin/customParseFormat') -const uniqBy = require('../../scripts/functions') +const uniqBy = require('lodash.uniqby') dayjs.extend(utc) dayjs.extend(timezone) diff --git a/sites/tvhebdo.com/tvhebdo.com.config.js b/sites/tvhebdo.com/tvhebdo.com.config.js index 5f77af29..29f94a44 100644 --- a/sites/tvhebdo.com/tvhebdo.com.config.js +++ b/sites/tvhebdo.com/tvhebdo.com.config.js @@ -1,7 +1,7 @@ const cheerio = require('cheerio') const axios = require('axios') const { DateTime } = require('luxon') -const { uniqBy } = require('../../scripts/functions') +const uniqBy = require('lodash.uniqby') module.exports = { site: 'tvhebdo.com', diff --git a/sites/tvireland.ie/tvireland.ie.config.js b/sites/tvireland.ie/tvireland.ie.config.js index 0ea0e349..10f6274b 100644 --- a/sites/tvireland.ie/tvireland.ie.config.js +++ b/sites/tvireland.ie/tvireland.ie.config.js @@ -3,7 +3,7 @@ const dayjs = require('dayjs') const utc = require('dayjs/plugin/utc') const timezone = require('dayjs/plugin/timezone') const customParseFormat = require('dayjs/plugin/customParseFormat') -const { uniqBy } = require('../../scripts/functions') +const uniqBy = require('lodash.uniqby') dayjs.extend(utc) dayjs.extend(timezone) diff --git a/sites/tvmusor.hu/tvmusor.hu.config.js b/sites/tvmusor.hu/tvmusor.hu.config.js index 52ecb286..a0f92bbc 100644 --- a/sites/tvmusor.hu/tvmusor.hu.config.js +++ b/sites/tvmusor.hu/tvmusor.hu.config.js @@ -1,6 +1,6 @@ const axios = require('axios') const dayjs = require('dayjs') -const { uniqBy } = require('../../scripts/functions') +const uniqBy = require('lodash.uniqby') module.exports = { site: 'tvmusor.hu', diff --git a/sites/vidio.com/vidio.com.channels.xml b/sites/vidio.com/vidio.com.channels.xml index 54e56fde..892af07e 100644 --- a/sites/vidio.com/vidio.com.channels.xml +++ b/sites/vidio.com/vidio.com.channels.xml @@ -1,60 +1,60 @@ - - - ABC Australia - AFRICANEWS TV - Ajwa TV - Aljazeera - ANTV - Arirang - Bein 1 - Bein 2 - Bein 3 - BeritaSatu - BTV - CTV 1 - CTV 2 - CTV 3 - CTV 5 - CTV 6 - Premier League TV - Champions Golf 1 - Champions Golf 2 - News Asia - DAAI TV - Daystar TV - DW English - Elshinta TV - Euro News - GGS TV - Hip Hip Horee! - Horee - Indosiar - Jaktv - jawaposTV - JTV - Kompas TV - Magna TV - Makkah TV - MDTV - Metro TV - Moji - MUSICA - NBA TV - NHK World Japan - Nusantara TV - RTV - ROCK Entertainment - Rock Action - SCTV - SPOTV 2 - SPOTV - Tawaf TV - Trans7 - TRANS TV - TV5Monde - TVN - TVOne - TVRI - U-Channel TV - Zoomoo - + + + ABC Australia + AFRICANEWS TV + Ajwa TV + Aljazeera + ANTV + Arirang + Bein 1 + Bein 2 + Bein 3 + BeritaSatu + BTV + CTV 1 + CTV 2 + CTV 3 + CTV 5 + CTV 6 + Premier League TV + Champions Golf 1 + Champions Golf 2 + News Asia + DAAI TV + Daystar TV + DW English + Elshinta TV + Euro News + GGS TV + Hip Hip Horee! + Horee + Indosiar + Jaktv + jawaposTV + JTV + Kompas TV + Magna TV + Makkah TV + MDTV + Metro TV + Moji + MUSICA + NBA TV + NHK World Japan + Nusantara TV + RTV + ROCK Entertainment + Rock Action + SCTV + SPOTV 2 + SPOTV + Tawaf TV + Trans7 + TRANS TV + TV5Monde + TVN + TVOne + TVRI + U-Channel TV + Zoomoo + diff --git a/sites/vidio.com/vidio.com.config.js b/sites/vidio.com/vidio.com.config.js index 3d0ab272..000566c4 100644 --- a/sites/vidio.com/vidio.com.config.js +++ b/sites/vidio.com/vidio.com.config.js @@ -1,89 +1,89 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const crypto = require('crypto') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -const WEB_CLIENT_SECRET = Buffer.from('dPr0QImQ7bc5o9LMntNba2DOsSbZcjUh') -const WEB_CLIENT_IV = Buffer.from('C8RWsrtFsoeyCyPt') - -module.exports = { - site: 'vidio.com', - days: 2, - url({ date, channel }) { - return `https://api.vidio.com/livestreamings/${channel.site_id}/schedules?filter[date]=${date.format('YYYY-MM-DD')}` - }, - request: { - async headers() { - const session = await loadSessionDetails() - if (!session || !session.api_key) return null - - var cipher = crypto.createCipheriv('aes-256-cbc', WEB_CLIENT_SECRET, WEB_CLIENT_IV) - return { - 'X-API-Key': cipher.update(session.api_key, 'utf8', 'base64') + cipher.final('base64'), - 'X-Secure-Level': 2 - } - } - }, - parser({ content }) { - const programs = [] - const json = JSON.parse(content) - if (Array.isArray(json?.data)) { - for (const program of json.data) { - programs.push({ - title: program.attributes.title, - description: program.attributes.description, - start: dayjs(program.attributes.start_time), - stop: dayjs(program.attributes.end_time), - image: program.attributes.image_landscape_url - }) - } - } - - return programs - }, - async channels() { - const channels = [] - const json = await axios - .get( - 'https://api.vidio.com/livestreamings?stream_type=tv_stream', - { - headers: await this.request.headers() - } - ) - .then(response => response.data) - .catch(console.error) - - if (Array.isArray(json?.data)) { - for (const channel of json.data) { - channels.push({ - lang: 'id', - site_id: channel.id, - name: channel.attributes.title - }) - } - } - - return channels - } -} - -function loadSessionDetails() { - return axios - .post( - 'https://www.vidio.com/auth', - {}, - { - headers: { - 'Content-Type': 'application/json' - } - } - ) - .then(r => r.data) - .catch(console.log) +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const crypto = require('crypto') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +const WEB_CLIENT_SECRET = Buffer.from('dPr0QImQ7bc5o9LMntNba2DOsSbZcjUh') +const WEB_CLIENT_IV = Buffer.from('C8RWsrtFsoeyCyPt') + +module.exports = { + site: 'vidio.com', + days: 2, + url({ date, channel }) { + return `https://api.vidio.com/livestreamings/${channel.site_id}/schedules?filter[date]=${date.format('YYYY-MM-DD')}` + }, + request: { + async headers() { + const session = await loadSessionDetails() + if (!session || !session.api_key) return null + + var cipher = crypto.createCipheriv('aes-256-cbc', WEB_CLIENT_SECRET, WEB_CLIENT_IV) + return { + 'X-API-Key': cipher.update(session.api_key, 'utf8', 'base64') + cipher.final('base64'), + 'X-Secure-Level': 2 + } + } + }, + parser({ content }) { + const programs = [] + const json = JSON.parse(content) + if (Array.isArray(json?.data)) { + for (const program of json.data) { + programs.push({ + title: program.attributes.title, + description: program.attributes.description, + start: dayjs(program.attributes.start_time), + stop: dayjs(program.attributes.end_time), + image: program.attributes.image_landscape_url + }) + } + } + + return programs + }, + async channels() { + const channels = [] + const json = await axios + .get( + 'https://api.vidio.com/livestreamings?stream_type=tv_stream', + { + headers: await this.request.headers() + } + ) + .then(response => response.data) + .catch(console.error) + + if (Array.isArray(json?.data)) { + for (const channel of json.data) { + channels.push({ + lang: 'id', + site_id: channel.id, + name: channel.attributes.title + }) + } + } + + return channels + } +} + +function loadSessionDetails() { + return axios + .post( + 'https://www.vidio.com/auth', + {}, + { + headers: { + 'Content-Type': 'application/json' + } + } + ) + .then(r => r.data) + .catch(console.log) } \ No newline at end of file diff --git a/sites/vidio.com/vidio.com.test.js b/sites/vidio.com/vidio.com.test.js index 2e6689eb..58a66e65 100644 --- a/sites/vidio.com/vidio.com.test.js +++ b/sites/vidio.com/vidio.com.test.js @@ -1,67 +1,67 @@ -const { parser, url, request } = require('./vidio.com.config.js') -const fs = require('fs') -const path = require('path') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2025-07-01', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '204', - xmltv_id: 'SCTV.id' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://api.vidio.com/livestreamings/204/schedules?filter[date]=2025-07-01' - ) -}) - -it('can generate valid request headers', async () => { - axios.post.mockImplementation(url => { - if (url === 'https://www.vidio.com/auth') { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/auth.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - const result = await request.headers() - expect(result).toMatchObject({ - 'X-API-Key': - 'CH1ZFsN4N/MIfAds1DL9mP151CNqIpWHqZGRr+LkvUyiq3FRPuP1Kt6aK+pG3nEC1FXt0ZAAJ5FKP8QU8CZ5/jQdSYLVeFwl9NoIkegVpR6b7W2ZwbaF00OPr6ON1/FpLQ3RiUzTPpAqe7f+fwhOr0KrKy8PpCa54OHogaEjI3w=', - 'X-Secure-Level': 2, - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const results = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(21) - expect(results[0]).toMatchObject({ - start: '2025-06-30T15:57:00.000Z', - stop: '2025-06-30T17:29:00.000Z', - title: 'Ftv PrimeTime : Cinta Dodol Inilah Yang Membuatku Lengket Padamu', - description: 'Film televisi yang mengangkat kisah romantisme kehidupan dengan konflik yang menarik. tayang setiap hari' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const results = parser({ content, channel }) - - expect(results).toMatchObject([]) -}) +const { parser, url, request } = require('./vidio.com.config.js') +const fs = require('fs') +const path = require('path') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2025-07-01', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '204', + xmltv_id: 'SCTV.id' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://api.vidio.com/livestreamings/204/schedules?filter[date]=2025-07-01' + ) +}) + +it('can generate valid request headers', async () => { + axios.post.mockImplementation(url => { + if (url === 'https://www.vidio.com/auth') { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/auth.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + const result = await request.headers() + expect(result).toMatchObject({ + 'X-API-Key': + 'CH1ZFsN4N/MIfAds1DL9mP151CNqIpWHqZGRr+LkvUyiq3FRPuP1Kt6aK+pG3nEC1FXt0ZAAJ5FKP8QU8CZ5/jQdSYLVeFwl9NoIkegVpR6b7W2ZwbaF00OPr6ON1/FpLQ3RiUzTPpAqe7f+fwhOr0KrKy8PpCa54OHogaEjI3w=', + 'X-Secure-Level': 2, + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(21) + expect(results[0]).toMatchObject({ + start: '2025-06-30T15:57:00.000Z', + stop: '2025-06-30T17:29:00.000Z', + title: 'Ftv PrimeTime : Cinta Dodol Inilah Yang Membuatku Lengket Padamu', + description: 'Film televisi yang mengangkat kisah romantisme kehidupan dengan konflik yang menarik. tayang setiap hari' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const results = parser({ content, channel }) + + expect(results).toMatchObject([]) +}) diff --git a/tests/__data__/expected/epg_grab/base.guide.xml b/tests/__data__/expected/epg_grab/base.guide.xml index 09471f36..0f9c2ca0 100644 --- a/tests/__data__/expected/epg_grab/base.guide.xml +++ b/tests/__data__/expected/epg_grab/base.guide.xml @@ -1,9 +1,9 @@ - -Channel 2https://example.com36 -Channel 1https://example.com -Channel 1https://example.com -Programme1 (example.com) -Program1 (example.com) -Programme1 (example.com) -Program1 (example.com) + +Channel 2https://example.com36 +Channel 1https://example.com +Channel 1https://example.com +Programme1 (example.com) +Program1 (example.com) +Programme1 (example.com) +Program1 (example.com) \ No newline at end of file diff --git a/tests/__data__/expected/epg_grab/custom_channels.guide.xml b/tests/__data__/expected/epg_grab/custom_channels.guide.xml index e2c7bf4c..dd84f7dc 100644 --- a/tests/__data__/expected/epg_grab/custom_channels.guide.xml +++ b/tests/__data__/expected/epg_grab/custom_channels.guide.xml @@ -1,15 +1,15 @@ - -Custom Channel 1https://example.com -Custom Channel 2https://example.com -Channel 1https://example.com -Channel 3https://example2.com -Channel 4https://example2.com -Channel 1https://example2.com -Programme1 (example.com) -Program1 (example.com) -Programme1 (example2.com) -Programme1 (example.com) -Program1 (example.com) -Program1 (example2.com) -Program1 (example2.com) + +Custom Channel 1https://example.com +Custom Channel 2https://example.com +Channel 1https://example.com +Channel 3https://example2.com +Channel 4https://example2.com +Channel 1https://example2.com +Programme1 (example.com) +Program1 (example.com) +Programme1 (example2.com) +Programme1 (example.com) +Program1 (example.com) +Program1 (example2.com) +Program1 (example2.com) \ No newline at end of file diff --git a/tests/__data__/expected/epg_grab/guides/en/example.com.xml b/tests/__data__/expected/epg_grab/guides/en/example.com.xml index c1270824..8b20e42a 100644 --- a/tests/__data__/expected/epg_grab/guides/en/example.com.xml +++ b/tests/__data__/expected/epg_grab/guides/en/example.com.xml @@ -1,6 +1,6 @@ - -Channel 2https://example.com36 -Channel 1https://example.com -Program1 (example.com) -Program1 (example.com) + +Channel 2https://example.com36 +Channel 1https://example.com +Program1 (example.com) +Program1 (example.com) \ No newline at end of file diff --git a/tests/__data__/expected/epg_grab/lang.guide.xml b/tests/__data__/expected/epg_grab/lang.guide.xml index dc4b8238..7016b8e8 100644 --- a/tests/__data__/expected/epg_grab/lang.guide.xml +++ b/tests/__data__/expected/epg_grab/lang.guide.xml @@ -1,8 +1,8 @@ - -Channel 1https://example.com -Channel 3https://example.com -Programme1 (example.com) -Programme1 (example.com) -Program1 (example.com) -Program1 (example.com) + +Channel 1https://example.com +Channel 3https://example.com +Programme1 (example.com) +Programme1 (example.com) +Program1 (example.com) +Program1 (example.com) \ No newline at end of file diff --git a/tests/__data__/expected/epg_grab/proxy.guide.xml b/tests/__data__/expected/epg_grab/proxy.guide.xml index 09471f36..0f9c2ca0 100644 --- a/tests/__data__/expected/epg_grab/proxy.guide.xml +++ b/tests/__data__/expected/epg_grab/proxy.guide.xml @@ -1,9 +1,9 @@ - -Channel 2https://example.com36 -Channel 1https://example.com -Channel 1https://example.com -Programme1 (example.com) -Program1 (example.com) -Programme1 (example.com) -Program1 (example.com) + +Channel 2https://example.com36 +Channel 1https://example.com +Channel 1https://example.com +Programme1 (example.com) +Program1 (example.com) +Programme1 (example.com) +Program1 (example.com) \ No newline at end of file diff --git a/tests/__data__/expected/epg_grab/template.guide.xml b/tests/__data__/expected/epg_grab/template.guide.xml index cd54510c..8eb79411 100644 --- a/tests/__data__/expected/epg_grab/template.guide.xml +++ b/tests/__data__/expected/epg_grab/template.guide.xml @@ -1,15 +1,15 @@ - -Channel 2https://example.com36 -Channel 1https://example.com -Channel 1https://example.com -Channel 3https://example2.com -Channel 4https://example2.com -Channel 1https://example2.com -Programme1 (example.com) -Program1 (example.com) -Programme1 (example2.com) -Programme1 (example.com) -Program1 (example.com) -Program1 (example2.com) -Program1 (example2.com) + +Channel 2https://example.com36 +Channel 1https://example.com +Channel 1https://example.com +Channel 3https://example2.com +Channel 4https://example2.com +Channel 1https://example2.com +Programme1 (example.com) +Program1 (example.com) +Programme1 (example2.com) +Programme1 (example.com) +Program1 (example.com) +Program1 (example2.com) +Program1 (example2.com) \ No newline at end of file diff --git a/tests/__data__/input/__data__/channels.json b/tests/__data__/input/__data__/channels.json index d838427f..d2e6a4d5 100644 --- a/tests/__data__/input/__data__/channels.json +++ b/tests/__data__/input/__data__/channels.json @@ -1,61 +1,61 @@ -[ - { - "id": "Bravo.us", - "name": "Bravo", - "network": null, - "country": "US", - "subdivision": null, - "city": null, - "categories": [], - "is_nsfw": false, - "closed": "2020-01-01", - "replaced_by": "R6.co" - }, - { - "id": "Bravos.us", - "name": "Bravos", - "network": null, - "country": "US", - "subdivision": null, - "city": null, - "categories": [], - "is_nsfw": false - }, - { - "id": "CNNInternational.us", - "name": "CNN International", - "alt_names": ["CNN", "CNN Int"], - "network": null, - "country": "US", - "subdivision": null, - "city": null, - "categories": [ - "news" - ], - "is_nsfw": false - }, - { - "id": "MNetMovies2.za", - "name": "M-Net Movies 2", - "network": null, - "country": "ZA", - "subdivision": null, - "city": null, - "categories": [], - "is_nsfw": false - }, - {"id":"6eren.dk","name":"6'eren","alt_names":[],"network":null,"owners":["Warner Bros. Discovery EMEA"],"country":"DK","subdivision":null,"city":null,"broadcast_area":["c/DK"],"languages":["dan"],"categories":[],"is_nsfw":false,"launched":"2009-01-01","closed":null,"replaced_by":null,"website":"http://www.6-eren.dk/"}, - {"id":"BBCNews.uk","name":"BBC News","alt_names":[],"network":null,"owners":[],"country":"UK","subdivision":null,"city":null,"broadcast_area":["c/UK"],"languages":["eng"],"categories":["news"],"is_nsfw":false,"launched":null,"closed":null,"replaced_by":null,"website":"http://news.bbc.co.uk/"}, - { - "id": "CNN.us", - "name": "CNN", - "network": null, - "country": "US", - "subdivision": null, - "city": null, - "categories": [], - "is_nsfw": false - }, - {"id":"Channel2.us","name":"Channel 2 [API]","alt_names":[],"network":null,"owners":[],"country":"UK","subdivision":null,"city":null,"broadcast_area":["c/US"],"languages":["eng"],"categories":[],"is_nsfw":false,"launched":null,"closed":null,"replaced_by":null,"website":""}, - {"id":"Channel3.us","name":"Channel 3 [API]","alt_names":[],"network":null,"owners":[],"country":"UK","subdivision":null,"city":null,"broadcast_area":["c/US"],"languages":["eng"],"categories":[],"is_nsfw":false,"launched":null,"closed":null,"replaced_by":null,"website":""} +[ + { + "id": "Bravo.us", + "name": "Bravo", + "network": null, + "country": "US", + "subdivision": null, + "city": null, + "categories": [], + "is_nsfw": false, + "closed": "2020-01-01", + "replaced_by": "R6.co" + }, + { + "id": "Bravos.us", + "name": "Bravos", + "network": null, + "country": "US", + "subdivision": null, + "city": null, + "categories": [], + "is_nsfw": false + }, + { + "id": "CNNInternational.us", + "name": "CNN International", + "alt_names": ["CNN", "CNN Int"], + "network": null, + "country": "US", + "subdivision": null, + "city": null, + "categories": [ + "news" + ], + "is_nsfw": false + }, + { + "id": "MNetMovies2.za", + "name": "M-Net Movies 2", + "network": null, + "country": "ZA", + "subdivision": null, + "city": null, + "categories": [], + "is_nsfw": false + }, + {"id":"6eren.dk","name":"6'eren","alt_names":[],"network":null,"owners":["Warner Bros. Discovery EMEA"],"country":"DK","subdivision":null,"city":null,"broadcast_area":["c/DK"],"languages":["dan"],"categories":[],"is_nsfw":false,"launched":"2009-01-01","closed":null,"replaced_by":null,"website":"http://www.6-eren.dk/"}, + {"id":"BBCNews.uk","name":"BBC News","alt_names":[],"network":null,"owners":[],"country":"UK","subdivision":null,"city":null,"broadcast_area":["c/UK"],"languages":["eng"],"categories":["news"],"is_nsfw":false,"launched":null,"closed":null,"replaced_by":null,"website":"http://news.bbc.co.uk/"}, + { + "id": "CNN.us", + "name": "CNN", + "network": null, + "country": "US", + "subdivision": null, + "city": null, + "categories": [], + "is_nsfw": false + }, + {"id":"Channel2.us","name":"Channel 2 [API]","alt_names":[],"network":null,"owners":[],"country":"UK","subdivision":null,"city":null,"broadcast_area":["c/US"],"languages":["eng"],"categories":[],"is_nsfw":false,"launched":null,"closed":null,"replaced_by":null,"website":""}, + {"id":"Channel3.us","name":"Channel 3 [API]","alt_names":[],"network":null,"owners":[],"country":"UK","subdivision":null,"city":null,"broadcast_area":["c/US"],"languages":["eng"],"categories":[],"is_nsfw":false,"launched":null,"closed":null,"replaced_by":null,"website":""} ] \ No newline at end of file diff --git a/tests/__data__/input/epg_grab/example.com/example.com.config.js b/tests/__data__/input/epg_grab/example.com/example.com.config.js index 52370045..107a761e 100644 --- a/tests/__data__/input/epg_grab/example.com/example.com.config.js +++ b/tests/__data__/input/epg_grab/example.com/example.com.config.js @@ -1,28 +1,28 @@ -module.exports = { - site: 'example.com', - days: 2, - request: { - timeout: 1000 - }, - url: 'https://example.com', - parser({ channel, date }) { - if (channel.xmltv_id === 'Channel2.us') return [] - else if (channel.xmltv_id === 'Channel1.us' && channel.lang === 'fr') { - return [ - { - title: 'Programme1 (example.com)', - start: `${date.format('YYYY-MM-DD')}T04:30:00.000Z`, - stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` - } - ] - } - - return [ - { - title: 'Program1 (example.com)', - start: `${date.format('YYYY-MM-DD')}T04:31:00.000Z`, - stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` - } - ] - } -} +module.exports = { + site: 'example.com', + days: 2, + request: { + timeout: 1000 + }, + url: 'https://example.com', + parser({ channel, date }) { + if (channel.xmltv_id === 'Channel2.us') return [] + else if (channel.xmltv_id === 'Channel1.us' && channel.lang === 'fr') { + return [ + { + title: 'Programme1 (example.com)', + start: `${date.format('YYYY-MM-DD')}T04:30:00.000Z`, + stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` + } + ] + } + + return [ + { + title: 'Program1 (example.com)', + start: `${date.format('YYYY-MM-DD')}T04:31:00.000Z`, + stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` + } + ] + } +} diff --git a/tests/__data__/input/epg_grab/sites/example.com/example.com.config.js b/tests/__data__/input/epg_grab/sites/example.com/example.com.config.js index 52370045..107a761e 100644 --- a/tests/__data__/input/epg_grab/sites/example.com/example.com.config.js +++ b/tests/__data__/input/epg_grab/sites/example.com/example.com.config.js @@ -1,28 +1,28 @@ -module.exports = { - site: 'example.com', - days: 2, - request: { - timeout: 1000 - }, - url: 'https://example.com', - parser({ channel, date }) { - if (channel.xmltv_id === 'Channel2.us') return [] - else if (channel.xmltv_id === 'Channel1.us' && channel.lang === 'fr') { - return [ - { - title: 'Programme1 (example.com)', - start: `${date.format('YYYY-MM-DD')}T04:30:00.000Z`, - stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` - } - ] - } - - return [ - { - title: 'Program1 (example.com)', - start: `${date.format('YYYY-MM-DD')}T04:31:00.000Z`, - stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` - } - ] - } -} +module.exports = { + site: 'example.com', + days: 2, + request: { + timeout: 1000 + }, + url: 'https://example.com', + parser({ channel, date }) { + if (channel.xmltv_id === 'Channel2.us') return [] + else if (channel.xmltv_id === 'Channel1.us' && channel.lang === 'fr') { + return [ + { + title: 'Programme1 (example.com)', + start: `${date.format('YYYY-MM-DD')}T04:30:00.000Z`, + stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` + } + ] + } + + return [ + { + title: 'Program1 (example.com)', + start: `${date.format('YYYY-MM-DD')}T04:31:00.000Z`, + stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` + } + ] + } +} diff --git a/tests/__data__/input/epg_grab/sites/example2.com/example2.com.config.js b/tests/__data__/input/epg_grab/sites/example2.com/example2.com.config.js index 9e4f58d3..b724fa19 100644 --- a/tests/__data__/input/epg_grab/sites/example2.com/example2.com.config.js +++ b/tests/__data__/input/epg_grab/sites/example2.com/example2.com.config.js @@ -1,23 +1,23 @@ -module.exports = { - site: 'example2.com', - url: 'https://example2.com', - parser({ channel, date }) { - if (channel.lang === 'fr') { - return [ - { - title: 'Programme1 (example2.com)', - start: `${date.format('YYYY-MM-DD')}T04:40:00.000Z`, - stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` - } - ] - } - - return [ - { - title: 'Program1 (example2.com)', - start: `${date.format('YYYY-MM-DD')}T04:31:00.000Z`, - stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` - } - ] - } -} +module.exports = { + site: 'example2.com', + url: 'https://example2.com', + parser({ channel, date }) { + if (channel.lang === 'fr') { + return [ + { + title: 'Programme1 (example2.com)', + start: `${date.format('YYYY-MM-DD')}T04:40:00.000Z`, + stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` + } + ] + } + + return [ + { + title: 'Program1 (example2.com)', + start: `${date.format('YYYY-MM-DD')}T04:31:00.000Z`, + stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` + } + ] + } +} diff --git a/tests/commands/api/generate.test.ts b/tests/commands/api/generate.test.ts index b129ed26..2044f0d9 100644 --- a/tests/commands/api/generate.test.ts +++ b/tests/commands/api/generate.test.ts @@ -1,27 +1,27 @@ -import { execSync } from 'child_process' -import fs from 'fs-extra' -import { pathToFileURL } from 'node:url' - -const ENV_VAR = 'cross-env SITES_DIR=tests/__data__/input/api_generate/sites API_DIR=tests/__data__/output' - -beforeEach(() => { - fs.emptyDirSync('tests/__data__/output') -}) - -describe('api:generate', () => { - it('can generate guides.json', () => { - const cmd = `${ENV_VAR} npm run api:generate` - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - - expect(content('tests/__data__/output/guides.json')).toEqual( - content('tests/__data__/expected/api_generate/guides.json') - ) - }) -}) - -function content(filepath: string) { - return fs.readFileSync(pathToFileURL(filepath), { - encoding: 'utf8' - }) -} +import { execSync } from 'child_process' +import fs from 'fs-extra' +import { pathToFileURL } from 'node:url' + +const ENV_VAR = 'cross-env SITES_DIR=tests/__data__/input/api_generate/sites API_DIR=tests/__data__/output' + +beforeEach(() => { + fs.emptyDirSync('tests/__data__/output') +}) + +describe('api:generate', () => { + it('can generate guides.json', () => { + const cmd = `${ENV_VAR} npm run api:generate` + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + + expect(content('tests/__data__/output/guides.json')).toEqual( + content('tests/__data__/expected/api_generate/guides.json') + ) + }) +}) + +function content(filepath: string) { + return fs.readFileSync(pathToFileURL(filepath), { + encoding: 'utf8' + }) +} diff --git a/tests/commands/channels/edit.test.ts b/tests/commands/channels/edit.test.ts index 0c640543..b150648a 100644 --- a/tests/commands/channels/edit.test.ts +++ b/tests/commands/channels/edit.test.ts @@ -1,36 +1,36 @@ -import { execSync } from 'child_process' -import fs from 'fs-extra' - -const ENV_VAR = 'cross-env DATA_DIR=tests/__data__/input/__data__' - -beforeEach(() => { - fs.emptyDirSync('tests/__data__/output') - fs.copySync( - 'tests/__data__/input/channels_edit/example.com.channels.xml', - 'tests/__data__/output/channels.xml' - ) -}) - -describe('channels:edit', () => { - it('shows list of options for a channel', () => { - const cmd = `${ENV_VAR} npm run channels:edit --- tests/__data__/output/channels.xml` - try { - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - checkStdout(stdout) - } catch (error: unknown) { - // NOTE: for Windows only - if (process.env.DEBUG === 'true') console.log(cmd, error) - if (error && typeof error === 'object' && 'stdout' in error) { - checkStdout(error.stdout as string) - } - } - }) -}) - -function checkStdout(stdout: string) { - expect(stdout).toContain('CNNInternational.us (CNN International, CNN, CNN Int)') - expect(stdout).toContain('Type...') - expect(stdout).toContain('Skip') - expect(stdout).toContain("File 'tests/__data__/output/channels.xml' successfully saved") -} +import { execSync } from 'child_process' +import fs from 'fs-extra' + +const ENV_VAR = 'cross-env DATA_DIR=tests/__data__/input/__data__' + +beforeEach(() => { + fs.emptyDirSync('tests/__data__/output') + fs.copySync( + 'tests/__data__/input/channels_edit/example.com.channels.xml', + 'tests/__data__/output/channels.xml' + ) +}) + +describe('channels:edit', () => { + it('shows list of options for a channel', () => { + const cmd = `${ENV_VAR} npm run channels:edit --- tests/__data__/output/channels.xml` + try { + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + checkStdout(stdout) + } catch (error: unknown) { + // NOTE: for Windows only + if (process.env.DEBUG === 'true') console.log(cmd, error) + if (error && typeof error === 'object' && 'stdout' in error) { + checkStdout(error.stdout as string) + } + } + }) +}) + +function checkStdout(stdout: string) { + expect(stdout).toContain('CNNInternational.us (CNN International, CNN, CNN Int)') + expect(stdout).toContain('Type...') + expect(stdout).toContain('Skip') + expect(stdout).toContain("File 'tests/__data__/output/channels.xml' successfully saved") +} diff --git a/tests/commands/epg/grab.test.ts b/tests/commands/epg/grab.test.ts index 3459f99a..37d35da1 100644 --- a/tests/commands/epg/grab.test.ts +++ b/tests/commands/epg/grab.test.ts @@ -1,162 +1,162 @@ -import { pathToFileURL } from 'node:url' -import { execSync } from 'child_process' -import { Zip } from '@freearhey/core' -import fs from 'fs-extra' -import path from 'path' - -const ENV_VAR = - 'cross-env SITES_DIR=tests/__data__/input/epg_grab/sites CURR_DATE=2022-10-20 DATA_DIR=tests/__data__/input/__data__' - -beforeEach(() => { - fs.emptyDirSync('tests/__data__/output') -}) - -describe('epg:grab', () => { - it('can grab epg by site name', () => { - const cmd = `${ENV_VAR} npm run grab --- --site=example.com --output="${path.resolve( - 'tests/__data__/output/guide.xml' - )}" --timeout=100` - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - - expect(content('tests/__data__/output/guide.xml')).toEqual( - content('tests/__data__/expected/epg_grab/base.guide.xml') - ) - }) - - it('it will raise an error if the timeout is exceeded', () => { - const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/custom.channels.xml --output=tests/__data__/output/guide.xml --timeout=0` - let errorThrown = false - try { - execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }) - // If no error is thrown, explicitly fail the test - fail('Expected command to throw an error due to timeout, but it did not.') - } catch (error) { - errorThrown = true - if (process.env.DEBUG === 'true') { - const stderr = error.stderr?.toString() || '' - const stdout = error.stdout?.toString() || '' - const combined = stderr + stdout - console.log('stdout:', stdout) - console.log('stderr:', stderr) - console.log('combined:', combined) - console.log('exit code:', error.exitCode) - console.log('Error output:', combined) - } - } - expect(errorThrown).toBe(true) - }) - - it('can grab epg with wildcard as output', () => { - const cmd = `${ENV_VAR} npm run grab --- --channels="tests/__data__/input/epg_grab/sites/example.com/example.com.channels.xml" --output="tests/__data__/output/guides/{lang}/{site}.xml" --timeout=100` - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - - expect(content('tests/__data__/output/guides/en/example.com.xml')).toEqual( - content('tests/__data__/expected/epg_grab/guides/en/example.com.xml') - ) - - expect(content('tests/__data__/output/guides/fr/example.com.xml')).toEqual( - content('tests/__data__/expected/epg_grab/guides/fr/example.com.xml') - ) - }) - - it('can grab epg then language filter enabled', () => { - const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/sites/example.com/example.com.channels.xml --output=tests/__data__/output/guides/{lang}/{site}.xml --lang=fr --timeout=100` - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - - expect(content('tests/__data__/output/guides/fr/example.com.xml')).toEqual( - content('tests/__data__/expected/epg_grab/guides/fr/example.com.xml') - ) - }) - - it('can grab epg then using a multi-language filter', () => { - const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/example.com/example.com.channels.xml --output=tests/__data__/output/guides/{site}.xml --lang=fr,it --timeout=100` - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - - expect(content('tests/__data__/output/guides/example.com.xml')).toEqual( - content('tests/__data__/expected/epg_grab/lang.guide.xml') - ) - }) - - it('can grab epg via https proxy', () => { - const cmd = `${ENV_VAR} npm run grab --- --site=example.com --proxy=https://bob:123456@proxy.com:1234 --output="${path.resolve( - 'tests/__data__/output/guide.xml' - )}" --timeout=100` - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - - expect(content('tests/__data__/output/guide.xml')).toEqual( - content('tests/__data__/expected/epg_grab/proxy.guide.xml') - ) - }) - - it('can grab epg via socks5 proxy', () => { - const cmd = `${ENV_VAR} npm run grab --- --site=example.com --proxy=socks5://bob:123456@proxy.com:1234 --output="${path.resolve( - 'tests/__data__/output/guide.xml' - )}" --timeout=100` - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - - expect(content('tests/__data__/output/guide.xml')).toEqual( - content('tests/__data__/expected/epg_grab/proxy.guide.xml') - ) - }) - - it('can grab epg with curl option', () => { - const cmd = `${ENV_VAR} npm run grab --- --site=example.com --curl --output="${path.resolve( - 'tests/__data__/output/guide.xml' - )}" --timeout=100` - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - - expect(stdout).toContain('curl https://example.com') - }) - - it('can grab epg with multiple channels.xml files', () => { - const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/sites/**/*.channels.xml --output=tests/__data__/output/guide.xml --timeout=100` - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - - expect(content('tests/__data__/output/guide.xml')).toEqual( - content('tests/__data__/expected/epg_grab/template.guide.xml') - ) - }) - - it('can grab epg using custom channels list', () => { - const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/custom.channels.xml --output=tests/__data__/output/guide.xml --timeout=100` - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - - expect(content('tests/__data__/output/guide.xml')).toEqual( - content('tests/__data__/expected/epg_grab/custom_channels.guide.xml') - ) - }) - - it('can grab epg with gzip option enabled', () => { - const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/sites/**/*.channels.xml --output="${path.resolve( - 'tests/__data__/output/guide.xml' - )}" --gzip --timeout=100` - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - - expect(content('tests/__data__/output/guide.xml')).toEqual( - content('tests/__data__/expected/epg_grab/template.guide.xml') - ) - - const zip = new Zip() - const expected = zip.decompress(fs.readFileSync('tests/__data__/output/guide.xml.gz')) - const result = zip.decompress( - fs.readFileSync('tests/__data__/expected/epg_grab/template.guide.xml.gz') - ) - expect(expected).toEqual(result) - }) -}) - -function content(filepath: string) { - return fs.readFileSync(pathToFileURL(filepath), { - encoding: 'utf8' - }) -} +import { pathToFileURL } from 'node:url' +import { execSync } from 'child_process' +import { Zip } from '@freearhey/core' +import fs from 'fs-extra' +import path from 'path' + +const ENV_VAR = + 'cross-env SITES_DIR=tests/__data__/input/epg_grab/sites CURR_DATE=2022-10-20 DATA_DIR=tests/__data__/input/__data__' + +beforeEach(() => { + fs.emptyDirSync('tests/__data__/output') +}) + +describe('epg:grab', () => { + it('can grab epg by site name', () => { + const cmd = `${ENV_VAR} npm run grab --- --site=example.com --output="${path.resolve( + 'tests/__data__/output/guide.xml' + )}" --timeout=100` + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + + expect(content('tests/__data__/output/guide.xml')).toEqual( + content('tests/__data__/expected/epg_grab/base.guide.xml') + ) + }) + + it('it will raise an error if the timeout is exceeded', () => { + const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/custom.channels.xml --output=tests/__data__/output/guide.xml --timeout=0` + let errorThrown = false + try { + execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }) + // If no error is thrown, explicitly fail the test + fail('Expected command to throw an error due to timeout, but it did not.') + } catch (error) { + errorThrown = true + if (process.env.DEBUG === 'true') { + const stderr = error.stderr?.toString() || '' + const stdout = error.stdout?.toString() || '' + const combined = stderr + stdout + console.log('stdout:', stdout) + console.log('stderr:', stderr) + console.log('combined:', combined) + console.log('exit code:', error.exitCode) + console.log('Error output:', combined) + } + } + expect(errorThrown).toBe(true) + }) + + it('can grab epg with wildcard as output', () => { + const cmd = `${ENV_VAR} npm run grab --- --channels="tests/__data__/input/epg_grab/sites/example.com/example.com.channels.xml" --output="tests/__data__/output/guides/{lang}/{site}.xml" --timeout=100` + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + + expect(content('tests/__data__/output/guides/en/example.com.xml')).toEqual( + content('tests/__data__/expected/epg_grab/guides/en/example.com.xml') + ) + + expect(content('tests/__data__/output/guides/fr/example.com.xml')).toEqual( + content('tests/__data__/expected/epg_grab/guides/fr/example.com.xml') + ) + }) + + it('can grab epg then language filter enabled', () => { + const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/sites/example.com/example.com.channels.xml --output=tests/__data__/output/guides/{lang}/{site}.xml --lang=fr --timeout=100` + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + + expect(content('tests/__data__/output/guides/fr/example.com.xml')).toEqual( + content('tests/__data__/expected/epg_grab/guides/fr/example.com.xml') + ) + }) + + it('can grab epg then using a multi-language filter', () => { + const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/example.com/example.com.channels.xml --output=tests/__data__/output/guides/{site}.xml --lang=fr,it --timeout=100` + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + + expect(content('tests/__data__/output/guides/example.com.xml')).toEqual( + content('tests/__data__/expected/epg_grab/lang.guide.xml') + ) + }) + + it('can grab epg via https proxy', () => { + const cmd = `${ENV_VAR} npm run grab --- --site=example.com --proxy=https://bob:123456@proxy.com:1234 --output="${path.resolve( + 'tests/__data__/output/guide.xml' + )}" --timeout=100` + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + + expect(content('tests/__data__/output/guide.xml')).toEqual( + content('tests/__data__/expected/epg_grab/proxy.guide.xml') + ) + }) + + it('can grab epg via socks5 proxy', () => { + const cmd = `${ENV_VAR} npm run grab --- --site=example.com --proxy=socks5://bob:123456@proxy.com:1234 --output="${path.resolve( + 'tests/__data__/output/guide.xml' + )}" --timeout=100` + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + + expect(content('tests/__data__/output/guide.xml')).toEqual( + content('tests/__data__/expected/epg_grab/proxy.guide.xml') + ) + }) + + it('can grab epg with curl option', () => { + const cmd = `${ENV_VAR} npm run grab --- --site=example.com --curl --output="${path.resolve( + 'tests/__data__/output/guide.xml' + )}" --timeout=100` + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + + expect(stdout).toContain('curl https://example.com') + }) + + it('can grab epg with multiple channels.xml files', () => { + const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/sites/**/*.channels.xml --output=tests/__data__/output/guide.xml --timeout=100` + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + + expect(content('tests/__data__/output/guide.xml')).toEqual( + content('tests/__data__/expected/epg_grab/template.guide.xml') + ) + }) + + it('can grab epg using custom channels list', () => { + const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/custom.channels.xml --output=tests/__data__/output/guide.xml --timeout=100` + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + + expect(content('tests/__data__/output/guide.xml')).toEqual( + content('tests/__data__/expected/epg_grab/custom_channels.guide.xml') + ) + }) + + it('can grab epg with gzip option enabled', () => { + const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/sites/**/*.channels.xml --output="${path.resolve( + 'tests/__data__/output/guide.xml' + )}" --gzip --timeout=100` + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + + expect(content('tests/__data__/output/guide.xml')).toEqual( + content('tests/__data__/expected/epg_grab/template.guide.xml') + ) + + const zip = new Zip() + const expected = zip.decompress(fs.readFileSync('tests/__data__/output/guide.xml.gz')) + const result = zip.decompress( + fs.readFileSync('tests/__data__/expected/epg_grab/template.guide.xml.gz') + ) + expect(expected).toEqual(result) + }) +}) + +function content(filepath: string) { + return fs.readFileSync(pathToFileURL(filepath), { + encoding: 'utf8' + }) +} diff --git a/tests/commands/sites/init.test.ts b/tests/commands/sites/init.test.ts index caae889b..3149481b 100644 --- a/tests/commands/sites/init.test.ts +++ b/tests/commands/sites/init.test.ts @@ -1,41 +1,41 @@ -import { execSync } from 'child_process' -import fs from 'fs-extra' -import { pathToFileURL } from 'node:url' - -const ENV_VAR = 'cross-env SITES_DIR=tests/__data__/output/sites' - -beforeEach(() => { - fs.emptyDirSync('tests/__data__/output') - fs.mkdirSync('tests/__data__/output/sites') -}) - -it('can create new site config from template', () => { - const cmd = `${ENV_VAR} npm run sites:init --- example.com` - - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - - expect(exists('tests/__data__/output/sites/example.com')).toBe(true) - expect(exists('tests/__data__/output/sites/example.com/example.com.test.js')).toBe(true) - expect(exists('tests/__data__/output/sites/example.com/example.com.config.js')).toBe(true) - expect(exists('tests/__data__/output/sites/example.com/readme.md')).toBe(true) - expect(content('tests/__data__/output/sites/example.com/example.com.test.js')).toEqual( - content('tests/__data__/expected/sites_init/example.com.test.js') - ) - expect(content('tests/__data__/output/sites/example.com/example.com.config.js')).toEqual( - content('tests/__data__/expected/sites_init/example.com.config.js') - ) - expect(content('tests/__data__/output/sites/example.com/readme.md')).toEqual( - content('tests/__data__/expected/sites_init/readme.md') - ) -}) - -function content(filepath: string) { - return fs.readFileSync(pathToFileURL(filepath), { - encoding: 'utf8' - }) -} - -function exists(filepath: string) { - return fs.existsSync(pathToFileURL(filepath)) -} +import { execSync } from 'child_process' +import fs from 'fs-extra' +import { pathToFileURL } from 'node:url' + +const ENV_VAR = 'cross-env SITES_DIR=tests/__data__/output/sites' + +beforeEach(() => { + fs.emptyDirSync('tests/__data__/output') + fs.mkdirSync('tests/__data__/output/sites') +}) + +it('can create new site config from template', () => { + const cmd = `${ENV_VAR} npm run sites:init --- example.com` + + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + + expect(exists('tests/__data__/output/sites/example.com')).toBe(true) + expect(exists('tests/__data__/output/sites/example.com/example.com.test.js')).toBe(true) + expect(exists('tests/__data__/output/sites/example.com/example.com.config.js')).toBe(true) + expect(exists('tests/__data__/output/sites/example.com/readme.md')).toBe(true) + expect(content('tests/__data__/output/sites/example.com/example.com.test.js')).toEqual( + content('tests/__data__/expected/sites_init/example.com.test.js') + ) + expect(content('tests/__data__/output/sites/example.com/example.com.config.js')).toEqual( + content('tests/__data__/expected/sites_init/example.com.config.js') + ) + expect(content('tests/__data__/output/sites/example.com/readme.md')).toEqual( + content('tests/__data__/expected/sites_init/readme.md') + ) +}) + +function content(filepath: string) { + return fs.readFileSync(pathToFileURL(filepath), { + encoding: 'utf8' + }) +} + +function exists(filepath: string) { + return fs.existsSync(pathToFileURL(filepath)) +} diff --git a/tests/commands/sites/update.test.ts b/tests/commands/sites/update.test.ts index a11a0bad..2a553a20 100644 --- a/tests/commands/sites/update.test.ts +++ b/tests/commands/sites/update.test.ts @@ -1,28 +1,28 @@ -import { execSync } from 'child_process' -import fs from 'fs-extra' -import { pathToFileURL } from 'node:url' - -const ENV_VAR = 'cross-env SITES_DIR=tests/__data__/input/sites_update/sites ROOT_DIR=tests/__data__/output' - -beforeEach(() => { - fs.emptyDirSync('tests/__data__/output') -}) - -it('can update SITES.md', () => { - const cmd = `${ENV_VAR} npm run sites:update` - - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - - expect(content('tests/__data__/output/SITES.md')).toEqual( - content('tests/__data__/expected/sites_update/SITES.md') - ) -}) - -function content(filepath: string) { - const data = fs.readFileSync(pathToFileURL(filepath), { - encoding: 'utf8' - }) - - return JSON.stringify(data) -} +import { execSync } from 'child_process' +import fs from 'fs-extra' +import { pathToFileURL } from 'node:url' + +const ENV_VAR = 'cross-env SITES_DIR=tests/__data__/input/sites_update/sites ROOT_DIR=tests/__data__/output' + +beforeEach(() => { + fs.emptyDirSync('tests/__data__/output') +}) + +it('can update SITES.md', () => { + const cmd = `${ENV_VAR} npm run sites:update` + + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + + expect(content('tests/__data__/output/SITES.md')).toEqual( + content('tests/__data__/expected/sites_update/SITES.md') + ) +}) + +function content(filepath: string) { + const data = fs.readFileSync(pathToFileURL(filepath), { + encoding: 'utf8' + }) + + return JSON.stringify(data) +}