diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..f7cc30f0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Enforce the usage of CRLF in GitHub Actions per ESLint configuration. +* text eol=crlf \ No newline at end of file diff --git a/.gitignore b/.gitignore index 01962643..62054de4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,11 +5,4 @@ /guide.xml.gz # macOS -.DS_Store - -# If Yarn is used (yarn.lock) -/.yarn/* -.yarnrc -.yarnrc.yml - -.idea \ No newline at end of file +.DS_Store \ No newline at end of file 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/SITES.md b/SITES.md index 4fb9e1d9..29255343 100644 --- a/SITES.md +++ b/SITES.md @@ -1,242 +1,242 @@ -# Sites - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SiteChannels
(total / with xmltv-id)
StatusNotes
9tv.co.il11🟢
abc.net.au5480🟢
allente.dk7443🟢
allente.fi7124🟢
allente.no8452🟢
allente.se9291🟢
andorradifusio.ad11🟢
anteltv.com.uy5347🟢
antennaeurope.gr11🟢
antennapacific.gr11🟢
antennasatellite.gr11🟢
arianaafgtv.com11🟢
arianatelevision.com11🟢
arirang.com33🟢
artonline.tv55🟢
awilime.com1110🟢
bein.com160160🟢
beinsports.com10481🟢
berrymedia.co.kr55🟢
cableplus.com.uy17147🟢
canalplus.com11720212🟢
cgates.lt10261🟢
chada.ma11🟢
chaines-tv.orange.fr295146🟢
clickthecity.com3230🟢
content.astro.com.my157112🟢
cosmotetv.gr1080🟢
ctc.ru11🟢
cubmu.com174122🟢
cyta.com.cy1160🟢
dens.tv6764🟢
derana.lk11🟢
digea.gr920🟢
digiturk.com.tr108107🟢
directv.com1043696🔴https://github.com/iptv-org/epg/issues/2284
directv.com.ar412229🔴https://github.com/iptv-org/epg/issues/2339
directv.com.uy143142🟢
dishtv.in44889🟢
dna.fi1220🟢
dsmart.com.tr10490🟢
dstv.com6983181🟢
dtv8.net11🟢
elcinema.com262226🟢
ena.skylifetv.co.kr66🟢
energeek.cl62🟢
entertainment.ie10995🟢
epg.112114.xyz9301🟢
epg.iptvx.one2862747🟢
epg.telemach.ba2590🟢
epg.telemach.me2160🟢
epgmaster.com11🟢
epgshare01.online2097117🟢
firstmedia.com116101🟢
foxsports.com.au77🟢
foxtel.com.au9961🟢
freetv.tv77🟢
freeview.co.uk171100🟢
frikanalen.no11🟢
galamtv.kz2722🟢
gatotv.com475362🟢
getafteritmedia.com55🟢
gigatv.3bbtv.co.th7938🟢
guiadetv.com1240🟢
guida.tv8888🟢
guidatv.sky.it168153🟢
guidetnt.com6969🟢
horizon.tv184172🟢
hoy.tv31🟢
i.mjh.nz64581489🟢
i24news.tv43🟢
iltalehti.fi14244🟢
indihometv.com130124🟢
ionplustv.com11🟢
ipko.tv194152🟢
jiotv.com10940🟢
kan.org.il33🔴https://github.com/iptv-org/epg/issues/2273
knr.gl11🟢
kvf.fo11🟢
m.tv.sms.cz1027450🟢
m.tving.com3026🟢
magticom.ge240110🟢
mako.co.il11🟢
makrodigitaltelevision.com11🟢
maxtvgo.mk11048🟢
mediagenie.co.kr54🟢
mediaklikk.hu88🟢
mediasetinfinity.mediaset.it1313🟢
melita.com127111🟢
meo.pt216192🟢
meuguia.tv10297🟢
mewatch.sg2524🟢
mi.tv2084620🟢
mncvision.id276223🟢
moji.id11🟢
mojmaxtv.hrvatskitelekom.hr2430🟢
mon-programme-tv.be11195🟢
movistarplus.es1780🟢
mtel.ba5010🟢
mts.rs4570🟢
mujtvprogram.cz216202🟢
musor.tv181145🟢
mysky.com.ph11543🟢
mytelly.co.uk488401🟢
mytvsuper.com10899🟢
neo.io337241🟢
nhkworldpremium.com22🟢
nhl.com11🟢
nostv.pt168155🟢
novacyprus.com2924🟢
novasports.gr1616🟢
nowplayer.now.com288229🟢
nuevosiglo.com.uy17347🟢
nzxmltv.com532118🟢
ontvtonight.com5177532🟢
opto.sic.pt44🟢
orangetv.orange.es168165🟢
osn.com11898🟢
pbsguam.org11🟢
pickx.be404391🟢
player.ee.co.uk241206🟢
playtv.unifi.com.my6661🟢
plex.tv170119🟢
pluto.tv33020🟢
programacion-tv.elpais.com195104🟢
programacion.tcc.com.uy14956🟢
programetv.ro331224🟢
programme-tv.net295197🟢
programme-tv.vini.pf582🟢
programme.tvb.com86🟢
programtv.onet.pl590362🟢
raiplay.it1713🟢
reportv.com.ar16397🟢
rikstv.no800🟢
rotana.net3228🟢
rtb.gov.bn33🔴https://github.com/iptv-org/epg/issues/2257
rthk.hk88🟢
rtmklik.rtm.gov.my86🟢
rtp.pt1010🟢
ruv.is22🟢
s.mxtv.jp22🟢
sat.tv30308249🟢
shahid.mbc.net231165🟢
siba.com.co9896🟢
singtel.com155113🟢
sjonvarp.is1313🟢
sky.co.nz11193🟢
sky.com559458🟡https://github.com/iptv-org/epg/issues/2763
sky.de7575🟢
skylife.co.kr2510🟢
skyperfectv.co.jp137130🟢
snrt.ma117🟢
sporttv.pt98🟢
starhubtvplus.com232208🟢
startimestv.com7758🟢
stod2.is128🟢
streamingtvguides.com30661🟢
superguidatv.it204163🟢
taiwanplus.com11🟢
tapdmv.com397🟢
tataplay.com785401🟢
telebilbao.es11🟢
teleboy.ch3250🟢
telenet.tv26091🟢
teliatv.ee342233🟢
telkussa.fi6632🟢
telsu.fi1715🟢
thesportplus.com30🟢
tivie.id4544🟢
tivu.tv6966🟢
toonamiaftermath.com11🟢
turksatkablo.com.tr177118🟢
tv-programme.telecablesat.fr268250🟢
tv-spored.siol.net3120🟢
tv.blue.ch1030565🟢
tv.cctv.com9488🟢
tv.dir.bg11193🔴https://github.com/iptv-org/epg/issues/2779
tv.lv13749🟢
tv.magenta.at307228🟢
tv.mail.ru664643🟢
tv.movistar.com.pe28240🟢
tv.nu199180🟢
tv.post.lu332242🟢
tv.sfr.fr489456🟢
tv.trueid.net26674🟢
tv.yandex.ru9767🔴https://github.com/iptv-org/epg/issues/2803
tv24.co.uk107239🟢
tv24.se326157🟢
tv2go.t-2.net335254🟢
tvarenasport.com1412🟢
tvarenasport.hr1010🟢
tvcesoir.fr135133🟢
tvcubana.icrt.cu1010🟢
tvgids.nl11590🟢
tvguide.com153149🟡https://github.com/iptv-org/epg/issues/2644
tvguide.myjcom.jp145140🟢
tvhebdo.com317215🟢
tvheute.at5353🟢
tvi.iol.pt66🟢
tvim.tv2519🟢
tvinsider.com3740🟢
tvireland.ie334304🟢
tvkaista.org1490🟢
tvmi.mt33🟢
tvmusor.hu9967🟢
tvmustra.hu1880🟢
tvpassport.com192872509🟢
tvplus.com.tr143134🟢https://github.com/iptv-org/epg/issues/2816
tvprofil.com5836455🟢
tvtv.us22992255🟢
v3.myafn.dodmedia.osd.mil88🟢
vidio.com5752🟢
virginmediatelevision.ie55🟢
virgintvgo.virginmedia.com238195🟢
visionplus.id250226🟢
vivoplay.com.br3890🟢
vtm.be76🟢
walesi.com.fj98🟢
watch.sportsnet.ca88🟢
watchyour.tv4024🟢
wavve.com7776🟢
web.magentatv.de348247🟢
webtv.delta.nl247218🟢
winplay.co22🟢
worldfishingnetwork.com11🟢
www3.nhk.or.jp11🟢
xem.kplus.vn770🟢
xumo.tv35033🟢
yes.co.il1740🟢
zap.co.ao11464🟢
zap2it.com5950🟢
ziggogo.tv152130🟢
znbc.co.zm44🟢
zuragt.mn3625🟢
+# Sites + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SiteChannels
(total / with xmltv-id)
StatusNotes
9tv.co.il11🟢
abc.net.au5480🟢
allente.dk7443🟢
allente.fi7124🟢
allente.no8452🟢
allente.se9291🟢
andorradifusio.ad11🟢
anteltv.com.uy5347🟢
antennaeurope.gr11🟢
antennapacific.gr11🟢
antennasatellite.gr11🟢
arianaafgtv.com11🟢
arianatelevision.com11🟢
arirang.com33🟢
artonline.tv55🟢
awilime.com1110🟢
bein.com160160🟢
beinsports.com10481🟢
berrymedia.co.kr55🟢
cableplus.com.uy17147🟢
canalplus.com11720212🟢
cgates.lt10261🟢
chada.ma11🟢
chaines-tv.orange.fr295146🟢
clickthecity.com3230🟢
content.astro.com.my157112🟢
cosmotetv.gr1080🟢
ctc.ru11🟢
cubmu.com174122🟢
cyta.com.cy1160🟢
dens.tv6764🟢
derana.lk11🟢
digea.gr920🟢
digiturk.com.tr108107🟢
directv.com1043696🔴https://github.com/iptv-org/epg/issues/2284
directv.com.ar412229🔴https://github.com/iptv-org/epg/issues/2339
directv.com.uy143142🟢
dishtv.in44889🟢
dna.fi1220🟢
dsmart.com.tr10490🟢
dstv.com6983181🟢
dtv8.net11🟢
elcinema.com262226🟢
ena.skylifetv.co.kr66🟢
energeek.cl62🟢
entertainment.ie10995🟢
epg.112114.xyz9301🟢
epg.iptvx.one2862747🟢
epg.telemach.ba2590🟢
epg.telemach.me2160🟢
epgmaster.com11🟢
epgshare01.online2097117🟢
firstmedia.com116101🟢
foxsports.com.au77🟢
foxtel.com.au9961🟢
freetv.tv77🟢
freeview.co.uk171100🟢
frikanalen.no11🟢
galamtv.kz2722🟢
gatotv.com475362🟢
getafteritmedia.com55🟢
gigatv.3bbtv.co.th7938🟢
guiadetv.com1240🟢
guida.tv8888🟢
guidatv.sky.it168153🟢
guidetnt.com6969🟢
horizon.tv184172🟢
hoy.tv31🟢
i.mjh.nz64581489🟢
i24news.tv43🟢
iltalehti.fi14244🟢
indihometv.com130124🟢
ionplustv.com11🟢
ipko.tv194152🟢
jiotv.com10940🟢
kan.org.il33🔴https://github.com/iptv-org/epg/issues/2273
knr.gl11🟢
kvf.fo11🟢
m.tv.sms.cz1027450🟢
m.tving.com3026🟢
magticom.ge240110🟢
mako.co.il11🟢
makrodigitaltelevision.com11🟢
maxtvgo.mk11048🟢
mediagenie.co.kr54🟢
mediaklikk.hu88🟢
mediasetinfinity.mediaset.it1313🟢
melita.com127111🟢
meo.pt216192🟢
meuguia.tv10297🟢
mewatch.sg2524🟢
mi.tv2084620🟢
mncvision.id276223🟢
moji.id11🟢
mojmaxtv.hrvatskitelekom.hr2430🟢
mon-programme-tv.be11195🟢
movistarplus.es1780🟢
mtel.ba5010🟢
mts.rs4570🟢
mujtvprogram.cz216202🟢
musor.tv181145🟢
mysky.com.ph11543🟢
mytelly.co.uk488401🟢
mytvsuper.com10899🟢
neo.io337241🟢
nhkworldpremium.com22🟢
nhl.com11🟢
nostv.pt168155🟢
novacyprus.com2924🟢
novasports.gr1616🟢
nowplayer.now.com288229🟢
nuevosiglo.com.uy17347🟢
nzxmltv.com532118🟢
ontvtonight.com5177532🟢
opto.sic.pt44🟢
orangetv.orange.es168165🟢
osn.com11898🟢
pbsguam.org11🟢
pickx.be404391🟢
player.ee.co.uk241206🟢
playtv.unifi.com.my6661🟢
plex.tv170119🟢
pluto.tv33020🟢
programacion-tv.elpais.com195104🟢
programacion.tcc.com.uy14956🟢
programetv.ro331224🟢
programme-tv.net295197🟢
programme-tv.vini.pf582🟢
programme.tvb.com86🟢
programtv.onet.pl590362🟢
raiplay.it1713🟢
reportv.com.ar16397🟢
rikstv.no800🟢
rotana.net3228🟢
rtb.gov.bn33🔴https://github.com/iptv-org/epg/issues/2257
rthk.hk88🟢
rtmklik.rtm.gov.my86🟢
rtp.pt1010🟢
ruv.is22🟢
s.mxtv.jp22🟢
sat.tv30308249🟢
shahid.mbc.net231165🟢
siba.com.co9896🟢
singtel.com155113🟢
sjonvarp.is1313🟢
sky.co.nz11193🟢
sky.com559458🟡https://github.com/iptv-org/epg/issues/2763
sky.de7575🟢
skylife.co.kr2510🟢
skyperfectv.co.jp137130🟢
snrt.ma117🟢
sporttv.pt98🟢
starhubtvplus.com232208🟢
startimestv.com7758🟢
stod2.is128🟢
streamingtvguides.com30661🟢
superguidatv.it204163🟢
taiwanplus.com11🟢
tapdmv.com397🟢
tataplay.com785401🟢
telebilbao.es11🟢
teleboy.ch3250🟢
telenet.tv26091🟢
teliatv.ee342233🟢
telkussa.fi6632🟢
telsu.fi1715🟢
thesportplus.com30🟢
tivie.id4544🟢
tivu.tv6966🟢
toonamiaftermath.com11🟢
turksatkablo.com.tr177118🟢
tv-programme.telecablesat.fr268250🟢
tv-spored.siol.net3120🟢
tv.blue.ch1030565🟢
tv.cctv.com9488🟢
tv.dir.bg11193🔴https://github.com/iptv-org/epg/issues/2779
tv.lv13749🟢
tv.magenta.at307228🟢
tv.mail.ru664643🟢
tv.movistar.com.pe28240🟢
tv.nu199180🟢
tv.post.lu332242🟢
tv.sfr.fr489456🟢
tv.trueid.net26674🟢
tv.yandex.ru9767🔴https://github.com/iptv-org/epg/issues/2803
tv24.co.uk107239🟢
tv24.se326157🟢
tv2go.t-2.net335254🟢
tvarenasport.com1412🟢
tvarenasport.hr1010🟢
tvcesoir.fr135133🟢
tvcubana.icrt.cu1010🟢
tvgids.nl11590🟢
tvguide.com153149🟡https://github.com/iptv-org/epg/issues/2644
tvguide.myjcom.jp145140🟢
tvhebdo.com317215🟢
tvheute.at5353🟢
tvi.iol.pt66🟢
tvim.tv2519🟢
tvinsider.com3740🟢
tvireland.ie334304🟢
tvkaista.org1490🟢
tvmi.mt33🟢
tvmusor.hu9967🟢
tvmustra.hu1880🟢
tvpassport.com192872509🟢
tvplus.com.tr150144🟢
tvprofil.com5836455🟢
tvtv.us22992255🟢
v3.myafn.dodmedia.osd.mil88🟢
vidio.com5752🟢
virginmediatelevision.ie55🟢
virgintvgo.virginmedia.com238195🟢
visionplus.id250226🟢
vivoplay.com.br3890🟢
vtm.be76🟢
walesi.com.fj98🟢
watch.sportsnet.ca88🟢
watchyour.tv4024🟢
wavve.com7776🟢
web.magentatv.de348247🟢
webtv.delta.nl247218🟢
winplay.co22🟢
worldfishingnetwork.com11🟢
www3.nhk.or.jp11🟢
xem.kplus.vn770🟢
xumo.tv35033🟢
yes.co.il1740🟢
zap.co.ao11464🟢
zap2it.com5950🟢
ziggogo.tv152130🟢
znbc.co.zm44🟢
zuragt.mn3625🟢
diff --git a/eslint.config.mjs b/eslint.config.mjs index 57f9e877..7f8ffde6 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,55 +1,57 @@ -import typescriptEslint from '@typescript-eslint/eslint-plugin' -import globals from 'globals' -import tsParser from '@typescript-eslint/parser' -import path from 'node:path' -import { fileURLToPath } from 'node:url' -import js from '@eslint/js' -import { FlatCompat } from '@eslint/eslintrc' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) -const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, - allConfig: js.configs.all -}) - -export default [ - ...compat.extends('eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'), - { - plugins: { - '@typescript-eslint': typescriptEslint - }, - - languageOptions: { - globals: { - ...globals.node, - ...globals.jest - }, - - parser: tsParser, - ecmaVersion: 'latest', - sourceType: 'module' - }, - - rules: { - '@typescript-eslint/no-require-imports': 'off', - '@typescript-eslint/no-var-requires': 'off', - 'no-case-declarations': 'off', - 'linebreak-style': ['error', 'windows'], - - quotes: [ - 'error', - 'single', - { - avoidEscape: true - } - ], - - semi: ['error', 'never'] - } - }, - { - ignores: ['tests/__data__/'] - } -] +import typescriptEslint from '@typescript-eslint/eslint-plugin' +import stylistic from '@stylistic/eslint-plugin' +import globals from 'globals' +import tsParser from '@typescript-eslint/parser' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import js from '@eslint/js' +import { FlatCompat } from '@eslint/eslintrc' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}) + +export default [ + ...compat.extends('eslint:recommended', 'plugin:@typescript-eslint/strict', 'plugin:@typescript-eslint/stylistic', 'prettier'), + { + plugins: { + '@typescript-eslint': typescriptEslint, + '@stylistic': stylistic + }, + + languageOptions: { + globals: { + ...globals.node, + ...globals.jest + }, + + parser: tsParser, + ecmaVersion: 'latest', + sourceType: 'module' + }, + + rules: { + '@typescript-eslint/no-require-imports': 'off', + '@typescript-eslint/no-var-requires': 'off', + 'no-case-declarations': 'off', + '@stylistic/linebreak-style': ['error', 'windows'], + + quotes: [ + 'error', + 'single', + { + avoidEscape: true + } + ], + + semi: ['error', 'never'] + } + }, + { + ignores: ['tests/__data__/'] + } +] diff --git a/package-lock.json b/package-lock.json index fa3de2ec..94050c79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@alex_neo/jest-expect-message": "^1.0.5", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.30.0", + "@eslint/js": "^9.32.0", "@freearhey/chronos": "^0.0.1", "@freearhey/core": "^0.10.2", "@freearhey/search-js": "^0.1.2", @@ -18,33 +18,37 @@ "@octokit/core": "^7.0.3", "@octokit/plugin-paginate-rest": "^13.1.1", "@octokit/plugin-rest-endpoint-methods": "^16.0.0", - "@swc/core": "^1.13.0", + "@stylistic/eslint-plugin": "^5.2.2", + "@swc/core": "^1.13.2", "@swc/jest": "^0.2.39", "@types/cli-progress": "^3.11.6", "@types/fs-extra": "^11.0.4", "@types/inquirer": "^9.0.8", "@types/jest": "^30.0.0", "@types/langs": "^2.0.5", - "@types/lodash": "^4.17.19", - "@types/node": "^24.0.14", + "@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", - "@typescript-eslint/eslint-plugin": "^8.35.0", - "@typescript-eslint/parser": "^8.35.0", - "axios": "^1.10.0", + "@typescript-eslint/eslint-plugin": "^8.38.0", + "@typescript-eslint/parser": "^8.38.0", + "axios": "^1.11.0", "axios-cookiejar-support": "^6.0.4", "chalk": "^5.4.1", - "cheerio": "^1.1.0", + "cheerio": "^1.1.2", "cli-progress": "^3.12.0", "commander": "^14.0.0", "consola": "^3.4.2", - "cross-env": "^7.0.3", + "cross-env": "^10.0.0", "csv-parser": "^3.2.0", "cwait": "^1.1.2", "dayjs": "^1.11.13", "epg-grabber": "^0.41.0", "epg-parser": "^0.3.1", - "eslint": "^9.30.0", + "eslint": "^9.32.0", "eslint-config-prettier": "^10.1.8", "form-data": "^4.0.4", "fs-extra": "^11.3.0", @@ -52,12 +56,15 @@ "globals": "^16.3.0", "husky": "^9.1.7", "iconv-lite": "^0.6.3", - "inquirer": "^12.7.0", - "jest": "^30.0.3", + "inquirer": "^12.8.2", + "jest": "^30.0.5", "jest-offline": "^1.0.1", "langs": "^2.0.0", "libxml2-wasm": "^0.5.0", - "lodash": "^4.17.21", + "lodash.orderby": "^4.6.0", + "lodash.sortby": "^4.7.0", + "lodash.startcase": "^4.4.0", + "lodash.uniqby": "^4.7.0", "luxon": "^3.7.1", "mockdate": "^3.0.5", "nedb-promises": "^6.2.3", @@ -82,6 +89,7 @@ "tsx": "^4.20.3", "typescript": "^5.8.3", "unzipit": "^1.4.3", + "uuid": "^11.1.0", "wildcard-match": "^5.1.4" } }, @@ -277,12 +285,12 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", - "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", + "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.27.6" + "@babel/types": "^7.28.2" }, "engines": { "node": ">=6.9.0" @@ -538,9 +546,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", - "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" @@ -573,33 +581,38 @@ } }, "node_modules/@emnapi/core": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.4.tgz", - "integrity": "sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", + "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.0.3", + "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.4.tgz", - "integrity": "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", + "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.3.tgz", - "integrity": "sha512-8K5IFFsQqF9wQNJptGbS6FNKgUTsSRYnTqNCG1vPP8jFdjSv18n2mQfJpkt2Oibo9iBEzcDnDxNwKTzC7svlJw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", + "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", @@ -1084,9 +1097,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", - "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", + "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1214,13 +1227,13 @@ } }, "node_modules/@inquirer/checkbox": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.9.tgz", - "integrity": "sha512-DBJBkzI5Wx4jFaYm221LHvAhpKYkhVS0k9plqHwaHhofGNxvYB7J3Bz8w+bFJ05zaMb0sZNHo4KdmENQFlNTuQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.2.0.tgz", + "integrity": "sha512-fdSw07FLJEU5vbpOPzXo5c6xmMGDzbZE2+niuDHX5N6mc6V0Ebso/q3xiHra4D73+PMsC8MJmcaZKuAAoaQsSA==", "dependencies": { - "@inquirer/core": "^10.1.14", - "@inquirer/figures": "^1.0.12", - "@inquirer/type": "^3.0.7", + "@inquirer/core": "^10.1.15", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, @@ -1237,12 +1250,12 @@ } }, "node_modules/@inquirer/confirm": { - "version": "5.1.13", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.13.tgz", - "integrity": "sha512-EkCtvp67ICIVVzjsquUiVSd+V5HRGOGQfsqA4E4vMWhYnB7InUL0pa0TIWt1i+OfP16Gkds8CdIu6yGZwOM1Yw==", + "version": "5.1.14", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.14.tgz", + "integrity": "sha512-5yR4IBfe0kXe59r1YCTG8WXkUbl7Z35HK87Sw+WUyGD8wNUx7JvY7laahzeytyE1oLn74bQnL7hstctQxisQ8Q==", "dependencies": { - "@inquirer/core": "^10.1.14", - "@inquirer/type": "^3.0.7" + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8" }, "engines": { "node": ">=18" @@ -1257,12 +1270,12 @@ } }, "node_modules/@inquirer/core": { - "version": "10.1.14", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.14.tgz", - "integrity": "sha512-Ma+ZpOJPewtIYl6HZHZckeX1STvDnHTCB2GVINNUlSEn2Am6LddWwfPkIGY0IUFVjUUrr/93XlBwTK6mfLjf0A==", + "version": "10.1.15", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.15.tgz", + "integrity": "sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA==", "dependencies": { - "@inquirer/figures": "^1.0.12", - "@inquirer/type": "^3.0.7", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", @@ -1302,12 +1315,12 @@ } }, "node_modules/@inquirer/editor": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.14.tgz", - "integrity": "sha512-yd2qtLl4QIIax9DTMZ1ZN2pFrrj+yL3kgIWxm34SS6uwCr0sIhsNyudUjAo5q3TqI03xx4SEBkUJqZuAInp9uA==", + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.15.tgz", + "integrity": "sha512-wst31XT8DnGOSS4nNJDIklGKnf+8shuauVrWzgKegWUe28zfCftcWZ2vktGdzJgcylWSS2SrDnYUb6alZcwnCQ==", "dependencies": { - "@inquirer/core": "^10.1.14", - "@inquirer/type": "^3.0.7", + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", "external-editor": "^3.1.0" }, "engines": { @@ -1323,12 +1336,12 @@ } }, "node_modules/@inquirer/expand": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.16.tgz", - "integrity": "sha512-oiDqafWzMtofeJyyGkb1CTPaxUkjIcSxePHHQCfif8t3HV9pHcw1Kgdw3/uGpDvaFfeTluwQtWiqzPVjAqS3zA==", + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.17.tgz", + "integrity": "sha512-PSqy9VmJx/VbE3CT453yOfNa+PykpKg/0SYP7odez1/NWBGuDXgPhp4AeGYYKjhLn5lUUavVS/JbeYMPdH50Mw==", "dependencies": { - "@inquirer/core": "^10.1.14", - "@inquirer/type": "^3.0.7", + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -1344,20 +1357,20 @@ } }, "node_modules/@inquirer/figures": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.12.tgz", - "integrity": "sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", + "integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==", "engines": { "node": ">=18" } }, "node_modules/@inquirer/input": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.2.0.tgz", - "integrity": "sha512-opqpHPB1NjAmDISi3uvZOTrjEEU5CWVu/HBkDby8t93+6UxYX0Z7Ps0Ltjm5sZiEbWenjubwUkivAEYQmy9xHw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.2.1.tgz", + "integrity": "sha512-tVC+O1rBl0lJpoUZv4xY+WGWY8V5b0zxU1XDsMsIHYregdh7bN5X5QnIONNBAl0K765FYlAfNHS2Bhn7SSOVow==", "dependencies": { - "@inquirer/core": "^10.1.14", - "@inquirer/type": "^3.0.7" + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8" }, "engines": { "node": ">=18" @@ -1372,12 +1385,12 @@ } }, "node_modules/@inquirer/number": { - "version": "3.0.16", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.16.tgz", - "integrity": "sha512-kMrXAaKGavBEoBYUCgualbwA9jWUx2TjMA46ek+pEKy38+LFpL9QHlTd8PO2kWPUgI/KB+qi02o4y2rwXbzr3Q==", + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.17.tgz", + "integrity": "sha512-GcvGHkyIgfZgVnnimURdOueMk0CztycfC8NZTiIY9arIAkeOgt6zG57G+7vC59Jns3UX27LMkPKnKWAOF5xEYg==", "dependencies": { - "@inquirer/core": "^10.1.14", - "@inquirer/type": "^3.0.7" + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8" }, "engines": { "node": ">=18" @@ -1392,12 +1405,12 @@ } }, "node_modules/@inquirer/password": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.16.tgz", - "integrity": "sha512-g8BVNBj5Zeb5/Y3cSN+hDUL7CsIFDIuVxb9EPty3lkxBaYpjL5BNRKSYOF9yOLe+JOcKFd+TSVeADQ4iSY7rbg==", + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.17.tgz", + "integrity": "sha512-DJolTnNeZ00E1+1TW+8614F7rOJJCM4y4BAGQ3Gq6kQIG+OJ4zr3GLjIjVVJCbKsk2jmkmv6v2kQuN/vriHdZA==", "dependencies": { - "@inquirer/core": "^10.1.14", - "@inquirer/type": "^3.0.7", + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2" }, "engines": { @@ -1413,20 +1426,20 @@ } }, "node_modules/@inquirer/prompts": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.6.0.tgz", - "integrity": "sha512-jAhL7tyMxB3Gfwn4HIJ0yuJ5pvcB5maYUcouGcgd/ub79f9MqZ+aVnBtuFf+VC2GTkCBF+R+eo7Vi63w5VZlzw==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.8.0.tgz", + "integrity": "sha512-JHwGbQ6wjf1dxxnalDYpZwZxUEosT+6CPGD9Zh4sm9WXdtUp9XODCQD3NjSTmu+0OAyxWXNOqf0spjIymJa2Tw==", "dependencies": { - "@inquirer/checkbox": "^4.1.9", - "@inquirer/confirm": "^5.1.13", - "@inquirer/editor": "^4.2.14", - "@inquirer/expand": "^4.0.16", - "@inquirer/input": "^4.2.0", - "@inquirer/number": "^3.0.16", - "@inquirer/password": "^4.0.16", - "@inquirer/rawlist": "^4.1.4", - "@inquirer/search": "^3.0.16", - "@inquirer/select": "^4.2.4" + "@inquirer/checkbox": "^4.2.0", + "@inquirer/confirm": "^5.1.14", + "@inquirer/editor": "^4.2.15", + "@inquirer/expand": "^4.0.17", + "@inquirer/input": "^4.2.1", + "@inquirer/number": "^3.0.17", + "@inquirer/password": "^4.0.17", + "@inquirer/rawlist": "^4.1.5", + "@inquirer/search": "^3.1.0", + "@inquirer/select": "^4.3.1" }, "engines": { "node": ">=18" @@ -1441,12 +1454,12 @@ } }, "node_modules/@inquirer/rawlist": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.4.tgz", - "integrity": "sha512-5GGvxVpXXMmfZNtvWw4IsHpR7RzqAR624xtkPd1NxxlV5M+pShMqzL4oRddRkg8rVEOK9fKdJp1jjVML2Lr7TQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.5.tgz", + "integrity": "sha512-R5qMyGJqtDdi4Ht521iAkNqyB6p2UPuZUbMifakg1sWtu24gc2Z8CJuw8rP081OckNDMgtDCuLe42Q2Kr3BolA==", "dependencies": { - "@inquirer/core": "^10.1.14", - "@inquirer/type": "^3.0.7", + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -1462,13 +1475,13 @@ } }, "node_modules/@inquirer/search": { - "version": "3.0.16", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.16.tgz", - "integrity": "sha512-POCmXo+j97kTGU6aeRjsPyuCpQQfKcMXdeTMw708ZMtWrj5aykZvlUxH4Qgz3+Y1L/cAVZsSpA+UgZCu2GMOMg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.1.0.tgz", + "integrity": "sha512-PMk1+O/WBcYJDq2H7foV0aAZSmDdkzZB9Mw2v/DmONRJopwA/128cS9M/TXWLKKdEQKZnKwBzqu2G4x/2Nqx8Q==", "dependencies": { - "@inquirer/core": "^10.1.14", - "@inquirer/figures": "^1.0.12", - "@inquirer/type": "^3.0.7", + "@inquirer/core": "^10.1.15", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -1484,13 +1497,13 @@ } }, "node_modules/@inquirer/select": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.2.4.tgz", - "integrity": "sha512-unTppUcTjmnbl/q+h8XeQDhAqIOmwWYWNyiiP2e3orXrg6tOaa5DHXja9PChCSbChOsktyKgOieRZFnajzxoBg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.3.1.tgz", + "integrity": "sha512-Gfl/5sqOF5vS/LIrSndFgOh7jgoe0UXEizDqahFRkq5aJBLegZ6WjuMh/hVEJwlFQjyLq1z9fRtvUMkb7jM1LA==", "dependencies": { - "@inquirer/core": "^10.1.14", - "@inquirer/figures": "^1.0.12", - "@inquirer/type": "^3.0.7", + "@inquirer/core": "^10.1.15", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, @@ -1507,9 +1520,9 @@ } }, "node_modules/@inquirer/type": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.7.tgz", - "integrity": "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.8.tgz", + "integrity": "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==", "engines": { "node": ">=18" }, @@ -1663,21 +1676,49 @@ } }, "node_modules/@jest/console": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.0.4.tgz", - "integrity": "sha512-tMLCDvBJBwPqMm4OAiuKm2uF5y5Qe26KgcMn+nrDSWpEW+eeFmqA0iO4zJfL16GP7gE3bUUQ3hIuUJ22AqVRnw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.0.5.tgz", + "integrity": "sha512-xY6b0XiL0Nav3ReresUarwl2oIz1gTnxGbGpho9/rbUWsLH0f1OD/VT84xs8c7VmH7MChnLb0pag6PhZhAdDiA==", "dependencies": { - "@jest/types": "30.0.1", + "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", - "jest-message-util": "30.0.2", - "jest-util": "30.0.2", + "jest-message-util": "30.0.5", + "jest-util": "30.0.5", "slash": "^3.0.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/@jest/console/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/console/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/console/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1694,37 +1735,37 @@ } }, "node_modules/@jest/core": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.0.4.tgz", - "integrity": "sha512-MWScSO9GuU5/HoWjpXAOBs6F/iobvK1XlioelgOM9St7S0Z5WTI9kjCQLPeo4eQRRYusyLW25/J7J5lbFkrYXw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.0.5.tgz", + "integrity": "sha512-fKD0OulvRsXF1hmaFgHhVJzczWzA1RXMMo9LTPuFXo9q/alDbME3JIyWYqovWsUBWSoBcsHaGPSLF9rz4l9Qeg==", "dependencies": { - "@jest/console": "30.0.4", + "@jest/console": "30.0.5", "@jest/pattern": "30.0.1", - "@jest/reporters": "30.0.4", - "@jest/test-result": "30.0.4", - "@jest/transform": "30.0.4", - "@jest/types": "30.0.1", + "@jest/reporters": "30.0.5", + "@jest/test-result": "30.0.5", + "@jest/transform": "30.0.5", + "@jest/types": "30.0.5", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "ci-info": "^4.2.0", "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", - "jest-changed-files": "30.0.2", - "jest-config": "30.0.4", - "jest-haste-map": "30.0.2", - "jest-message-util": "30.0.2", + "jest-changed-files": "30.0.5", + "jest-config": "30.0.5", + "jest-haste-map": "30.0.5", + "jest-message-util": "30.0.5", "jest-regex-util": "30.0.1", - "jest-resolve": "30.0.2", - "jest-resolve-dependencies": "30.0.4", - "jest-runner": "30.0.4", - "jest-runtime": "30.0.4", - "jest-snapshot": "30.0.4", - "jest-util": "30.0.2", - "jest-validate": "30.0.2", - "jest-watcher": "30.0.4", + "jest-resolve": "30.0.5", + "jest-resolve-dependencies": "30.0.5", + "jest-runner": "30.0.5", + "jest-runtime": "30.0.5", + "jest-snapshot": "30.0.5", + "jest-util": "30.0.5", + "jest-validate": "30.0.5", + "jest-watcher": "30.0.5", "micromatch": "^4.0.8", - "pretty-format": "30.0.2", + "pretty-format": "30.0.5", "slash": "^3.0.0" }, "engines": { @@ -1739,6 +1780,34 @@ } } }, + "node_modules/@jest/core/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/core/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1774,35 +1843,78 @@ } }, "node_modules/@jest/environment": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.4.tgz", - "integrity": "sha512-5NT+sr7ZOb8wW7C4r7wOKnRQ8zmRWQT2gW4j73IXAKp5/PX1Z8MCStBLQDYfIG3n1Sw0NRfYGdp0iIPVooBAFQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.5.tgz", + "integrity": "sha512-aRX7WoaWx1oaOkDQvCWImVQ8XNtdv5sEWgk4gxR6NXb7WBUnL5sRak4WRzIQRZ1VTWPvV4VI4mgGjNL9TeKMYA==", "dependencies": { - "@jest/fake-timers": "30.0.4", - "@jest/types": "30.0.1", + "@jest/fake-timers": "30.0.5", + "@jest/types": "30.0.5", "@types/node": "*", - "jest-mock": "30.0.2" + "jest-mock": "30.0.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/expect": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.0.4.tgz", - "integrity": "sha512-Z/DL7t67LBHSX4UzDyeYKqOxE/n7lbrrgEwWM3dGiH5Dgn35nk+YtgzKudmfIrBI8DRRrKYY5BCo3317HZV1Fw==", + "node_modules/@jest/environment/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "dependencies": { - "expect": "30.0.4", - "jest-snapshot": "30.0.4" + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/expect": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.0.5.tgz", + "integrity": "sha512-6udac8KKrtTtC+AXZ2iUN/R7dp7Ydry+Fo6FPFnDG54wjVMnb6vW/XNlf7Xj8UDjAE3aAVAsR4KFyKk3TCXmTA==", + "dependencies": { + "expect": "30.0.5", + "jest-snapshot": "30.0.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.4.tgz", - "integrity": "sha512-EgXecHDNfANeqOkcak0DxsoVI4qkDUsR7n/Lr2vtmTBjwLPBnnPOF71S11Q8IObWzxm2QgQoY6f9hzrRD3gHRA==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.5.tgz", + "integrity": "sha512-F3lmTT7CXWYywoVUGTCmom0vXq3HTTkaZyTAzIy+bXSBizB7o5qzlC9VCtq0arOa8GqmNsbg/cE9C6HLn7Szew==", "dependencies": { "@jest/get-type": "30.0.1" }, @@ -1811,21 +1923,64 @@ } }, "node_modules/@jest/fake-timers": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.4.tgz", - "integrity": "sha512-qZ7nxOcL5+gwBO6LErvwVy5k06VsX/deqo2XnVUSTV0TNC9lrg8FC3dARbi+5lmrr5VyX5drragK+xLcOjvjYw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.5.tgz", + "integrity": "sha512-ZO5DHfNV+kgEAeP3gK3XlpJLL4U3Sz6ebl/n68Uwt64qFFs5bv4bfEEjyRGK5uM0C90ewooNgFuKMdkbEoMEXw==", "dependencies": { - "@jest/types": "30.0.1", + "@jest/types": "30.0.5", "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", - "jest-message-util": "30.0.2", - "jest-mock": "30.0.2", - "jest-util": "30.0.2" + "jest-message-util": "30.0.5", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/@jest/fake-timers/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/@jest/get-type": { "version": "30.0.1", "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz", @@ -1836,19 +1991,62 @@ } }, "node_modules/@jest/globals": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.0.4.tgz", - "integrity": "sha512-avyZuxEHF2EUhFF6NEWVdxkRRV6iXXcIES66DLhuLlU7lXhtFG/ySq/a8SRZmEJSsLkNAFX6z6mm8KWyXe9OEA==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.0.5.tgz", + "integrity": "sha512-7oEJT19WW4oe6HR7oLRvHxwlJk2gev0U9px3ufs8sX9PoD1Eza68KF0/tlN7X0dq/WVsBScXQGgCldA1V9Y/jA==", "dependencies": { - "@jest/environment": "30.0.4", - "@jest/expect": "30.0.4", - "@jest/types": "30.0.1", - "jest-mock": "30.0.2" + "@jest/environment": "30.0.5", + "@jest/expect": "30.0.5", + "@jest/types": "30.0.5", + "jest-mock": "30.0.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/@jest/globals/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/@jest/pattern": { "version": "30.0.1", "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", @@ -1863,15 +2061,15 @@ } }, "node_modules/@jest/reporters": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.0.4.tgz", - "integrity": "sha512-6ycNmP0JSJEEys1FbIzHtjl9BP0tOZ/KN6iMeAKrdvGmUsa1qfRdlQRUDKJ4P84hJ3xHw1yTqJt4fvPNHhyE+g==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.0.5.tgz", + "integrity": "sha512-mafft7VBX4jzED1FwGC1o/9QUM2xebzavImZMeqnsklgcyxBto8mV4HzNSzUrryJ+8R9MFOM3HgYuDradWR+4g==", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.0.4", - "@jest/test-result": "30.0.4", - "@jest/transform": "30.0.4", - "@jest/types": "30.0.1", + "@jest/console": "30.0.5", + "@jest/test-result": "30.0.5", + "@jest/transform": "30.0.5", + "@jest/types": "30.0.5", "@jridgewell/trace-mapping": "^0.3.25", "@types/node": "*", "chalk": "^4.1.2", @@ -1884,9 +2082,9 @@ "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^5.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "30.0.2", - "jest-util": "30.0.2", - "jest-worker": "30.0.2", + "jest-message-util": "30.0.5", + "jest-util": "30.0.5", + "jest-worker": "30.0.5", "slash": "^3.0.0", "string-length": "^4.0.2", "v8-to-istanbul": "^9.0.1" @@ -1903,6 +2101,34 @@ } } }, + "node_modules/@jest/reporters/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/reporters/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -2005,11 +2231,11 @@ } }, "node_modules/@jest/snapshot-utils": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.0.4.tgz", - "integrity": "sha512-BEpX8M/Y5lG7MI3fmiO+xCnacOrVsnbqVrcDZIT8aSGkKV1w2WwvRQxSWw5SIS8ozg7+h8tSj5EO1Riqqxcdag==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.0.5.tgz", + "integrity": "sha512-XcCQ5qWHLvi29UUrowgDFvV4t7ETxX91CbDczMnoqXPOIcZOxyNdSjm6kV5XMc8+HkxfRegU/MUmnTbJRzGrUQ==", "dependencies": { - "@jest/types": "30.0.1", + "@jest/types": "30.0.5", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "natural-compare": "^1.4.0" @@ -2018,6 +2244,34 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/@jest/snapshot-utils/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/snapshot-utils/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2047,12 +2301,12 @@ } }, "node_modules/@jest/test-result": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.0.4.tgz", - "integrity": "sha512-Mfpv8kjyKTHqsuu9YugB6z1gcdB3TSSOaKlehtVaiNlClMkEHY+5ZqCY2CrEE3ntpBMlstX/ShDAf84HKWsyIw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.0.5.tgz", + "integrity": "sha512-wPyztnK0gbDMQAJZ43tdMro+qblDHH1Ru/ylzUo21TBKqt88ZqnKKK2m30LKmLLoKtR2lxdpCC/P3g1vfKcawQ==", "dependencies": { - "@jest/console": "30.0.4", - "@jest/types": "30.0.1", + "@jest/console": "30.0.5", + "@jest/types": "30.0.5", "@types/istanbul-lib-coverage": "^2.0.6", "collect-v8-coverage": "^1.0.2" }, @@ -2060,14 +2314,57 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/test-sequencer": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.0.4.tgz", - "integrity": "sha512-bj6ePmqi4uxAE8EHE0Slmk5uBYd9Vd/PcVt06CsBxzH4bbA8nGsI1YbXl/NH+eii4XRtyrRx+Cikub0x8H4vDg==", + "node_modules/@jest/test-result/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "dependencies": { - "@jest/test-result": "30.0.4", + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.0.5.tgz", + "integrity": "sha512-Aea/G1egWoIIozmDD7PBXUOxkekXl7ueGzrsGGi1SbeKgQqCYCIf+wfbflEbf2LiPxL8j2JZGLyrzZagjvW4YQ==", + "dependencies": { + "@jest/test-result": "30.0.5", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.2", + "jest-haste-map": "30.0.5", "slash": "^3.0.0" }, "engines": { @@ -2075,21 +2372,21 @@ } }, "node_modules/@jest/transform": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.4.tgz", - "integrity": "sha512-atvy4hRph/UxdCIBp+UB2jhEA/jJiUeGZ7QPgBi9jUUKNgi3WEoMXGNG7zbbELG2+88PMabUNCDchmqgJy3ELg==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.5.tgz", + "integrity": "sha512-Vk8amLQCmuZyy6GbBht1Jfo9RSdBtg7Lks+B0PecnjI8J+PCLQPGh7uI8Q/2wwpW2gLdiAfiHNsmekKlywULqg==", "dependencies": { "@babel/core": "^7.27.4", - "@jest/types": "30.0.1", + "@jest/types": "30.0.5", "@jridgewell/trace-mapping": "^0.3.25", "babel-plugin-istanbul": "^7.0.0", "chalk": "^4.1.2", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.2", + "jest-haste-map": "30.0.5", "jest-regex-util": "30.0.1", - "jest-util": "30.0.2", + "jest-util": "30.0.5", "micromatch": "^4.0.8", "pirates": "^4.0.7", "slash": "^3.0.0", @@ -2099,6 +2396,34 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/@jest/transform/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/transform/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2353,102 +2678,6 @@ "@octokit/openapi-types": "^25.1.0" } }, - "node_modules/@oxlint/darwin-arm64": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@oxlint/darwin-arm64/-/darwin-arm64-1.7.0.tgz", - "integrity": "sha512-51vhCSQO4NSkedwEwOyqThiYqV0DAUkwNdqMQK0d29j5zmtNJJJRRBLeQuLGdstNmn3F7WMQ75Ci0/3Nq4ff8A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oxlint/darwin-x64": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@oxlint/darwin-x64/-/darwin-x64-1.7.0.tgz", - "integrity": "sha512-c0GN52yehYZ4TYuh4lBH9wYbBOI/RDOxZhJdBsttG0GwfvKYg/tiPNrNEsPzu0/rd1j6x3yT0zt6vezDMeC1sQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oxlint/linux-arm64-gnu": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-gnu/-/linux-arm64-gnu-1.7.0.tgz", - "integrity": "sha512-pam/lbzbzVMDzc3f1hoRPtnUMEIqkn0dynlB5nUll/MVBSIvIPLS9kJLrRA48lrlqbkS9LGiF37JvpwXA58A9A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxlint/linux-arm64-musl": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-musl/-/linux-arm64-musl-1.7.0.tgz", - "integrity": "sha512-LTyPy9FYS3SZ2XxJx+ITvlAq/ek5PtZK9Z2m3W72TA8hchGhJy5eQ+aotYjd/YVXOpGRpB12RdOpOTsZRu50bA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxlint/linux-x64-gnu": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-gnu/-/linux-x64-gnu-1.7.0.tgz", - "integrity": "sha512-YtZ4DiAgjaEiqUiwnvtJ/znZMAAVPKR7pnsi6lqbA3BfXJ/IwMaNpdoGlCGVdDGeN4BuGCwnFtBVqKVvVg3DDg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxlint/linux-x64-musl": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-musl/-/linux-x64-musl-1.7.0.tgz", - "integrity": "sha512-5aIpemNUBvwMMk4MCx1V3M6R9eMB1/SS6/24Orax9FqaI1lDX08tySdv696sr4Lms9ocA+rotxIPW9NP9439vA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxlint/win32-arm64": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@oxlint/win32-arm64/-/win32-arm64-1.7.0.tgz", - "integrity": "sha512-fpFpkHwbAu0NcR5bc1WapCPcM9qSYi5lCRVOp1WwDoFLKI2b9/UWB8OEg8UHWV5dnBu7HZAWH/SEslYGkZNsbQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@oxlint/win32-x64": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@oxlint/win32-x64/-/win32-x64-1.7.0.tgz", - "integrity": "sha512-0EPWBWOiD3wZHgeWDlTUaiFzhzIonXykxYUC+NRerPQFkO/G+bd9uLMJddHDKqfP/7g8s3E5V6KvBvvFpb7U6g==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2459,9 +2688,9 @@ } }, "node_modules/@pkgr/core": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz", - "integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", "engines": { "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, @@ -2687,10 +2916,51 @@ "@sinonjs/commons": "^3.0.1" } }, + "node_modules/@stylistic/eslint-plugin": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.2.2.tgz", + "integrity": "sha512-bE2DUjruqXlHYP3Q2Gpqiuj2bHq7/88FnuaS0FjeGGLCy+X6a07bGVuwtiOYnPSLHR6jmx5Bwdv+j7l8H+G97A==", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/types": "^8.37.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0" + } + }, + "node_modules/@stylistic/eslint-plugin/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@stylistic/eslint-plugin/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@swc/core": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.0.tgz", - "integrity": "sha512-7Fh16ZH/Rj3Di720if+sw9BictD4N5kbTpsyDC+URXhvsZ7qRt1lH7PaeIQYyJJQHwFhoKpwwGxfGU9SHgPLdw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.2.tgz", + "integrity": "sha512-YWqn+0IKXDhqVLKoac4v2tV6hJqB/wOh8/Br8zjqeqBkKa77Qb0Kw2i7LOFzjFNZbZaPH6AlMGlBwNrxaauaAg==", "hasInstallScript": true, "dependencies": { "@swc/counter": "^0.1.3", @@ -2704,16 +2974,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.13.0", - "@swc/core-darwin-x64": "1.13.0", - "@swc/core-linux-arm-gnueabihf": "1.13.0", - "@swc/core-linux-arm64-gnu": "1.13.0", - "@swc/core-linux-arm64-musl": "1.13.0", - "@swc/core-linux-x64-gnu": "1.13.0", - "@swc/core-linux-x64-musl": "1.13.0", - "@swc/core-win32-arm64-msvc": "1.13.0", - "@swc/core-win32-ia32-msvc": "1.13.0", - "@swc/core-win32-x64-msvc": "1.13.0" + "@swc/core-darwin-arm64": "1.13.2", + "@swc/core-darwin-x64": "1.13.2", + "@swc/core-linux-arm-gnueabihf": "1.13.2", + "@swc/core-linux-arm64-gnu": "1.13.2", + "@swc/core-linux-arm64-musl": "1.13.2", + "@swc/core-linux-x64-gnu": "1.13.2", + "@swc/core-linux-x64-musl": "1.13.2", + "@swc/core-win32-arm64-msvc": "1.13.2", + "@swc/core-win32-ia32-msvc": "1.13.2", + "@swc/core-win32-x64-msvc": "1.13.2" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -2725,9 +2995,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.0.tgz", - "integrity": "sha512-SkmR9u7MHDu2X8hf7SjZTmsAfQTmel0mi+TJ7AGtufLwGySv6pwQfJ/CIJpcPxYENVqDJAFnDrHaKV8mgA6kxQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.2.tgz", + "integrity": "sha512-44p7ivuLSGFJ15Vly4ivLJjg3ARo4879LtEBAabcHhSZygpmkP8eyjyWxrH3OxkY1eRZSIJe8yRZPFw4kPXFPw==", "cpu": [ "arm64" ], @@ -2740,9 +3010,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.0.tgz", - "integrity": "sha512-15/SyDjXRtFJ09fYHBXUXrj4tpiSpCkjgsF1z3/sSpHH1POWpQUQzxmFyomPQVZ/SsDqP18WGH09Vph4Qriuiw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.2.tgz", + "integrity": "sha512-Lb9EZi7X2XDAVmuUlBm2UvVAgSCbD3qKqDCxSI4jEOddzVOpNCnyZ/xEampdngUIyDDhhJLYU9duC+Mcsv5Y+A==", "cpu": [ "x64" ], @@ -2755,9 +3025,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.0.tgz", - "integrity": "sha512-AHauVHZQEJI/dCZQg6VYNNQ6HROz8dSOnCSheXzzBw1DGWo77BlcxRP0fF0jaAXM9WNqtCUOY1HiJ9ohkAE61Q==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.2.tgz", + "integrity": "sha512-9TDe/92ee1x57x+0OqL1huG4BeljVx0nWW4QOOxp8CCK67Rpc/HHl2wciJ0Kl9Dxf2NvpNtkPvqj9+BUmM9WVA==", "cpu": [ "arm" ], @@ -2770,9 +3040,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.0.tgz", - "integrity": "sha512-qyZmBZF7asF6954/x7yn6R7Bzd45KRG05rK2atIF9J3MTa8az7vubP1Q3BWmmss1j8699DELpbuoJucGuhsNXw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.2.tgz", + "integrity": "sha512-KJUSl56DBk7AWMAIEcU83zl5mg3vlQYhLELhjwRFkGFMvghQvdqQ3zFOYa4TexKA7noBZa3C8fb24rI5sw9Exg==", "cpu": [ "arm64" ], @@ -2785,9 +3055,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.0.tgz", - "integrity": "sha512-whskQCOUlLQT7MjnronpHmyHegBka5ig9JkQvecbqhWzRfdwN+c2xTJs3kQsWy2Vc2f1hcL3D8hGIwY5TwPxMQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.2.tgz", + "integrity": "sha512-teU27iG1oyWpNh9CzcGQ48ClDRt/RCem7mYO7ehd2FY102UeTws2+OzLESS1TS1tEZipq/5xwx3FzbVgiolCiQ==", "cpu": [ "arm64" ], @@ -2800,9 +3070,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.0.tgz", - "integrity": "sha512-51n4P4nv6rblXyH3zCEktvmR9uSAZ7+zbfeby0sxbj8LS/IKuVd7iCwD5dwMj4CxG9Fs+HgjN73dLQF/OerHhg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.2.tgz", + "integrity": "sha512-dRPsyPyqpLD0HMRCRpYALIh4kdOir8pPg4AhNQZLehKowigRd30RcLXGNVZcc31Ua8CiPI4QSgjOIxK+EQe4LQ==", "cpu": [ "x64" ], @@ -2815,9 +3085,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.0.tgz", - "integrity": "sha512-VMqelgvnXs27eQyhDf1S2O2MxSdchIH7c1tkxODRtu9eotcAeniNNgqqLjZ5ML0MGeRk/WpbsAY/GWi7eSpiHw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.2.tgz", + "integrity": "sha512-CCxETW+KkYEQDqz1SYC15YIWYheqFC+PJVOW76Maa/8yu8Biw+HTAcblKf2isrlUtK8RvrQN94v3UXkC2NzCEw==", "cpu": [ "x64" ], @@ -2830,9 +3100,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.0.tgz", - "integrity": "sha512-NLJmseWJngWeENgat+O/WB4ptNxtx2X4OfPnSG5a/A4sxcn2E4jq91OPvbeUQwDkH+ZQWKXmbXFzt7Nn661QYA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.2.tgz", + "integrity": "sha512-Wv/QTA6PjyRLlmKcN6AmSI4jwSMRl0VTLGs57PHTqYRwwfwd7y4s2fIPJVBNbAlXd795dOEP6d/bGSQSyhOX3A==", "cpu": [ "arm64" ], @@ -2845,9 +3115,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.0.tgz", - "integrity": "sha512-UBfwrp0xW37KQGTA08mwrCLIm1ZKy6pXK8IVwou7BvhMgrItRNweTGyUrCnvDLUfyYFuJCmzcEaJ3NudtctD6g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.2.tgz", + "integrity": "sha512-PuCdtNynEkUNbUXX/wsyUC+t4mamIU5y00lT5vJcAvco3/r16Iaxl5UCzhXYaWZSNVZMzPp9qN8NlSL8M5pPxw==", "cpu": [ "ia32" ], @@ -2860,9 +3130,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.0.tgz", - "integrity": "sha512-BAB1P7Z/y2EENsfsPytPnjIyBVRZN2WULY+s3ozW4QkGmYHde6XXG28n0ABTHhcIOmmR2VzM+uaW1x48laSimw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.2.tgz", + "integrity": "sha512-qlmMkFZJus8cYuBURx1a3YAG2G7IW44i+FEYV5/32ylKkzGNAr9tDJSA53XNnNXkAB5EXSPsOz7bn5C3JlEtdQ==", "cpu": [ "x64" ], @@ -3047,12 +3317,49 @@ "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==" + "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.0.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.14.tgz", - "integrity": "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw==", + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", + "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", "dependencies": { "undici-types": "~7.8.0" } @@ -3103,15 +3410,15 @@ "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz", - "integrity": "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz", + "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.37.0", - "@typescript-eslint/type-utils": "8.37.0", - "@typescript-eslint/utils": "8.37.0", - "@typescript-eslint/visitor-keys": "8.37.0", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/type-utils": "8.38.0", + "@typescript-eslint/utils": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -3125,7 +3432,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.37.0", + "@typescript-eslint/parser": "^8.38.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -3140,14 +3447,14 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz", - "integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz", + "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", "dependencies": { - "@typescript-eslint/scope-manager": "8.37.0", - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/typescript-estree": "8.37.0", - "@typescript-eslint/visitor-keys": "8.37.0", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4" }, "engines": { @@ -3163,12 +3470,12 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.37.0.tgz", - "integrity": "sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz", + "integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.37.0", - "@typescript-eslint/types": "^8.37.0", + "@typescript-eslint/tsconfig-utils": "^8.38.0", + "@typescript-eslint/types": "^8.38.0", "debug": "^4.3.4" }, "engines": { @@ -3183,12 +3490,12 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz", - "integrity": "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz", + "integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==", "dependencies": { - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/visitor-keys": "8.37.0" + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3199,9 +3506,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz", - "integrity": "sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz", + "integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -3214,13 +3521,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz", - "integrity": "sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz", + "integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==", "dependencies": { - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/typescript-estree": "8.37.0", - "@typescript-eslint/utils": "8.37.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/utils": "8.38.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -3237,9 +3544,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.37.0.tgz", - "integrity": "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz", + "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -3249,14 +3556,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz", - "integrity": "sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz", + "integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==", "dependencies": { - "@typescript-eslint/project-service": "8.37.0", - "@typescript-eslint/tsconfig-utils": "8.37.0", - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/visitor-keys": "8.37.0", + "@typescript-eslint/project-service": "8.38.0", + "@typescript-eslint/tsconfig-utils": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -3298,14 +3605,14 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.37.0.tgz", - "integrity": "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz", + "integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.37.0", - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/typescript-estree": "8.37.0" + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3320,11 +3627,11 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz", - "integrity": "sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz", + "integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==", "dependencies": { - "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/types": "8.38.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -3801,13 +4108,12 @@ } }, "node_modules/axios": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", - "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", - "license": "MIT", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -3849,11 +4155,11 @@ } }, "node_modules/babel-jest": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.4.tgz", - "integrity": "sha512-UjG2j7sAOqsp2Xua1mS/e+ekddkSu3wpf4nZUSvXNHuVWdaOUXQ77+uyjJLDE9i0atm5x4kds8K9yb5lRsRtcA==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.5.tgz", + "integrity": "sha512-mRijnKimhGDMsizTvBTWotwNpzrkHr+VvZUQBof2AufXKB8NXrL1W69TG20EvOz7aevx6FTJIaBuBkYxS8zolg==", "dependencies": { - "@jest/transform": "30.0.4", + "@jest/transform": "30.0.5", "@types/babel__core": "^7.20.5", "babel-plugin-istanbul": "^7.0.0", "babel-preset-jest": "30.0.1", @@ -3912,9 +4218,9 @@ } }, "node_modules/babel-preset-current-node-syntax": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", - "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.1.tgz", + "integrity": "sha512-23fWKohMTvS5s0wwJKycOe0dBdCwQ6+iiLaNR9zy8P13mtFRFM9qLLX6HJX5DL2pi/FNDf3fCQHM4FIMoHH/7w==", "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", @@ -3933,7 +4239,7 @@ "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "node_modules/babel-preset-jest": { @@ -4345,25 +4651,24 @@ "integrity": "sha512-syedaZ9cPe7r3hoQA9twWYKu5AIyCswN5+szkmPBe9ccdLrj4bYaCnLVPTLd2kgVRc7+zoX4tyPgRnFKCj5YjQ==" }, "node_modules/cheerio": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.0.tgz", - "integrity": "sha512-+0hMx9eYhJvWbgpKV9hN7jg0JcwydpopZE4hgi+KvQtByZXPp04NiCWU0LzcAbP63abZckIHkTQaXVF52mX3xQ==", - "license": "MIT", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", + "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==", "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", - "encoding-sniffer": "^0.2.0", + "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.0.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", - "undici": "^7.10.0", + "undici": "^7.12.0", "whatwg-mimetype": "^4.0.0" }, "engines": { - "node": ">=18.17" + "node": ">=20.18.1" }, "funding": { "url": "https://github.com/cheeriojs/cheerio?sponsor=1" @@ -4703,21 +5008,19 @@ "integrity": "sha512-/f6gpQuxDaqXu+1kwQYSckUglPaOrHdbIlBAu0YuW8/Cdb45XwXYNUBXg3r/9Mo6n540Kn/smKcZWko5x99KrQ==" }, "node_modules/cross-env": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", - "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", - "license": "MIT", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.0.0.tgz", + "integrity": "sha512-aU8qlEK/nHYtVuN4p7UQgAwVljzMg8hB4YK5ThRqD2l/ziSnryncPNn7bMLt5cFYsKVKBh8HqLqyCoTupEUu7Q==", "dependencies": { - "cross-spawn": "^7.0.1" + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" }, "bin": { - "cross-env": "src/bin/cross-env.js", - "cross-env-shell": "src/bin/cross-env-shell.js" + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" }, "engines": { - "node": ">=10.14", - "npm": ">=6", - "yarn": ">=1" + "node": ">=20" } }, "node_modules/cross-spawn": { @@ -4981,9 +5284,9 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, "node_modules/electron-to-chromium": { - "version": "1.5.185", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.185.tgz", - "integrity": "sha512-dYOZfUk57hSMPePoIQ1fZWl1Fkj+OshhEVuPacNKWzC1efe56OsHY3l/jCfiAgIICOU3VgOIdoq7ahg7r7n6MQ==" + "version": "1.5.192", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.192.tgz", + "integrity": "sha512-rP8Ez0w7UNw/9j5eSXCe10o1g/8B1P5SM90PCCMVkIRQn2R0LEHWz4Eh9RnxkniuDe1W0cTSOB3MLlkTGDcuCg==" }, "node_modules/emittery": { "version": "0.13.1", @@ -5209,9 +5512,9 @@ } }, "node_modules/eslint": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", - "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", + "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -5219,8 +5522,8 @@ "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.31.0", - "@eslint/plugin-kit": "^0.3.1", + "@eslint/js": "9.32.0", + "@eslint/plugin-kit": "^0.3.4", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -5508,16 +5811,16 @@ } }, "node_modules/expect": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.4.tgz", - "integrity": "sha512-dDLGjnP2cKbEppxVICxI/Uf4YemmGMPNy0QytCbfafbpYk9AFQsxb8Uyrxii0RPK7FWgLGlSem+07WirwS3cFQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.5.tgz", + "integrity": "sha512-P0te2pt+hHI5qLJkIR+iMvS+lYUZml8rKKsohVHAGY+uClp9XVbdyYNJOIjSRpHVp8s8YqxJCiHUkSYZGr8rtQ==", "dependencies": { - "@jest/expect-utils": "30.0.4", + "@jest/expect-utils": "30.0.5", "@jest/get-type": "30.0.1", - "jest-matcher-utils": "30.0.4", - "jest-message-util": "30.0.2", - "jest-mock": "30.0.2", - "jest-util": "30.0.2" + "jest-matcher-utils": "30.0.5", + "jest-message-util": "30.0.5", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -5800,8 +6103,7 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.3", @@ -6262,7 +6564,6 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -6279,16 +6580,16 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, "node_modules/inquirer": { - "version": "12.7.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.7.0.tgz", - "integrity": "sha512-KKFRc++IONSyE2UYw9CJ1V0IWx5yQKomwB+pp3cWomWs+v2+ZsG11G2OVfAjFS6WWCppKw+RfKmpqGfSzD5QBQ==", + "version": "12.9.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.9.0.tgz", + "integrity": "sha512-LlFVmvWVCun7uEgPB3vups9NzBrjJn48kRNtFGw3xU1H5UXExTEz/oF1JGLaB0fvlkUB+W6JfgLcSEaSdH7RPA==", "dependencies": { - "@inquirer/core": "^10.1.14", - "@inquirer/prompts": "^7.6.0", - "@inquirer/type": "^3.0.7", + "@inquirer/core": "^10.1.15", + "@inquirer/prompts": "^7.8.0", + "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2", "mute-stream": "^2.0.0", - "run-async": "^4.0.4", + "run-async": "^4.0.5", "rxjs": "^7.8.2" }, "engines": { @@ -6610,14 +6911,14 @@ } }, "node_modules/jest": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.0.4.tgz", - "integrity": "sha512-9QE0RS4WwTj/TtTC4h/eFVmFAhGNVerSB9XpJh8sqaXlP73ILcPcZ7JWjjEtJJe2m8QyBLKKfPQuK+3F+Xij/g==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.0.5.tgz", + "integrity": "sha512-y2mfcJywuTUkvLm2Lp1/pFX8kTgMO5yyQGq/Sk/n2mN7XWYp4JsCZ/QXW34M8YScgk8bPZlREH04f6blPnoHnQ==", "dependencies": { - "@jest/core": "30.0.4", - "@jest/types": "30.0.1", + "@jest/core": "30.0.5", + "@jest/types": "30.0.5", "import-local": "^3.2.0", - "jest-cli": "30.0.4" + "jest-cli": "30.0.5" }, "bin": { "jest": "bin/jest.js" @@ -6635,12 +6936,12 @@ } }, "node_modules/jest-changed-files": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.2.tgz", - "integrity": "sha512-Ius/iRST9FKfJI+I+kpiDh8JuUlAISnRszF9ixZDIqJF17FckH5sOzKC8a0wd0+D+8em5ADRHA5V5MnfeDk2WA==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.5.tgz", + "integrity": "sha512-bGl2Ntdx0eAwXuGpdLdVYVr5YQHnSZlQ0y9HVDu565lCUAe9sj6JOtBbMmBBikGIegne9piDDIOeiLVoqTkz4A==", "dependencies": { "execa": "^5.1.1", - "jest-util": "30.0.2", + "jest-util": "30.0.5", "p-limit": "^3.1.0" }, "engines": { @@ -6648,27 +6949,27 @@ } }, "node_modules/jest-circus": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.0.4.tgz", - "integrity": "sha512-o6UNVfbXbmzjYgmVPtSQrr5xFZCtkDZGdTlptYvGFSN80RuOOlTe73djvMrs+QAuSERZWcHBNIOMH+OEqvjWuw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.0.5.tgz", + "integrity": "sha512-h/sjXEs4GS+NFFfqBDYT7y5Msfxh04EwWLhQi0F8kuWpe+J/7tICSlswU8qvBqumR3kFgHbfu7vU6qruWWBPug==", "dependencies": { - "@jest/environment": "30.0.4", - "@jest/expect": "30.0.4", - "@jest/test-result": "30.0.4", - "@jest/types": "30.0.1", + "@jest/environment": "30.0.5", + "@jest/expect": "30.0.5", + "@jest/test-result": "30.0.5", + "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "co": "^4.6.0", "dedent": "^1.6.0", "is-generator-fn": "^2.1.0", - "jest-each": "30.0.2", - "jest-matcher-utils": "30.0.4", - "jest-message-util": "30.0.2", - "jest-runtime": "30.0.4", - "jest-snapshot": "30.0.4", - "jest-util": "30.0.2", + "jest-each": "30.0.5", + "jest-matcher-utils": "30.0.5", + "jest-message-util": "30.0.5", + "jest-runtime": "30.0.5", + "jest-snapshot": "30.0.5", + "jest-util": "30.0.5", "p-limit": "^3.1.0", - "pretty-format": "30.0.2", + "pretty-format": "30.0.5", "pure-rand": "^7.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" @@ -6677,6 +6978,34 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-circus/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-circus/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -6693,19 +7022,19 @@ } }, "node_modules/jest-cli": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.0.4.tgz", - "integrity": "sha512-3dOrP3zqCWBkjoVG1zjYJpD9143N9GUCbwaF2pFF5brnIgRLHmKcCIw+83BvF1LxggfMWBA0gxkn6RuQVuRhIQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.0.5.tgz", + "integrity": "sha512-Sa45PGMkBZzF94HMrlX4kUyPOwUpdZasaliKN3mifvDmkhLYqLLg8HQTzn6gq7vJGahFYMQjXgyJWfYImKZzOw==", "dependencies": { - "@jest/core": "30.0.4", - "@jest/test-result": "30.0.4", - "@jest/types": "30.0.1", + "@jest/core": "30.0.5", + "@jest/test-result": "30.0.5", + "@jest/types": "30.0.5", "chalk": "^4.1.2", "exit-x": "^0.2.2", "import-local": "^3.2.0", - "jest-config": "30.0.4", - "jest-util": "30.0.2", - "jest-validate": "30.0.2", + "jest-config": "30.0.5", + "jest-util": "30.0.5", + "jest-validate": "30.0.5", "yargs": "^17.7.2" }, "bin": { @@ -6723,6 +7052,34 @@ } } }, + "node_modules/jest-cli/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-cli/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -6739,32 +7096,32 @@ } }, "node_modules/jest-config": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.0.4.tgz", - "integrity": "sha512-3dzbO6sh34thAGEjJIW0fgT0GA0EVlkski6ZzMcbW6dzhenylXAE/Mj2MI4HonroWbkKc6wU6bLVQ8dvBSZ9lA==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.0.5.tgz", + "integrity": "sha512-aIVh+JNOOpzUgzUnPn5FLtyVnqc3TQHVMupYtyeURSb//iLColiMIR8TxCIDKyx9ZgjKnXGucuW68hCxgbrwmA==", "dependencies": { "@babel/core": "^7.27.4", "@jest/get-type": "30.0.1", "@jest/pattern": "30.0.1", - "@jest/test-sequencer": "30.0.4", - "@jest/types": "30.0.1", - "babel-jest": "30.0.4", + "@jest/test-sequencer": "30.0.5", + "@jest/types": "30.0.5", + "babel-jest": "30.0.5", "chalk": "^4.1.2", "ci-info": "^4.2.0", "deepmerge": "^4.3.1", "glob": "^10.3.10", "graceful-fs": "^4.2.11", - "jest-circus": "30.0.4", + "jest-circus": "30.0.5", "jest-docblock": "30.0.1", - "jest-environment-node": "30.0.4", + "jest-environment-node": "30.0.5", "jest-regex-util": "30.0.1", - "jest-resolve": "30.0.2", - "jest-runner": "30.0.4", - "jest-util": "30.0.2", - "jest-validate": "30.0.2", + "jest-resolve": "30.0.5", + "jest-runner": "30.0.5", + "jest-util": "30.0.5", + "jest-validate": "30.0.5", "micromatch": "^4.0.8", "parse-json": "^5.2.0", - "pretty-format": "30.0.2", + "pretty-format": "30.0.5", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -6788,6 +7145,34 @@ } } }, + "node_modules/jest-config/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-config/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-config/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -6879,14 +7264,14 @@ } }, "node_modules/jest-diff": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.4.tgz", - "integrity": "sha512-TSjceIf6797jyd+R64NXqicttROD+Qf98fex7CowmlSn7f8+En0da1Dglwr1AXxDtVizoxXYZBlUQwNhoOXkNw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.5.tgz", + "integrity": "sha512-1UIqE9PoEKaHcIKvq2vbibrCog4Y8G0zmOxgQUVEiTqwR5hJVMCoDsN1vFvI5JvwD37hjueZ1C4l2FyGnfpE0A==", "dependencies": { "@jest/diff-sequences": "30.0.1", "@jest/get-type": "30.0.1", "chalk": "^4.1.2", - "pretty-format": "30.0.2" + "pretty-format": "30.0.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -6919,15 +7304,43 @@ } }, "node_modules/jest-each": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.0.2.tgz", - "integrity": "sha512-ZFRsTpe5FUWFQ9cWTMguCaiA6kkW5whccPy9JjD1ezxh+mJeqmz8naL8Fl/oSbNJv3rgB0x87WBIkA5CObIUZQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.0.5.tgz", + "integrity": "sha512-dKjRsx1uZ96TVyejD3/aAWcNKy6ajMaN531CwWIsrazIqIoXI9TnnpPlkrEYku/8rkS3dh2rbH+kMOyiEIv0xQ==", "dependencies": { "@jest/get-type": "30.0.1", - "@jest/types": "30.0.1", + "@jest/types": "30.0.5", "chalk": "^4.1.2", - "jest-util": "30.0.2", - "pretty-format": "30.0.2" + "jest-util": "30.0.5", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -6949,35 +7362,78 @@ } }, "node_modules/jest-environment-node": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.0.4.tgz", - "integrity": "sha512-p+rLEzC2eThXqiNh9GHHTC0OW5Ca4ZfcURp7scPjYBcmgpR9HG6750716GuUipYf2AcThU3k20B31USuiaaIEg==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.0.5.tgz", + "integrity": "sha512-ppYizXdLMSvciGsRsMEnv/5EFpvOdXBaXRBzFUDPWrsfmog4kYrOGWXarLllz6AXan6ZAA/kYokgDWuos1IKDA==", "dependencies": { - "@jest/environment": "30.0.4", - "@jest/fake-timers": "30.0.4", - "@jest/types": "30.0.1", + "@jest/environment": "30.0.5", + "@jest/fake-timers": "30.0.5", + "@jest/types": "30.0.5", "@types/node": "*", - "jest-mock": "30.0.2", - "jest-util": "30.0.2", - "jest-validate": "30.0.2" + "jest-mock": "30.0.5", + "jest-util": "30.0.5", + "jest-validate": "30.0.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-haste-map": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.2.tgz", - "integrity": "sha512-telJBKpNLeCb4MaX+I5k496556Y2FiKR/QLZc0+MGBYl4k3OO0472drlV2LUe7c1Glng5HuAu+5GLYp//GpdOQ==", + "node_modules/jest-environment-node/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "dependencies": { - "@jest/types": "30.0.1", + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-haste-map": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.5.tgz", + "integrity": "sha512-dkmlWNlsTSR0nH3nRfW5BKbqHefLZv0/6LCccG0xFCTWcJu8TuEwG+5Cm75iBfjVoockmO6J35o5gxtFSn5xeg==", + "dependencies": { + "@jest/types": "30.0.5", "@types/node": "*", "anymatch": "^3.1.3", "fb-watchman": "^2.0.2", "graceful-fs": "^4.2.11", "jest-regex-util": "30.0.1", - "jest-util": "30.0.2", - "jest-worker": "30.0.2", + "jest-util": "30.0.5", + "jest-worker": "30.0.5", "micromatch": "^4.0.8", "walker": "^1.0.8" }, @@ -6988,27 +7444,70 @@ "fsevents": "^2.3.3" } }, + "node_modules/jest-haste-map/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/jest-leak-detector": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.0.2.tgz", - "integrity": "sha512-U66sRrAYdALq+2qtKffBLDWsQ/XoNNs2Lcr83sc9lvE/hEpNafJlq2lXCPUBMNqamMECNxSIekLfe69qg4KMIQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.0.5.tgz", + "integrity": "sha512-3Uxr5uP8jmHMcsOtYMRB/zf1gXN3yUIc+iPorhNETG54gErFIiUhLvyY/OggYpSMOEYqsmRxmuU4ZOoX5jpRFg==", "dependencies": { "@jest/get-type": "30.0.1", - "pretty-format": "30.0.2" + "pretty-format": "30.0.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.4.tgz", - "integrity": "sha512-ubCewJ54YzeAZ2JeHHGVoU+eDIpQFsfPQs0xURPWoNiO42LGJ+QGgfSf+hFIRplkZDkhH5MOvuxHKXRTUU3dUQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.5.tgz", + "integrity": "sha512-uQgGWt7GOrRLP1P7IwNWwK1WAQbq+m//ZY0yXygyfWp0rJlksMSLQAA4wYQC3b6wl3zfnchyTx+k3HZ5aPtCbQ==", "dependencies": { "@jest/get-type": "30.0.1", "chalk": "^4.1.2", - "jest-diff": "30.0.4", - "pretty-format": "30.0.2" + "jest-diff": "30.0.5", + "pretty-format": "30.0.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -7030,18 +7529,17 @@ } }, "node_modules/jest-message-util": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.2.tgz", - "integrity": "sha512-vXywcxmr0SsKXF/bAD7t7nMamRvPuJkras00gqYeB1V0WllxZrbZ0paRr3XqpFU2sYYjD0qAaG2fRyn/CGZ0aw==", - "license": "MIT", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.5.tgz", + "integrity": "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA==", "dependencies": { "@babel/code-frame": "^7.27.1", - "@jest/types": "30.0.1", + "@jest/types": "30.0.5", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "micromatch": "^4.0.8", - "pretty-format": "30.0.2", + "pretty-format": "30.0.5", "slash": "^3.0.0", "stack-utils": "^2.0.6" }, @@ -7049,11 +7547,38 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-message-util/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-message-util/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -7066,19 +7591,61 @@ } }, "node_modules/jest-mock": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.2.tgz", - "integrity": "sha512-PnZOHmqup/9cT/y+pXIVbbi8ID6U1XHRmbvR7MvUy4SLqhCbwpkmXhLbsWbGewHrV5x/1bF7YDjs+x24/QSvFA==", - "license": "MIT", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", + "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", "dependencies": { - "@jest/types": "30.0.1", + "@jest/types": "30.0.5", "@types/node": "*", - "jest-util": "30.0.2" + "jest-util": "30.0.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-mock/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/jest-offline": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/jest-offline/-/jest-offline-1.0.1.tgz", @@ -7113,16 +7680,16 @@ } }, "node_modules/jest-resolve": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.0.2.tgz", - "integrity": "sha512-q/XT0XQvRemykZsvRopbG6FQUT6/ra+XV6rPijyjT6D0msOyCvR2A5PlWZLd+fH0U8XWKZfDiAgrUNDNX2BkCw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.0.5.tgz", + "integrity": "sha512-d+DjBQ1tIhdz91B79mywH5yYu76bZuE96sSbxj8MkjWVx5WNdt1deEFRONVL4UkKLSrAbMkdhb24XN691yDRHg==", "dependencies": { "chalk": "^4.1.2", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.2", + "jest-haste-map": "30.0.5", "jest-pnp-resolver": "^1.2.3", - "jest-util": "30.0.2", - "jest-validate": "30.0.2", + "jest-util": "30.0.5", + "jest-validate": "30.0.5", "slash": "^3.0.0", "unrs-resolver": "^1.7.11" }, @@ -7131,12 +7698,12 @@ } }, "node_modules/jest-resolve-dependencies": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.0.4.tgz", - "integrity": "sha512-EQBYow19B/hKr4gUTn+l8Z+YLlP2X0IoPyp0UydOtrcPbIOYzJ8LKdFd+yrbwztPQvmlBFUwGPPEzHH1bAvFAw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.0.5.tgz", + "integrity": "sha512-/xMvBR4MpwkrHW4ikZIWRttBBRZgWK4d6xt3xW1iRDSKt4tXzYkMkyPfBnSCgv96cpkrctfXs6gexeqMYqdEpw==", "dependencies": { "jest-regex-util": "30.0.1", - "jest-snapshot": "30.0.4" + "jest-snapshot": "30.0.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -7158,30 +7725,30 @@ } }, "node_modules/jest-runner": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.0.4.tgz", - "integrity": "sha512-mxY0vTAEsowJwvFJo5pVivbCpuu6dgdXRmt3v3MXjBxFly7/lTk3Td0PaMyGOeNQUFmSuGEsGYqhbn7PA9OekQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.0.5.tgz", + "integrity": "sha512-JcCOucZmgp+YuGgLAXHNy7ualBx4wYSgJVWrYMRBnb79j9PD0Jxh0EHvR5Cx/r0Ce+ZBC4hCdz2AzFFLl9hCiw==", "dependencies": { - "@jest/console": "30.0.4", - "@jest/environment": "30.0.4", - "@jest/test-result": "30.0.4", - "@jest/transform": "30.0.4", - "@jest/types": "30.0.1", + "@jest/console": "30.0.5", + "@jest/environment": "30.0.5", + "@jest/test-result": "30.0.5", + "@jest/transform": "30.0.5", + "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "emittery": "^0.13.1", "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", "jest-docblock": "30.0.1", - "jest-environment-node": "30.0.4", - "jest-haste-map": "30.0.2", - "jest-leak-detector": "30.0.2", - "jest-message-util": "30.0.2", - "jest-resolve": "30.0.2", - "jest-runtime": "30.0.4", - "jest-util": "30.0.2", - "jest-watcher": "30.0.4", - "jest-worker": "30.0.2", + "jest-environment-node": "30.0.5", + "jest-haste-map": "30.0.5", + "jest-leak-detector": "30.0.5", + "jest-message-util": "30.0.5", + "jest-resolve": "30.0.5", + "jest-runtime": "30.0.5", + "jest-util": "30.0.5", + "jest-watcher": "30.0.5", + "jest-worker": "30.0.5", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, @@ -7189,6 +7756,34 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-runner/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-runner/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -7205,30 +7800,30 @@ } }, "node_modules/jest-runtime": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.0.4.tgz", - "integrity": "sha512-tUQrZ8+IzoZYIHoPDQEB4jZoPyzBjLjq7sk0KVyd5UPRjRDOsN7o6UlvaGF8ddpGsjznl9PW+KRgWqCNO+Hn7w==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.0.5.tgz", + "integrity": "sha512-7oySNDkqpe4xpX5PPiJTe5vEa+Ak/NnNz2bGYZrA1ftG3RL3EFlHaUkA1Cjx+R8IhK0Vg43RML5mJedGTPNz3A==", "dependencies": { - "@jest/environment": "30.0.4", - "@jest/fake-timers": "30.0.4", - "@jest/globals": "30.0.4", + "@jest/environment": "30.0.5", + "@jest/fake-timers": "30.0.5", + "@jest/globals": "30.0.5", "@jest/source-map": "30.0.1", - "@jest/test-result": "30.0.4", - "@jest/transform": "30.0.4", - "@jest/types": "30.0.1", + "@jest/test-result": "30.0.5", + "@jest/transform": "30.0.5", + "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "cjs-module-lexer": "^2.1.0", "collect-v8-coverage": "^1.0.2", "glob": "^10.3.10", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.2", - "jest-message-util": "30.0.2", - "jest-mock": "30.0.2", + "jest-haste-map": "30.0.5", + "jest-message-util": "30.0.5", + "jest-mock": "30.0.5", "jest-regex-util": "30.0.1", - "jest-resolve": "30.0.2", - "jest-snapshot": "30.0.4", - "jest-util": "30.0.2", + "jest-resolve": "30.0.5", + "jest-snapshot": "30.0.5", + "jest-util": "30.0.5", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, @@ -7236,6 +7831,34 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-runtime/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-runtime/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -7327,29 +7950,29 @@ } }, "node_modules/jest-snapshot": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.4.tgz", - "integrity": "sha512-S/8hmSkeUib8WRUq9pWEb5zMfsOjiYWDWzFzKnjX7eDyKKgimsu9hcmsUEg8a7dPAw8s/FacxsXquq71pDgPjQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.5.tgz", + "integrity": "sha512-T00dWU/Ek3LqTp4+DcW6PraVxjk28WY5Ua/s+3zUKSERZSNyxTqhDXCWKG5p2HAJ+crVQ3WJ2P9YVHpj1tkW+g==", "dependencies": { "@babel/core": "^7.27.4", "@babel/generator": "^7.27.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.0.4", + "@jest/expect-utils": "30.0.5", "@jest/get-type": "30.0.1", - "@jest/snapshot-utils": "30.0.4", - "@jest/transform": "30.0.4", - "@jest/types": "30.0.1", + "@jest/snapshot-utils": "30.0.5", + "@jest/transform": "30.0.5", + "@jest/types": "30.0.5", "babel-preset-current-node-syntax": "^1.1.0", "chalk": "^4.1.2", - "expect": "30.0.4", + "expect": "30.0.5", "graceful-fs": "^4.2.11", - "jest-diff": "30.0.4", - "jest-matcher-utils": "30.0.4", - "jest-message-util": "30.0.2", - "jest-util": "30.0.2", - "pretty-format": "30.0.2", + "jest-diff": "30.0.5", + "jest-matcher-utils": "30.0.5", + "jest-message-util": "30.0.5", + "jest-util": "30.0.5", + "pretty-format": "30.0.5", "semver": "^7.7.2", "synckit": "^0.11.8" }, @@ -7357,6 +7980,34 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-snapshot/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-snapshot/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -7373,12 +8024,11 @@ } }, "node_modules/jest-util": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.2.tgz", - "integrity": "sha512-8IyqfKS4MqprBuUpZNlFB5l+WFehc8bfCe1HSZFHzft2mOuND8Cvi9r1musli+u6F3TqanCZ/Ik4H4pXUolZIg==", - "license": "MIT", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", + "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", "dependencies": { - "@jest/types": "30.0.1", + "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", @@ -7389,11 +8039,38 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-util/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-util/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -7406,10 +8083,9 @@ } }, "node_modules/jest-util/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "license": "MIT", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "engines": { "node": ">=12" }, @@ -7418,16 +8094,44 @@ } }, "node_modules/jest-validate": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.0.2.tgz", - "integrity": "sha512-noOvul+SFER4RIvNAwGn6nmV2fXqBq67j+hKGHKGFCmK4ks/Iy1FSrqQNBLGKlu4ZZIRL6Kg1U72N1nxuRCrGQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.0.5.tgz", + "integrity": "sha512-ouTm6VFHaS2boyl+k4u+Qip4TSH7Uld5tyD8psQ8abGgt2uYYB8VwVfAHWHjHc0NWmGGbwO5h0sCPOGHHevefw==", "dependencies": { "@jest/get-type": "30.0.1", - "@jest/types": "30.0.1", + "@jest/types": "30.0.5", "camelcase": "^6.3.0", "chalk": "^4.1.2", "leven": "^3.1.0", - "pretty-format": "30.0.2" + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -7460,23 +8164,51 @@ } }, "node_modules/jest-watcher": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.0.4.tgz", - "integrity": "sha512-YESbdHDs7aQOCSSKffG8jXqOKFqw4q4YqR+wHYpR5GWEQioGvL0BfbcjvKIvPEM0XGfsfJrka7jJz3Cc3gI4VQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.0.5.tgz", + "integrity": "sha512-z9slj/0vOwBDBjN3L4z4ZYaA+pG56d6p3kTUhFRYGvXbXMWhXmb/FIxREZCD06DYUwDKKnj2T80+Pb71CQ0KEg==", "dependencies": { - "@jest/test-result": "30.0.4", - "@jest/types": "30.0.1", + "@jest/test-result": "30.0.5", + "@jest/types": "30.0.5", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "emittery": "^0.13.1", - "jest-util": "30.0.2", + "jest-util": "30.0.5", "string-length": "^4.0.2" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-watcher/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-watcher/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-watcher/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -7493,13 +8225,13 @@ } }, "node_modules/jest-worker": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.2.tgz", - "integrity": "sha512-RN1eQmx7qSLFA+o9pfJKlqViwL5wt+OL3Vff/A+/cPsmuw7NPwfgl33AP+/agRmHzPOFgXviRycR9kYwlcRQXg==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.5.tgz", + "integrity": "sha512-ojRXsWzEP16NdUuBw/4H/zkZdHOa7MMYCk4E430l+8fELeLg/mqmMlRhjL7UNZvQrDmnovWZV4DxX03fZF48fQ==", "dependencies": { "@types/node": "*", "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.0.2", + "jest-util": "30.0.5", "merge-stream": "^2.0.0", "supports-color": "^8.1.1" }, @@ -7521,6 +8253,49 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jest/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/js-git": { "version": "0.7.8", "resolved": "https://registry.npmjs.org/js-git/-/js-git-0.7.8.tgz", @@ -7761,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", @@ -7954,9 +8753,9 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" }, "node_modules/napi-postinstall": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.0.tgz", - "integrity": "sha512-M7NqKyhODKV1gRLdkwE7pDsZP2/SC2a2vHkOYh9MCpKMbWVfyVfUw5MaH83Fv6XMjxr5jryUp3IDDL9rlxsTeA==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.2.tgz", + "integrity": "sha512-tWVJxJHmBWLy69PvO96TZMZDrzmw5KeiZBz3RHmiM2XZ9grBJ2WgMAFVVg25nqp3ZjTFUs2Ftw1JhscL3Teliw==", "bin": { "napi-postinstall": "lib/cli.js" }, @@ -8150,7 +8949,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", "dependencies": { "wrappy": "1" } @@ -8201,31 +8999,6 @@ "node": ">=0.10.0" } }, - "node_modules/oxlint": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.7.0.tgz", - "integrity": "sha512-krJN1fIRhs3xK1FyVyPtYIV9tkT4WDoIwI7eiMEKBuCjxqjQt5ZemQm1htPvHqNDOaWFRFt4btcwFdU8bbwgvA==", - "bin": { - "oxc_language_server": "bin/oxc_language_server", - "oxlint": "bin/oxlint" - }, - "engines": { - "node": ">=8.*" - }, - "funding": { - "url": "https://github.com/sponsors/Boshen" - }, - "optionalDependencies": { - "@oxlint/darwin-arm64": "1.7.0", - "@oxlint/darwin-x64": "1.7.0", - "@oxlint/linux-arm64-gnu": "1.7.0", - "@oxlint/linux-arm64-musl": "1.7.0", - "@oxlint/linux-x64-gnu": "1.7.0", - "@oxlint/linux-x64-musl": "1.7.0", - "@oxlint/win32-arm64": "1.7.0", - "@oxlint/win32-x64": "1.7.0" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -8411,7 +9184,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -8845,27 +9617,12 @@ "node": ">= 0.8.0" } }, - "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/pretty-format": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz", - "integrity": "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==", - "license": "MIT", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", "dependencies": { - "@jest/schemas": "30.0.1", + "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" }, @@ -8873,6 +9630,17 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/pretty-format/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/pretty-format/node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", @@ -9152,13 +9920,9 @@ } }, "node_modules/run-async": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.4.tgz", - "integrity": "sha512-2cgeRHnV11lSXBEhq7sN7a5UVjTKm9JTb9x8ApIT//16D7QL96AgnNeWSGoB4gIHc0iYw/Ha0Z+waBaCYZVNhg==", - "dependencies": { - "oxlint": "^1.2.0", - "prettier": "^3.5.3" - }, + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.5.tgz", + "integrity": "sha512-oN9GTgxUNDBumHTTDmQ8dep6VIJbgj9S3dPP+9XylVLIK4xB9XTXtKWROd5pnhdXR9k0EgO1JRcNh0T+Ny2FsA==", "engines": { "node": ">=0.12.0" } @@ -9767,11 +10531,11 @@ } }, "node_modules/synckit": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz", - "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==", + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", "dependencies": { - "@pkgr/core": "^0.2.4" + "@pkgr/core": "^0.2.9" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -10045,10 +10809,9 @@ } }, "node_modules/undici": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.10.0.tgz", - "integrity": "sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==", - "license": "MIT", + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.12.0.tgz", + "integrity": "sha512-GrKEsc3ughskmGA9jevVlIOPMiiAHJ4OFUtaAH+NhfTUSiZ1wMPIQqQvAJUrJspFXJt3EBWgpAeoHEDVT1IBug==", "engines": { "node": ">=20.18.1" } @@ -10182,6 +10945,18 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/uzip-module": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/uzip-module/-/uzip-module-1.0.3.tgz", @@ -10428,8 +11203,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/write-file-atomic": { "version": "5.0.1", @@ -10689,12 +11463,12 @@ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==" }, "@babel/helpers": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", - "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", + "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", "requires": { "@babel/template": "^7.27.2", - "@babel/types": "^7.27.6" + "@babel/types": "^7.28.2" } }, "@babel/parser": { @@ -10866,9 +11640,9 @@ } }, "@babel/types": { - "version": "7.28.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", - "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "requires": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" @@ -10895,33 +11669,38 @@ } }, "@emnapi/core": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.4.tgz", - "integrity": "sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", + "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", "optional": true, "requires": { - "@emnapi/wasi-threads": "1.0.3", + "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" } }, "@emnapi/runtime": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.4.tgz", - "integrity": "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", + "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", "optional": true, "requires": { "tslib": "^2.4.0" } }, "@emnapi/wasi-threads": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.3.tgz", - "integrity": "sha512-8K5IFFsQqF9wQNJptGbS6FNKgUTsSRYnTqNCG1vPP8jFdjSv18n2mQfJpkt2Oibo9iBEzcDnDxNwKTzC7svlJw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", + "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", "optional": true, "requires": { "tslib": "^2.4.0" } }, + "@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==" + }, "@esbuild/aix-ppc64": { "version": "0.25.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", @@ -11145,9 +11924,9 @@ } }, "@eslint/js": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", - "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==" + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", + "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==" }, "@eslint/object-schema": { "version": "2.1.6", @@ -11230,33 +12009,33 @@ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==" }, "@inquirer/checkbox": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.9.tgz", - "integrity": "sha512-DBJBkzI5Wx4jFaYm221LHvAhpKYkhVS0k9plqHwaHhofGNxvYB7J3Bz8w+bFJ05zaMb0sZNHo4KdmENQFlNTuQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.2.0.tgz", + "integrity": "sha512-fdSw07FLJEU5vbpOPzXo5c6xmMGDzbZE2+niuDHX5N6mc6V0Ebso/q3xiHra4D73+PMsC8MJmcaZKuAAoaQsSA==", "requires": { - "@inquirer/core": "^10.1.14", - "@inquirer/figures": "^1.0.12", - "@inquirer/type": "^3.0.7", + "@inquirer/core": "^10.1.15", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" } }, "@inquirer/confirm": { - "version": "5.1.13", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.13.tgz", - "integrity": "sha512-EkCtvp67ICIVVzjsquUiVSd+V5HRGOGQfsqA4E4vMWhYnB7InUL0pa0TIWt1i+OfP16Gkds8CdIu6yGZwOM1Yw==", + "version": "5.1.14", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.14.tgz", + "integrity": "sha512-5yR4IBfe0kXe59r1YCTG8WXkUbl7Z35HK87Sw+WUyGD8wNUx7JvY7laahzeytyE1oLn74bQnL7hstctQxisQ8Q==", "requires": { - "@inquirer/core": "^10.1.14", - "@inquirer/type": "^3.0.7" + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8" } }, "@inquirer/core": { - "version": "10.1.14", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.14.tgz", - "integrity": "sha512-Ma+ZpOJPewtIYl6HZHZckeX1STvDnHTCB2GVINNUlSEn2Am6LddWwfPkIGY0IUFVjUUrr/93XlBwTK6mfLjf0A==", + "version": "10.1.15", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.15.tgz", + "integrity": "sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA==", "requires": { - "@inquirer/figures": "^1.0.12", - "@inquirer/type": "^3.0.7", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", @@ -11278,112 +12057,112 @@ } }, "@inquirer/editor": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.14.tgz", - "integrity": "sha512-yd2qtLl4QIIax9DTMZ1ZN2pFrrj+yL3kgIWxm34SS6uwCr0sIhsNyudUjAo5q3TqI03xx4SEBkUJqZuAInp9uA==", + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.15.tgz", + "integrity": "sha512-wst31XT8DnGOSS4nNJDIklGKnf+8shuauVrWzgKegWUe28zfCftcWZ2vktGdzJgcylWSS2SrDnYUb6alZcwnCQ==", "requires": { - "@inquirer/core": "^10.1.14", - "@inquirer/type": "^3.0.7", + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", "external-editor": "^3.1.0" } }, "@inquirer/expand": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.16.tgz", - "integrity": "sha512-oiDqafWzMtofeJyyGkb1CTPaxUkjIcSxePHHQCfif8t3HV9pHcw1Kgdw3/uGpDvaFfeTluwQtWiqzPVjAqS3zA==", + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.17.tgz", + "integrity": "sha512-PSqy9VmJx/VbE3CT453yOfNa+PykpKg/0SYP7odez1/NWBGuDXgPhp4AeGYYKjhLn5lUUavVS/JbeYMPdH50Mw==", "requires": { - "@inquirer/core": "^10.1.14", - "@inquirer/type": "^3.0.7", + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" } }, "@inquirer/figures": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.12.tgz", - "integrity": "sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==" + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", + "integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==" }, "@inquirer/input": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.2.0.tgz", - "integrity": "sha512-opqpHPB1NjAmDISi3uvZOTrjEEU5CWVu/HBkDby8t93+6UxYX0Z7Ps0Ltjm5sZiEbWenjubwUkivAEYQmy9xHw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.2.1.tgz", + "integrity": "sha512-tVC+O1rBl0lJpoUZv4xY+WGWY8V5b0zxU1XDsMsIHYregdh7bN5X5QnIONNBAl0K765FYlAfNHS2Bhn7SSOVow==", "requires": { - "@inquirer/core": "^10.1.14", - "@inquirer/type": "^3.0.7" + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8" } }, "@inquirer/number": { - "version": "3.0.16", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.16.tgz", - "integrity": "sha512-kMrXAaKGavBEoBYUCgualbwA9jWUx2TjMA46ek+pEKy38+LFpL9QHlTd8PO2kWPUgI/KB+qi02o4y2rwXbzr3Q==", + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.17.tgz", + "integrity": "sha512-GcvGHkyIgfZgVnnimURdOueMk0CztycfC8NZTiIY9arIAkeOgt6zG57G+7vC59Jns3UX27LMkPKnKWAOF5xEYg==", "requires": { - "@inquirer/core": "^10.1.14", - "@inquirer/type": "^3.0.7" + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8" } }, "@inquirer/password": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.16.tgz", - "integrity": "sha512-g8BVNBj5Zeb5/Y3cSN+hDUL7CsIFDIuVxb9EPty3lkxBaYpjL5BNRKSYOF9yOLe+JOcKFd+TSVeADQ4iSY7rbg==", + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.17.tgz", + "integrity": "sha512-DJolTnNeZ00E1+1TW+8614F7rOJJCM4y4BAGQ3Gq6kQIG+OJ4zr3GLjIjVVJCbKsk2jmkmv6v2kQuN/vriHdZA==", "requires": { - "@inquirer/core": "^10.1.14", - "@inquirer/type": "^3.0.7", + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2" } }, "@inquirer/prompts": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.6.0.tgz", - "integrity": "sha512-jAhL7tyMxB3Gfwn4HIJ0yuJ5pvcB5maYUcouGcgd/ub79f9MqZ+aVnBtuFf+VC2GTkCBF+R+eo7Vi63w5VZlzw==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.8.0.tgz", + "integrity": "sha512-JHwGbQ6wjf1dxxnalDYpZwZxUEosT+6CPGD9Zh4sm9WXdtUp9XODCQD3NjSTmu+0OAyxWXNOqf0spjIymJa2Tw==", "requires": { - "@inquirer/checkbox": "^4.1.9", - "@inquirer/confirm": "^5.1.13", - "@inquirer/editor": "^4.2.14", - "@inquirer/expand": "^4.0.16", - "@inquirer/input": "^4.2.0", - "@inquirer/number": "^3.0.16", - "@inquirer/password": "^4.0.16", - "@inquirer/rawlist": "^4.1.4", - "@inquirer/search": "^3.0.16", - "@inquirer/select": "^4.2.4" + "@inquirer/checkbox": "^4.2.0", + "@inquirer/confirm": "^5.1.14", + "@inquirer/editor": "^4.2.15", + "@inquirer/expand": "^4.0.17", + "@inquirer/input": "^4.2.1", + "@inquirer/number": "^3.0.17", + "@inquirer/password": "^4.0.17", + "@inquirer/rawlist": "^4.1.5", + "@inquirer/search": "^3.1.0", + "@inquirer/select": "^4.3.1" } }, "@inquirer/rawlist": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.4.tgz", - "integrity": "sha512-5GGvxVpXXMmfZNtvWw4IsHpR7RzqAR624xtkPd1NxxlV5M+pShMqzL4oRddRkg8rVEOK9fKdJp1jjVML2Lr7TQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.5.tgz", + "integrity": "sha512-R5qMyGJqtDdi4Ht521iAkNqyB6p2UPuZUbMifakg1sWtu24gc2Z8CJuw8rP081OckNDMgtDCuLe42Q2Kr3BolA==", "requires": { - "@inquirer/core": "^10.1.14", - "@inquirer/type": "^3.0.7", + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" } }, "@inquirer/search": { - "version": "3.0.16", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.16.tgz", - "integrity": "sha512-POCmXo+j97kTGU6aeRjsPyuCpQQfKcMXdeTMw708ZMtWrj5aykZvlUxH4Qgz3+Y1L/cAVZsSpA+UgZCu2GMOMg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.1.0.tgz", + "integrity": "sha512-PMk1+O/WBcYJDq2H7foV0aAZSmDdkzZB9Mw2v/DmONRJopwA/128cS9M/TXWLKKdEQKZnKwBzqu2G4x/2Nqx8Q==", "requires": { - "@inquirer/core": "^10.1.14", - "@inquirer/figures": "^1.0.12", - "@inquirer/type": "^3.0.7", + "@inquirer/core": "^10.1.15", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" } }, "@inquirer/select": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.2.4.tgz", - "integrity": "sha512-unTppUcTjmnbl/q+h8XeQDhAqIOmwWYWNyiiP2e3orXrg6tOaa5DHXja9PChCSbChOsktyKgOieRZFnajzxoBg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.3.1.tgz", + "integrity": "sha512-Gfl/5sqOF5vS/LIrSndFgOh7jgoe0UXEizDqahFRkq5aJBLegZ6WjuMh/hVEJwlFQjyLq1z9fRtvUMkb7jM1LA==", "requires": { - "@inquirer/core": "^10.1.14", - "@inquirer/figures": "^1.0.12", - "@inquirer/type": "^3.0.7", + "@inquirer/core": "^10.1.15", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" } }, "@inquirer/type": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.7.tgz", - "integrity": "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.8.tgz", + "integrity": "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==", "requires": {} }, "@isaacs/balanced-match": { @@ -11475,18 +12254,40 @@ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==" }, "@jest/console": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.0.4.tgz", - "integrity": "sha512-tMLCDvBJBwPqMm4OAiuKm2uF5y5Qe26KgcMn+nrDSWpEW+eeFmqA0iO4zJfL16GP7gE3bUUQ3hIuUJ22AqVRnw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.0.5.tgz", + "integrity": "sha512-xY6b0XiL0Nav3ReresUarwl2oIz1gTnxGbGpho9/rbUWsLH0f1OD/VT84xs8c7VmH7MChnLb0pag6PhZhAdDiA==", "requires": { - "@jest/types": "30.0.1", + "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", - "jest-message-util": "30.0.2", - "jest-util": "30.0.2", + "jest-message-util": "30.0.5", + "jest-util": "30.0.5", "slash": "^3.0.0" }, "dependencies": { + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -11499,40 +12300,62 @@ } }, "@jest/core": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.0.4.tgz", - "integrity": "sha512-MWScSO9GuU5/HoWjpXAOBs6F/iobvK1XlioelgOM9St7S0Z5WTI9kjCQLPeo4eQRRYusyLW25/J7J5lbFkrYXw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.0.5.tgz", + "integrity": "sha512-fKD0OulvRsXF1hmaFgHhVJzczWzA1RXMMo9LTPuFXo9q/alDbME3JIyWYqovWsUBWSoBcsHaGPSLF9rz4l9Qeg==", "requires": { - "@jest/console": "30.0.4", + "@jest/console": "30.0.5", "@jest/pattern": "30.0.1", - "@jest/reporters": "30.0.4", - "@jest/test-result": "30.0.4", - "@jest/transform": "30.0.4", - "@jest/types": "30.0.1", + "@jest/reporters": "30.0.5", + "@jest/test-result": "30.0.5", + "@jest/transform": "30.0.5", + "@jest/types": "30.0.5", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "ci-info": "^4.2.0", "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", - "jest-changed-files": "30.0.2", - "jest-config": "30.0.4", - "jest-haste-map": "30.0.2", - "jest-message-util": "30.0.2", + "jest-changed-files": "30.0.5", + "jest-config": "30.0.5", + "jest-haste-map": "30.0.5", + "jest-message-util": "30.0.5", "jest-regex-util": "30.0.1", - "jest-resolve": "30.0.2", - "jest-resolve-dependencies": "30.0.4", - "jest-runner": "30.0.4", - "jest-runtime": "30.0.4", - "jest-snapshot": "30.0.4", - "jest-util": "30.0.2", - "jest-validate": "30.0.2", - "jest-watcher": "30.0.4", + "jest-resolve": "30.0.5", + "jest-resolve-dependencies": "30.0.5", + "jest-runner": "30.0.5", + "jest-runtime": "30.0.5", + "jest-snapshot": "30.0.5", + "jest-util": "30.0.5", + "jest-validate": "30.0.5", + "jest-watcher": "30.0.5", "micromatch": "^4.0.8", - "pretty-format": "30.0.2", + "pretty-format": "30.0.5", "slash": "^3.0.0" }, "dependencies": { + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -11558,44 +12381,110 @@ "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==" }, "@jest/environment": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.4.tgz", - "integrity": "sha512-5NT+sr7ZOb8wW7C4r7wOKnRQ8zmRWQT2gW4j73IXAKp5/PX1Z8MCStBLQDYfIG3n1Sw0NRfYGdp0iIPVooBAFQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.5.tgz", + "integrity": "sha512-aRX7WoaWx1oaOkDQvCWImVQ8XNtdv5sEWgk4gxR6NXb7WBUnL5sRak4WRzIQRZ1VTWPvV4VI4mgGjNL9TeKMYA==", "requires": { - "@jest/fake-timers": "30.0.4", - "@jest/types": "30.0.1", + "@jest/fake-timers": "30.0.5", + "@jest/types": "30.0.5", "@types/node": "*", - "jest-mock": "30.0.2" + "jest-mock": "30.0.5" + }, + "dependencies": { + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } } }, "@jest/expect": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.0.4.tgz", - "integrity": "sha512-Z/DL7t67LBHSX4UzDyeYKqOxE/n7lbrrgEwWM3dGiH5Dgn35nk+YtgzKudmfIrBI8DRRrKYY5BCo3317HZV1Fw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.0.5.tgz", + "integrity": "sha512-6udac8KKrtTtC+AXZ2iUN/R7dp7Ydry+Fo6FPFnDG54wjVMnb6vW/XNlf7Xj8UDjAE3aAVAsR4KFyKk3TCXmTA==", "requires": { - "expect": "30.0.4", - "jest-snapshot": "30.0.4" + "expect": "30.0.5", + "jest-snapshot": "30.0.5" } }, "@jest/expect-utils": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.4.tgz", - "integrity": "sha512-EgXecHDNfANeqOkcak0DxsoVI4qkDUsR7n/Lr2vtmTBjwLPBnnPOF71S11Q8IObWzxm2QgQoY6f9hzrRD3gHRA==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.5.tgz", + "integrity": "sha512-F3lmTT7CXWYywoVUGTCmom0vXq3HTTkaZyTAzIy+bXSBizB7o5qzlC9VCtq0arOa8GqmNsbg/cE9C6HLn7Szew==", "requires": { "@jest/get-type": "30.0.1" } }, "@jest/fake-timers": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.4.tgz", - "integrity": "sha512-qZ7nxOcL5+gwBO6LErvwVy5k06VsX/deqo2XnVUSTV0TNC9lrg8FC3dARbi+5lmrr5VyX5drragK+xLcOjvjYw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.5.tgz", + "integrity": "sha512-ZO5DHfNV+kgEAeP3gK3XlpJLL4U3Sz6ebl/n68Uwt64qFFs5bv4bfEEjyRGK5uM0C90ewooNgFuKMdkbEoMEXw==", "requires": { - "@jest/types": "30.0.1", + "@jest/types": "30.0.5", "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", - "jest-message-util": "30.0.2", - "jest-mock": "30.0.2", - "jest-util": "30.0.2" + "jest-message-util": "30.0.5", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" + }, + "dependencies": { + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } } }, "@jest/get-type": { @@ -11604,14 +12493,47 @@ "integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==" }, "@jest/globals": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.0.4.tgz", - "integrity": "sha512-avyZuxEHF2EUhFF6NEWVdxkRRV6iXXcIES66DLhuLlU7lXhtFG/ySq/a8SRZmEJSsLkNAFX6z6mm8KWyXe9OEA==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.0.5.tgz", + "integrity": "sha512-7oEJT19WW4oe6HR7oLRvHxwlJk2gev0U9px3ufs8sX9PoD1Eza68KF0/tlN7X0dq/WVsBScXQGgCldA1V9Y/jA==", "requires": { - "@jest/environment": "30.0.4", - "@jest/expect": "30.0.4", - "@jest/types": "30.0.1", - "jest-mock": "30.0.2" + "@jest/environment": "30.0.5", + "@jest/expect": "30.0.5", + "@jest/types": "30.0.5", + "jest-mock": "30.0.5" + }, + "dependencies": { + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } } }, "@jest/pattern": { @@ -11624,15 +12546,15 @@ } }, "@jest/reporters": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.0.4.tgz", - "integrity": "sha512-6ycNmP0JSJEEys1FbIzHtjl9BP0tOZ/KN6iMeAKrdvGmUsa1qfRdlQRUDKJ4P84hJ3xHw1yTqJt4fvPNHhyE+g==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.0.5.tgz", + "integrity": "sha512-mafft7VBX4jzED1FwGC1o/9QUM2xebzavImZMeqnsklgcyxBto8mV4HzNSzUrryJ+8R9MFOM3HgYuDradWR+4g==", "requires": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.0.4", - "@jest/test-result": "30.0.4", - "@jest/transform": "30.0.4", - "@jest/types": "30.0.1", + "@jest/console": "30.0.5", + "@jest/test-result": "30.0.5", + "@jest/transform": "30.0.5", + "@jest/types": "30.0.5", "@jridgewell/trace-mapping": "^0.3.25", "@types/node": "*", "chalk": "^4.1.2", @@ -11645,14 +12567,36 @@ "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^5.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "30.0.2", - "jest-util": "30.0.2", - "jest-worker": "30.0.2", + "jest-message-util": "30.0.5", + "jest-util": "30.0.5", + "jest-worker": "30.0.5", "slash": "^3.0.0", "string-length": "^4.0.2", "v8-to-istanbul": "^9.0.1" }, "dependencies": { + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, "brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -11725,16 +12669,38 @@ } }, "@jest/snapshot-utils": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.0.4.tgz", - "integrity": "sha512-BEpX8M/Y5lG7MI3fmiO+xCnacOrVsnbqVrcDZIT8aSGkKV1w2WwvRQxSWw5SIS8ozg7+h8tSj5EO1Riqqxcdag==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.0.5.tgz", + "integrity": "sha512-XcCQ5qWHLvi29UUrowgDFvV4t7ETxX91CbDczMnoqXPOIcZOxyNdSjm6kV5XMc8+HkxfRegU/MUmnTbJRzGrUQ==", "requires": { - "@jest/types": "30.0.1", + "@jest/types": "30.0.5", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "natural-compare": "^1.4.0" }, "dependencies": { + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -11757,49 +12723,104 @@ } }, "@jest/test-result": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.0.4.tgz", - "integrity": "sha512-Mfpv8kjyKTHqsuu9YugB6z1gcdB3TSSOaKlehtVaiNlClMkEHY+5ZqCY2CrEE3ntpBMlstX/ShDAf84HKWsyIw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.0.5.tgz", + "integrity": "sha512-wPyztnK0gbDMQAJZ43tdMro+qblDHH1Ru/ylzUo21TBKqt88ZqnKKK2m30LKmLLoKtR2lxdpCC/P3g1vfKcawQ==", "requires": { - "@jest/console": "30.0.4", - "@jest/types": "30.0.1", + "@jest/console": "30.0.5", + "@jest/types": "30.0.5", "@types/istanbul-lib-coverage": "^2.0.6", "collect-v8-coverage": "^1.0.2" + }, + "dependencies": { + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } } }, "@jest/test-sequencer": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.0.4.tgz", - "integrity": "sha512-bj6ePmqi4uxAE8EHE0Slmk5uBYd9Vd/PcVt06CsBxzH4bbA8nGsI1YbXl/NH+eii4XRtyrRx+Cikub0x8H4vDg==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.0.5.tgz", + "integrity": "sha512-Aea/G1egWoIIozmDD7PBXUOxkekXl7ueGzrsGGi1SbeKgQqCYCIf+wfbflEbf2LiPxL8j2JZGLyrzZagjvW4YQ==", "requires": { - "@jest/test-result": "30.0.4", + "@jest/test-result": "30.0.5", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.2", + "jest-haste-map": "30.0.5", "slash": "^3.0.0" } }, "@jest/transform": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.4.tgz", - "integrity": "sha512-atvy4hRph/UxdCIBp+UB2jhEA/jJiUeGZ7QPgBi9jUUKNgi3WEoMXGNG7zbbELG2+88PMabUNCDchmqgJy3ELg==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.5.tgz", + "integrity": "sha512-Vk8amLQCmuZyy6GbBht1Jfo9RSdBtg7Lks+B0PecnjI8J+PCLQPGh7uI8Q/2wwpW2gLdiAfiHNsmekKlywULqg==", "requires": { "@babel/core": "^7.27.4", - "@jest/types": "30.0.1", + "@jest/types": "30.0.5", "@jridgewell/trace-mapping": "^0.3.25", "babel-plugin-istanbul": "^7.0.0", "chalk": "^4.1.2", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.2", + "jest-haste-map": "30.0.5", "jest-regex-util": "30.0.1", - "jest-util": "30.0.2", + "jest-util": "30.0.5", "micromatch": "^4.0.8", "pirates": "^4.0.7", "slash": "^3.0.0", "write-file-atomic": "^5.0.1" }, "dependencies": { + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -11993,54 +13014,6 @@ "@octokit/openapi-types": "^25.1.0" } }, - "@oxlint/darwin-arm64": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@oxlint/darwin-arm64/-/darwin-arm64-1.7.0.tgz", - "integrity": "sha512-51vhCSQO4NSkedwEwOyqThiYqV0DAUkwNdqMQK0d29j5zmtNJJJRRBLeQuLGdstNmn3F7WMQ75Ci0/3Nq4ff8A==", - "optional": true - }, - "@oxlint/darwin-x64": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@oxlint/darwin-x64/-/darwin-x64-1.7.0.tgz", - "integrity": "sha512-c0GN52yehYZ4TYuh4lBH9wYbBOI/RDOxZhJdBsttG0GwfvKYg/tiPNrNEsPzu0/rd1j6x3yT0zt6vezDMeC1sQ==", - "optional": true - }, - "@oxlint/linux-arm64-gnu": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-gnu/-/linux-arm64-gnu-1.7.0.tgz", - "integrity": "sha512-pam/lbzbzVMDzc3f1hoRPtnUMEIqkn0dynlB5nUll/MVBSIvIPLS9kJLrRA48lrlqbkS9LGiF37JvpwXA58A9A==", - "optional": true - }, - "@oxlint/linux-arm64-musl": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-musl/-/linux-arm64-musl-1.7.0.tgz", - "integrity": "sha512-LTyPy9FYS3SZ2XxJx+ITvlAq/ek5PtZK9Z2m3W72TA8hchGhJy5eQ+aotYjd/YVXOpGRpB12RdOpOTsZRu50bA==", - "optional": true - }, - "@oxlint/linux-x64-gnu": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-gnu/-/linux-x64-gnu-1.7.0.tgz", - "integrity": "sha512-YtZ4DiAgjaEiqUiwnvtJ/znZMAAVPKR7pnsi6lqbA3BfXJ/IwMaNpdoGlCGVdDGeN4BuGCwnFtBVqKVvVg3DDg==", - "optional": true - }, - "@oxlint/linux-x64-musl": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-musl/-/linux-x64-musl-1.7.0.tgz", - "integrity": "sha512-5aIpemNUBvwMMk4MCx1V3M6R9eMB1/SS6/24Orax9FqaI1lDX08tySdv696sr4Lms9ocA+rotxIPW9NP9439vA==", - "optional": true - }, - "@oxlint/win32-arm64": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@oxlint/win32-arm64/-/win32-arm64-1.7.0.tgz", - "integrity": "sha512-fpFpkHwbAu0NcR5bc1WapCPcM9qSYi5lCRVOp1WwDoFLKI2b9/UWB8OEg8UHWV5dnBu7HZAWH/SEslYGkZNsbQ==", - "optional": true - }, - "@oxlint/win32-x64": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@oxlint/win32-x64/-/win32-x64-1.7.0.tgz", - "integrity": "sha512-0EPWBWOiD3wZHgeWDlTUaiFzhzIonXykxYUC+NRerPQFkO/G+bd9uLMJddHDKqfP/7g8s3E5V6KvBvvFpb7U6g==", - "optional": true - }, "@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -12048,9 +13021,9 @@ "optional": true }, "@pkgr/core": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz", - "integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==" + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==" }, "@pm2/agent": { "version": "2.1.1", @@ -12235,83 +13208,108 @@ "@sinonjs/commons": "^3.0.1" } }, - "@swc/core": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.0.tgz", - "integrity": "sha512-7Fh16ZH/Rj3Di720if+sw9BictD4N5kbTpsyDC+URXhvsZ7qRt1lH7PaeIQYyJJQHwFhoKpwwGxfGU9SHgPLdw==", + "@stylistic/eslint-plugin": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.2.2.tgz", + "integrity": "sha512-bE2DUjruqXlHYP3Q2Gpqiuj2bHq7/88FnuaS0FjeGGLCy+X6a07bGVuwtiOYnPSLHR6jmx5Bwdv+j7l8H+G97A==", "requires": { - "@swc/core-darwin-arm64": "1.13.0", - "@swc/core-darwin-x64": "1.13.0", - "@swc/core-linux-arm-gnueabihf": "1.13.0", - "@swc/core-linux-arm64-gnu": "1.13.0", - "@swc/core-linux-arm64-musl": "1.13.0", - "@swc/core-linux-x64-gnu": "1.13.0", - "@swc/core-linux-x64-musl": "1.13.0", - "@swc/core-win32-arm64-msvc": "1.13.0", - "@swc/core-win32-ia32-msvc": "1.13.0", - "@swc/core-win32-x64-msvc": "1.13.0", + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/types": "^8.37.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.3" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==" + }, + "picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==" + } + } + }, + "@swc/core": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.2.tgz", + "integrity": "sha512-YWqn+0IKXDhqVLKoac4v2tV6hJqB/wOh8/Br8zjqeqBkKa77Qb0Kw2i7LOFzjFNZbZaPH6AlMGlBwNrxaauaAg==", + "requires": { + "@swc/core-darwin-arm64": "1.13.2", + "@swc/core-darwin-x64": "1.13.2", + "@swc/core-linux-arm-gnueabihf": "1.13.2", + "@swc/core-linux-arm64-gnu": "1.13.2", + "@swc/core-linux-arm64-musl": "1.13.2", + "@swc/core-linux-x64-gnu": "1.13.2", + "@swc/core-linux-x64-musl": "1.13.2", + "@swc/core-win32-arm64-msvc": "1.13.2", + "@swc/core-win32-ia32-msvc": "1.13.2", + "@swc/core-win32-x64-msvc": "1.13.2", "@swc/counter": "^0.1.3", "@swc/types": "^0.1.23" } }, "@swc/core-darwin-arm64": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.0.tgz", - "integrity": "sha512-SkmR9u7MHDu2X8hf7SjZTmsAfQTmel0mi+TJ7AGtufLwGySv6pwQfJ/CIJpcPxYENVqDJAFnDrHaKV8mgA6kxQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.2.tgz", + "integrity": "sha512-44p7ivuLSGFJ15Vly4ivLJjg3ARo4879LtEBAabcHhSZygpmkP8eyjyWxrH3OxkY1eRZSIJe8yRZPFw4kPXFPw==", "optional": true }, "@swc/core-darwin-x64": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.0.tgz", - "integrity": "sha512-15/SyDjXRtFJ09fYHBXUXrj4tpiSpCkjgsF1z3/sSpHH1POWpQUQzxmFyomPQVZ/SsDqP18WGH09Vph4Qriuiw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.2.tgz", + "integrity": "sha512-Lb9EZi7X2XDAVmuUlBm2UvVAgSCbD3qKqDCxSI4jEOddzVOpNCnyZ/xEampdngUIyDDhhJLYU9duC+Mcsv5Y+A==", "optional": true }, "@swc/core-linux-arm-gnueabihf": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.0.tgz", - "integrity": "sha512-AHauVHZQEJI/dCZQg6VYNNQ6HROz8dSOnCSheXzzBw1DGWo77BlcxRP0fF0jaAXM9WNqtCUOY1HiJ9ohkAE61Q==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.2.tgz", + "integrity": "sha512-9TDe/92ee1x57x+0OqL1huG4BeljVx0nWW4QOOxp8CCK67Rpc/HHl2wciJ0Kl9Dxf2NvpNtkPvqj9+BUmM9WVA==", "optional": true }, "@swc/core-linux-arm64-gnu": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.0.tgz", - "integrity": "sha512-qyZmBZF7asF6954/x7yn6R7Bzd45KRG05rK2atIF9J3MTa8az7vubP1Q3BWmmss1j8699DELpbuoJucGuhsNXw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.2.tgz", + "integrity": "sha512-KJUSl56DBk7AWMAIEcU83zl5mg3vlQYhLELhjwRFkGFMvghQvdqQ3zFOYa4TexKA7noBZa3C8fb24rI5sw9Exg==", "optional": true }, "@swc/core-linux-arm64-musl": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.0.tgz", - "integrity": "sha512-whskQCOUlLQT7MjnronpHmyHegBka5ig9JkQvecbqhWzRfdwN+c2xTJs3kQsWy2Vc2f1hcL3D8hGIwY5TwPxMQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.2.tgz", + "integrity": "sha512-teU27iG1oyWpNh9CzcGQ48ClDRt/RCem7mYO7ehd2FY102UeTws2+OzLESS1TS1tEZipq/5xwx3FzbVgiolCiQ==", "optional": true }, "@swc/core-linux-x64-gnu": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.0.tgz", - "integrity": "sha512-51n4P4nv6rblXyH3zCEktvmR9uSAZ7+zbfeby0sxbj8LS/IKuVd7iCwD5dwMj4CxG9Fs+HgjN73dLQF/OerHhg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.2.tgz", + "integrity": "sha512-dRPsyPyqpLD0HMRCRpYALIh4kdOir8pPg4AhNQZLehKowigRd30RcLXGNVZcc31Ua8CiPI4QSgjOIxK+EQe4LQ==", "optional": true }, "@swc/core-linux-x64-musl": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.0.tgz", - "integrity": "sha512-VMqelgvnXs27eQyhDf1S2O2MxSdchIH7c1tkxODRtu9eotcAeniNNgqqLjZ5ML0MGeRk/WpbsAY/GWi7eSpiHw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.2.tgz", + "integrity": "sha512-CCxETW+KkYEQDqz1SYC15YIWYheqFC+PJVOW76Maa/8yu8Biw+HTAcblKf2isrlUtK8RvrQN94v3UXkC2NzCEw==", "optional": true }, "@swc/core-win32-arm64-msvc": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.0.tgz", - "integrity": "sha512-NLJmseWJngWeENgat+O/WB4ptNxtx2X4OfPnSG5a/A4sxcn2E4jq91OPvbeUQwDkH+ZQWKXmbXFzt7Nn661QYA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.2.tgz", + "integrity": "sha512-Wv/QTA6PjyRLlmKcN6AmSI4jwSMRl0VTLGs57PHTqYRwwfwd7y4s2fIPJVBNbAlXd795dOEP6d/bGSQSyhOX3A==", "optional": true }, "@swc/core-win32-ia32-msvc": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.0.tgz", - "integrity": "sha512-UBfwrp0xW37KQGTA08mwrCLIm1ZKy6pXK8IVwou7BvhMgrItRNweTGyUrCnvDLUfyYFuJCmzcEaJ3NudtctD6g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.2.tgz", + "integrity": "sha512-PuCdtNynEkUNbUXX/wsyUC+t4mamIU5y00lT5vJcAvco3/r16Iaxl5UCzhXYaWZSNVZMzPp9qN8NlSL8M5pPxw==", "optional": true }, "@swc/core-win32-x64-msvc": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.0.tgz", - "integrity": "sha512-BAB1P7Z/y2EENsfsPytPnjIyBVRZN2WULY+s3ozW4QkGmYHde6XXG28n0ABTHhcIOmmR2VzM+uaW1x48laSimw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.2.tgz", + "integrity": "sha512-qlmMkFZJus8cYuBURx1a3YAG2G7IW44i+FEYV5/32ylKkzGNAr9tDJSA53XNnNXkAB5EXSPsOz7bn5C3JlEtdQ==", "optional": true }, "@swc/counter": { @@ -12472,10 +13470,42 @@ "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.0.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.14.tgz", - "integrity": "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw==", + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", + "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", "requires": { "undici-types": "~7.8.0" } @@ -12522,15 +13552,15 @@ "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==" }, "@typescript-eslint/eslint-plugin": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz", - "integrity": "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz", + "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==", "requires": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.37.0", - "@typescript-eslint/type-utils": "8.37.0", - "@typescript-eslint/utils": "8.37.0", - "@typescript-eslint/visitor-keys": "8.37.0", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/type-utils": "8.38.0", + "@typescript-eslint/utils": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -12545,68 +13575,68 @@ } }, "@typescript-eslint/parser": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz", - "integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz", + "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", "requires": { - "@typescript-eslint/scope-manager": "8.37.0", - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/typescript-estree": "8.37.0", - "@typescript-eslint/visitor-keys": "8.37.0", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4" } }, "@typescript-eslint/project-service": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.37.0.tgz", - "integrity": "sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz", + "integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==", "requires": { - "@typescript-eslint/tsconfig-utils": "^8.37.0", - "@typescript-eslint/types": "^8.37.0", + "@typescript-eslint/tsconfig-utils": "^8.38.0", + "@typescript-eslint/types": "^8.38.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz", - "integrity": "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz", + "integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==", "requires": { - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/visitor-keys": "8.37.0" + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0" } }, "@typescript-eslint/tsconfig-utils": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz", - "integrity": "sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz", + "integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==", "requires": {} }, "@typescript-eslint/type-utils": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz", - "integrity": "sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz", + "integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==", "requires": { - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/typescript-estree": "8.37.0", - "@typescript-eslint/utils": "8.37.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/utils": "8.38.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" } }, "@typescript-eslint/types": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.37.0.tgz", - "integrity": "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==" + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz", + "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==" }, "@typescript-eslint/typescript-estree": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz", - "integrity": "sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz", + "integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==", "requires": { - "@typescript-eslint/project-service": "8.37.0", - "@typescript-eslint/tsconfig-utils": "8.37.0", - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/visitor-keys": "8.37.0", + "@typescript-eslint/project-service": "8.38.0", + "@typescript-eslint/tsconfig-utils": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -12634,22 +13664,22 @@ } }, "@typescript-eslint/utils": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.37.0.tgz", - "integrity": "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz", + "integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==", "requires": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.37.0", - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/typescript-estree": "8.37.0" + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0" } }, "@typescript-eslint/visitor-keys": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz", - "integrity": "sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz", + "integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==", "requires": { - "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/types": "8.38.0", "eslint-visitor-keys": "^4.2.1" }, "dependencies": { @@ -12929,12 +13959,12 @@ } }, "axios": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", - "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", "requires": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -12957,11 +13987,11 @@ } }, "babel-jest": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.4.tgz", - "integrity": "sha512-UjG2j7sAOqsp2Xua1mS/e+ekddkSu3wpf4nZUSvXNHuVWdaOUXQ77+uyjJLDE9i0atm5x4kds8K9yb5lRsRtcA==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.5.tgz", + "integrity": "sha512-mRijnKimhGDMsizTvBTWotwNpzrkHr+VvZUQBof2AufXKB8NXrL1W69TG20EvOz7aevx6FTJIaBuBkYxS8zolg==", "requires": { - "@jest/transform": "30.0.4", + "@jest/transform": "30.0.5", "@types/babel__core": "^7.20.5", "babel-plugin-istanbul": "^7.0.0", "babel-preset-jest": "30.0.1", @@ -13004,9 +14034,9 @@ } }, "babel-preset-current-node-syntax": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", - "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.1.tgz", + "integrity": "sha512-23fWKohMTvS5s0wwJKycOe0dBdCwQ6+iiLaNR9zy8P13mtFRFM9qLLX6HJX5DL2pi/FNDf3fCQHM4FIMoHH/7w==", "requires": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", @@ -13279,20 +14309,20 @@ "integrity": "sha512-syedaZ9cPe7r3hoQA9twWYKu5AIyCswN5+szkmPBe9ccdLrj4bYaCnLVPTLd2kgVRc7+zoX4tyPgRnFKCj5YjQ==" }, "cheerio": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.0.tgz", - "integrity": "sha512-+0hMx9eYhJvWbgpKV9hN7jg0JcwydpopZE4hgi+KvQtByZXPp04NiCWU0LzcAbP63abZckIHkTQaXVF52mX3xQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", + "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==", "requires": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", - "encoding-sniffer": "^0.2.0", + "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.0.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", - "undici": "^7.10.0", + "undici": "^7.12.0", "whatwg-mimetype": "^4.0.0" } }, @@ -13554,11 +14584,12 @@ "integrity": "sha512-/f6gpQuxDaqXu+1kwQYSckUglPaOrHdbIlBAu0YuW8/Cdb45XwXYNUBXg3r/9Mo6n540Kn/smKcZWko5x99KrQ==" }, "cross-env": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", - "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.0.0.tgz", + "integrity": "sha512-aU8qlEK/nHYtVuN4p7UQgAwVljzMg8hB4YK5ThRqD2l/ziSnryncPNn7bMLt5cFYsKVKBh8HqLqyCoTupEUu7Q==", "requires": { - "cross-spawn": "^7.0.1" + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" } }, "cross-spawn": { @@ -13739,9 +14770,9 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, "electron-to-chromium": { - "version": "1.5.185", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.185.tgz", - "integrity": "sha512-dYOZfUk57hSMPePoIQ1fZWl1Fkj+OshhEVuPacNKWzC1efe56OsHY3l/jCfiAgIICOU3VgOIdoq7ahg7r7n6MQ==" + "version": "1.5.192", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.192.tgz", + "integrity": "sha512-rP8Ez0w7UNw/9j5eSXCe10o1g/8B1P5SM90PCCMVkIRQn2R0LEHWz4Eh9RnxkniuDe1W0cTSOB3MLlkTGDcuCg==" }, "emittery": { "version": "0.13.1", @@ -13902,9 +14933,9 @@ } }, "eslint": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", - "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", + "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -13912,8 +14943,8 @@ "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.31.0", - "@eslint/plugin-kit": "^0.3.1", + "@eslint/js": "9.32.0", + "@eslint/plugin-kit": "^0.3.4", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -14084,16 +15115,16 @@ "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==" }, "expect": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.4.tgz", - "integrity": "sha512-dDLGjnP2cKbEppxVICxI/Uf4YemmGMPNy0QytCbfafbpYk9AFQsxb8Uyrxii0RPK7FWgLGlSem+07WirwS3cFQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.5.tgz", + "integrity": "sha512-P0te2pt+hHI5qLJkIR+iMvS+lYUZml8rKKsohVHAGY+uClp9XVbdyYNJOIjSRpHVp8s8YqxJCiHUkSYZGr8rtQ==", "requires": { - "@jest/expect-utils": "30.0.4", + "@jest/expect-utils": "30.0.5", "@jest/get-type": "30.0.1", - "jest-matcher-utils": "30.0.4", - "jest-message-util": "30.0.2", - "jest-mock": "30.0.2", - "jest-util": "30.0.2" + "jest-matcher-utils": "30.0.5", + "jest-message-util": "30.0.5", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" } }, "external-editor": { @@ -14599,16 +15630,16 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, "inquirer": { - "version": "12.7.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.7.0.tgz", - "integrity": "sha512-KKFRc++IONSyE2UYw9CJ1V0IWx5yQKomwB+pp3cWomWs+v2+ZsG11G2OVfAjFS6WWCppKw+RfKmpqGfSzD5QBQ==", + "version": "12.9.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.9.0.tgz", + "integrity": "sha512-LlFVmvWVCun7uEgPB3vups9NzBrjJn48kRNtFGw3xU1H5UXExTEz/oF1JGLaB0fvlkUB+W6JfgLcSEaSdH7RPA==", "requires": { - "@inquirer/core": "^10.1.14", - "@inquirer/prompts": "^7.6.0", - "@inquirer/type": "^3.0.7", + "@inquirer/core": "^10.1.15", + "@inquirer/prompts": "^7.8.0", + "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2", "mute-stream": "^2.0.0", - "run-async": "^4.0.4", + "run-async": "^4.0.5", "rxjs": "^7.8.2" }, "dependencies": { @@ -14811,53 +15842,108 @@ } }, "jest": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.0.4.tgz", - "integrity": "sha512-9QE0RS4WwTj/TtTC4h/eFVmFAhGNVerSB9XpJh8sqaXlP73ILcPcZ7JWjjEtJJe2m8QyBLKKfPQuK+3F+Xij/g==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.0.5.tgz", + "integrity": "sha512-y2mfcJywuTUkvLm2Lp1/pFX8kTgMO5yyQGq/Sk/n2mN7XWYp4JsCZ/QXW34M8YScgk8bPZlREH04f6blPnoHnQ==", "requires": { - "@jest/core": "30.0.4", - "@jest/types": "30.0.1", + "@jest/core": "30.0.5", + "@jest/types": "30.0.5", "import-local": "^3.2.0", - "jest-cli": "30.0.4" + "jest-cli": "30.0.5" + }, + "dependencies": { + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } } }, "jest-changed-files": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.2.tgz", - "integrity": "sha512-Ius/iRST9FKfJI+I+kpiDh8JuUlAISnRszF9ixZDIqJF17FckH5sOzKC8a0wd0+D+8em5ADRHA5V5MnfeDk2WA==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.5.tgz", + "integrity": "sha512-bGl2Ntdx0eAwXuGpdLdVYVr5YQHnSZlQ0y9HVDu565lCUAe9sj6JOtBbMmBBikGIegne9piDDIOeiLVoqTkz4A==", "requires": { "execa": "^5.1.1", - "jest-util": "30.0.2", + "jest-util": "30.0.5", "p-limit": "^3.1.0" } }, "jest-circus": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.0.4.tgz", - "integrity": "sha512-o6UNVfbXbmzjYgmVPtSQrr5xFZCtkDZGdTlptYvGFSN80RuOOlTe73djvMrs+QAuSERZWcHBNIOMH+OEqvjWuw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.0.5.tgz", + "integrity": "sha512-h/sjXEs4GS+NFFfqBDYT7y5Msfxh04EwWLhQi0F8kuWpe+J/7tICSlswU8qvBqumR3kFgHbfu7vU6qruWWBPug==", "requires": { - "@jest/environment": "30.0.4", - "@jest/expect": "30.0.4", - "@jest/test-result": "30.0.4", - "@jest/types": "30.0.1", + "@jest/environment": "30.0.5", + "@jest/expect": "30.0.5", + "@jest/test-result": "30.0.5", + "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "co": "^4.6.0", "dedent": "^1.6.0", "is-generator-fn": "^2.1.0", - "jest-each": "30.0.2", - "jest-matcher-utils": "30.0.4", - "jest-message-util": "30.0.2", - "jest-runtime": "30.0.4", - "jest-snapshot": "30.0.4", - "jest-util": "30.0.2", + "jest-each": "30.0.5", + "jest-matcher-utils": "30.0.5", + "jest-message-util": "30.0.5", + "jest-runtime": "30.0.5", + "jest-snapshot": "30.0.5", + "jest-util": "30.0.5", "p-limit": "^3.1.0", - "pretty-format": "30.0.2", + "pretty-format": "30.0.5", "pure-rand": "^7.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" }, "dependencies": { + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -14870,22 +15956,44 @@ } }, "jest-cli": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.0.4.tgz", - "integrity": "sha512-3dOrP3zqCWBkjoVG1zjYJpD9143N9GUCbwaF2pFF5brnIgRLHmKcCIw+83BvF1LxggfMWBA0gxkn6RuQVuRhIQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.0.5.tgz", + "integrity": "sha512-Sa45PGMkBZzF94HMrlX4kUyPOwUpdZasaliKN3mifvDmkhLYqLLg8HQTzn6gq7vJGahFYMQjXgyJWfYImKZzOw==", "requires": { - "@jest/core": "30.0.4", - "@jest/test-result": "30.0.4", - "@jest/types": "30.0.1", + "@jest/core": "30.0.5", + "@jest/test-result": "30.0.5", + "@jest/types": "30.0.5", "chalk": "^4.1.2", "exit-x": "^0.2.2", "import-local": "^3.2.0", - "jest-config": "30.0.4", - "jest-util": "30.0.2", - "jest-validate": "30.0.2", + "jest-config": "30.0.5", + "jest-util": "30.0.5", + "jest-validate": "30.0.5", "yargs": "^17.7.2" }, "dependencies": { + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -14898,36 +16006,58 @@ } }, "jest-config": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.0.4.tgz", - "integrity": "sha512-3dzbO6sh34thAGEjJIW0fgT0GA0EVlkski6ZzMcbW6dzhenylXAE/Mj2MI4HonroWbkKc6wU6bLVQ8dvBSZ9lA==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.0.5.tgz", + "integrity": "sha512-aIVh+JNOOpzUgzUnPn5FLtyVnqc3TQHVMupYtyeURSb//iLColiMIR8TxCIDKyx9ZgjKnXGucuW68hCxgbrwmA==", "requires": { "@babel/core": "^7.27.4", "@jest/get-type": "30.0.1", "@jest/pattern": "30.0.1", - "@jest/test-sequencer": "30.0.4", - "@jest/types": "30.0.1", - "babel-jest": "30.0.4", + "@jest/test-sequencer": "30.0.5", + "@jest/types": "30.0.5", + "babel-jest": "30.0.5", "chalk": "^4.1.2", "ci-info": "^4.2.0", "deepmerge": "^4.3.1", "glob": "^10.3.10", "graceful-fs": "^4.2.11", - "jest-circus": "30.0.4", + "jest-circus": "30.0.5", "jest-docblock": "30.0.1", - "jest-environment-node": "30.0.4", + "jest-environment-node": "30.0.5", "jest-regex-util": "30.0.1", - "jest-resolve": "30.0.2", - "jest-runner": "30.0.4", - "jest-util": "30.0.2", - "jest-validate": "30.0.2", + "jest-resolve": "30.0.5", + "jest-runner": "30.0.5", + "jest-util": "30.0.5", + "jest-validate": "30.0.5", "micromatch": "^4.0.8", "parse-json": "^5.2.0", - "pretty-format": "30.0.2", + "pretty-format": "30.0.5", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, "dependencies": { + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, "brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -14992,14 +16122,14 @@ } }, "jest-diff": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.4.tgz", - "integrity": "sha512-TSjceIf6797jyd+R64NXqicttROD+Qf98fex7CowmlSn7f8+En0da1Dglwr1AXxDtVizoxXYZBlUQwNhoOXkNw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.5.tgz", + "integrity": "sha512-1UIqE9PoEKaHcIKvq2vbibrCog4Y8G0zmOxgQUVEiTqwR5hJVMCoDsN1vFvI5JvwD37hjueZ1C4l2FyGnfpE0A==", "requires": { "@jest/diff-sequences": "30.0.1", "@jest/get-type": "30.0.1", "chalk": "^4.1.2", - "pretty-format": "30.0.2" + "pretty-format": "30.0.5" }, "dependencies": { "chalk": { @@ -15022,17 +16152,39 @@ } }, "jest-each": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.0.2.tgz", - "integrity": "sha512-ZFRsTpe5FUWFQ9cWTMguCaiA6kkW5whccPy9JjD1ezxh+mJeqmz8naL8Fl/oSbNJv3rgB0x87WBIkA5CObIUZQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.0.5.tgz", + "integrity": "sha512-dKjRsx1uZ96TVyejD3/aAWcNKy6ajMaN531CwWIsrazIqIoXI9TnnpPlkrEYku/8rkS3dh2rbH+kMOyiEIv0xQ==", "requires": { "@jest/get-type": "30.0.1", - "@jest/types": "30.0.1", + "@jest/types": "30.0.5", "chalk": "^4.1.2", - "jest-util": "30.0.2", - "pretty-format": "30.0.2" + "jest-util": "30.0.5", + "pretty-format": "30.0.5" }, "dependencies": { + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -15045,55 +16197,121 @@ } }, "jest-environment-node": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.0.4.tgz", - "integrity": "sha512-p+rLEzC2eThXqiNh9GHHTC0OW5Ca4ZfcURp7scPjYBcmgpR9HG6750716GuUipYf2AcThU3k20B31USuiaaIEg==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.0.5.tgz", + "integrity": "sha512-ppYizXdLMSvciGsRsMEnv/5EFpvOdXBaXRBzFUDPWrsfmog4kYrOGWXarLllz6AXan6ZAA/kYokgDWuos1IKDA==", "requires": { - "@jest/environment": "30.0.4", - "@jest/fake-timers": "30.0.4", - "@jest/types": "30.0.1", + "@jest/environment": "30.0.5", + "@jest/fake-timers": "30.0.5", + "@jest/types": "30.0.5", "@types/node": "*", - "jest-mock": "30.0.2", - "jest-util": "30.0.2", - "jest-validate": "30.0.2" + "jest-mock": "30.0.5", + "jest-util": "30.0.5", + "jest-validate": "30.0.5" + }, + "dependencies": { + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } } }, "jest-haste-map": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.2.tgz", - "integrity": "sha512-telJBKpNLeCb4MaX+I5k496556Y2FiKR/QLZc0+MGBYl4k3OO0472drlV2LUe7c1Glng5HuAu+5GLYp//GpdOQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.5.tgz", + "integrity": "sha512-dkmlWNlsTSR0nH3nRfW5BKbqHefLZv0/6LCccG0xFCTWcJu8TuEwG+5Cm75iBfjVoockmO6J35o5gxtFSn5xeg==", "requires": { - "@jest/types": "30.0.1", + "@jest/types": "30.0.5", "@types/node": "*", "anymatch": "^3.1.3", "fb-watchman": "^2.0.2", "fsevents": "^2.3.3", "graceful-fs": "^4.2.11", "jest-regex-util": "30.0.1", - "jest-util": "30.0.2", - "jest-worker": "30.0.2", + "jest-util": "30.0.5", + "jest-worker": "30.0.5", "micromatch": "^4.0.8", "walker": "^1.0.8" + }, + "dependencies": { + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } } }, "jest-leak-detector": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.0.2.tgz", - "integrity": "sha512-U66sRrAYdALq+2qtKffBLDWsQ/XoNNs2Lcr83sc9lvE/hEpNafJlq2lXCPUBMNqamMECNxSIekLfe69qg4KMIQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.0.5.tgz", + "integrity": "sha512-3Uxr5uP8jmHMcsOtYMRB/zf1gXN3yUIc+iPorhNETG54gErFIiUhLvyY/OggYpSMOEYqsmRxmuU4ZOoX5jpRFg==", "requires": { "@jest/get-type": "30.0.1", - "pretty-format": "30.0.2" + "pretty-format": "30.0.5" } }, "jest-matcher-utils": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.4.tgz", - "integrity": "sha512-ubCewJ54YzeAZ2JeHHGVoU+eDIpQFsfPQs0xURPWoNiO42LGJ+QGgfSf+hFIRplkZDkhH5MOvuxHKXRTUU3dUQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.5.tgz", + "integrity": "sha512-uQgGWt7GOrRLP1P7IwNWwK1WAQbq+m//ZY0yXygyfWp0rJlksMSLQAA4wYQC3b6wl3zfnchyTx+k3HZ5aPtCbQ==", "requires": { "@jest/get-type": "30.0.1", "chalk": "^4.1.2", - "jest-diff": "30.0.4", - "pretty-format": "30.0.2" + "jest-diff": "30.0.5", + "pretty-format": "30.0.5" }, "dependencies": { "chalk": { @@ -15108,21 +16326,43 @@ } }, "jest-message-util": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.2.tgz", - "integrity": "sha512-vXywcxmr0SsKXF/bAD7t7nMamRvPuJkras00gqYeB1V0WllxZrbZ0paRr3XqpFU2sYYjD0qAaG2fRyn/CGZ0aw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.5.tgz", + "integrity": "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA==", "requires": { "@babel/code-frame": "^7.27.1", - "@jest/types": "30.0.1", + "@jest/types": "30.0.5", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "micromatch": "^4.0.8", - "pretty-format": "30.0.2", + "pretty-format": "30.0.5", "slash": "^3.0.0", "stack-utils": "^2.0.6" }, "dependencies": { + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -15135,13 +16375,46 @@ } }, "jest-mock": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.2.tgz", - "integrity": "sha512-PnZOHmqup/9cT/y+pXIVbbi8ID6U1XHRmbvR7MvUy4SLqhCbwpkmXhLbsWbGewHrV5x/1bF7YDjs+x24/QSvFA==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", + "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", "requires": { - "@jest/types": "30.0.1", + "@jest/types": "30.0.5", "@types/node": "*", - "jest-util": "30.0.2" + "jest-util": "30.0.5" + }, + "dependencies": { + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } } }, "jest-offline": { @@ -15164,16 +16437,16 @@ "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==" }, "jest-resolve": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.0.2.tgz", - "integrity": "sha512-q/XT0XQvRemykZsvRopbG6FQUT6/ra+XV6rPijyjT6D0msOyCvR2A5PlWZLd+fH0U8XWKZfDiAgrUNDNX2BkCw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.0.5.tgz", + "integrity": "sha512-d+DjBQ1tIhdz91B79mywH5yYu76bZuE96sSbxj8MkjWVx5WNdt1deEFRONVL4UkKLSrAbMkdhb24XN691yDRHg==", "requires": { "chalk": "^4.1.2", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.2", + "jest-haste-map": "30.0.5", "jest-pnp-resolver": "^1.2.3", - "jest-util": "30.0.2", - "jest-validate": "30.0.2", + "jest-util": "30.0.5", + "jest-validate": "30.0.5", "slash": "^3.0.0", "unrs-resolver": "^1.7.11" }, @@ -15190,43 +16463,65 @@ } }, "jest-resolve-dependencies": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.0.4.tgz", - "integrity": "sha512-EQBYow19B/hKr4gUTn+l8Z+YLlP2X0IoPyp0UydOtrcPbIOYzJ8LKdFd+yrbwztPQvmlBFUwGPPEzHH1bAvFAw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.0.5.tgz", + "integrity": "sha512-/xMvBR4MpwkrHW4ikZIWRttBBRZgWK4d6xt3xW1iRDSKt4tXzYkMkyPfBnSCgv96cpkrctfXs6gexeqMYqdEpw==", "requires": { "jest-regex-util": "30.0.1", - "jest-snapshot": "30.0.4" + "jest-snapshot": "30.0.5" } }, "jest-runner": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.0.4.tgz", - "integrity": "sha512-mxY0vTAEsowJwvFJo5pVivbCpuu6dgdXRmt3v3MXjBxFly7/lTk3Td0PaMyGOeNQUFmSuGEsGYqhbn7PA9OekQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.0.5.tgz", + "integrity": "sha512-JcCOucZmgp+YuGgLAXHNy7ualBx4wYSgJVWrYMRBnb79j9PD0Jxh0EHvR5Cx/r0Ce+ZBC4hCdz2AzFFLl9hCiw==", "requires": { - "@jest/console": "30.0.4", - "@jest/environment": "30.0.4", - "@jest/test-result": "30.0.4", - "@jest/transform": "30.0.4", - "@jest/types": "30.0.1", + "@jest/console": "30.0.5", + "@jest/environment": "30.0.5", + "@jest/test-result": "30.0.5", + "@jest/transform": "30.0.5", + "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "emittery": "^0.13.1", "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", "jest-docblock": "30.0.1", - "jest-environment-node": "30.0.4", - "jest-haste-map": "30.0.2", - "jest-leak-detector": "30.0.2", - "jest-message-util": "30.0.2", - "jest-resolve": "30.0.2", - "jest-runtime": "30.0.4", - "jest-util": "30.0.2", - "jest-watcher": "30.0.4", - "jest-worker": "30.0.2", + "jest-environment-node": "30.0.5", + "jest-haste-map": "30.0.5", + "jest-leak-detector": "30.0.5", + "jest-message-util": "30.0.5", + "jest-resolve": "30.0.5", + "jest-runtime": "30.0.5", + "jest-util": "30.0.5", + "jest-watcher": "30.0.5", + "jest-worker": "30.0.5", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, "dependencies": { + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -15239,34 +16534,56 @@ } }, "jest-runtime": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.0.4.tgz", - "integrity": "sha512-tUQrZ8+IzoZYIHoPDQEB4jZoPyzBjLjq7sk0KVyd5UPRjRDOsN7o6UlvaGF8ddpGsjznl9PW+KRgWqCNO+Hn7w==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.0.5.tgz", + "integrity": "sha512-7oySNDkqpe4xpX5PPiJTe5vEa+Ak/NnNz2bGYZrA1ftG3RL3EFlHaUkA1Cjx+R8IhK0Vg43RML5mJedGTPNz3A==", "requires": { - "@jest/environment": "30.0.4", - "@jest/fake-timers": "30.0.4", - "@jest/globals": "30.0.4", + "@jest/environment": "30.0.5", + "@jest/fake-timers": "30.0.5", + "@jest/globals": "30.0.5", "@jest/source-map": "30.0.1", - "@jest/test-result": "30.0.4", - "@jest/transform": "30.0.4", - "@jest/types": "30.0.1", + "@jest/test-result": "30.0.5", + "@jest/transform": "30.0.5", + "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "cjs-module-lexer": "^2.1.0", "collect-v8-coverage": "^1.0.2", "glob": "^10.3.10", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.2", - "jest-message-util": "30.0.2", - "jest-mock": "30.0.2", + "jest-haste-map": "30.0.5", + "jest-message-util": "30.0.5", + "jest-mock": "30.0.5", "jest-regex-util": "30.0.1", - "jest-resolve": "30.0.2", - "jest-snapshot": "30.0.4", - "jest-util": "30.0.2", + "jest-resolve": "30.0.5", + "jest-snapshot": "30.0.5", + "jest-util": "30.0.5", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, "dependencies": { + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, "brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -15331,33 +16648,55 @@ } }, "jest-snapshot": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.4.tgz", - "integrity": "sha512-S/8hmSkeUib8WRUq9pWEb5zMfsOjiYWDWzFzKnjX7eDyKKgimsu9hcmsUEg8a7dPAw8s/FacxsXquq71pDgPjQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.5.tgz", + "integrity": "sha512-T00dWU/Ek3LqTp4+DcW6PraVxjk28WY5Ua/s+3zUKSERZSNyxTqhDXCWKG5p2HAJ+crVQ3WJ2P9YVHpj1tkW+g==", "requires": { "@babel/core": "^7.27.4", "@babel/generator": "^7.27.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.0.4", + "@jest/expect-utils": "30.0.5", "@jest/get-type": "30.0.1", - "@jest/snapshot-utils": "30.0.4", - "@jest/transform": "30.0.4", - "@jest/types": "30.0.1", + "@jest/snapshot-utils": "30.0.5", + "@jest/transform": "30.0.5", + "@jest/types": "30.0.5", "babel-preset-current-node-syntax": "^1.1.0", "chalk": "^4.1.2", - "expect": "30.0.4", + "expect": "30.0.5", "graceful-fs": "^4.2.11", - "jest-diff": "30.0.4", - "jest-matcher-utils": "30.0.4", - "jest-message-util": "30.0.2", - "jest-util": "30.0.2", - "pretty-format": "30.0.2", + "jest-diff": "30.0.5", + "jest-matcher-utils": "30.0.5", + "jest-message-util": "30.0.5", + "jest-util": "30.0.5", + "pretty-format": "30.0.5", "semver": "^7.7.2", "synckit": "^0.11.8" }, "dependencies": { + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -15370,11 +16709,11 @@ } }, "jest-util": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.2.tgz", - "integrity": "sha512-8IyqfKS4MqprBuUpZNlFB5l+WFehc8bfCe1HSZFHzft2mOuND8Cvi9r1musli+u6F3TqanCZ/Ik4H4pXUolZIg==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", + "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", "requires": { - "@jest/types": "30.0.1", + "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", @@ -15382,6 +16721,28 @@ "picomatch": "^4.0.2" }, "dependencies": { + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -15392,25 +16753,47 @@ } }, "picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==" + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==" } } }, "jest-validate": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.0.2.tgz", - "integrity": "sha512-noOvul+SFER4RIvNAwGn6nmV2fXqBq67j+hKGHKGFCmK4ks/Iy1FSrqQNBLGKlu4ZZIRL6Kg1U72N1nxuRCrGQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.0.5.tgz", + "integrity": "sha512-ouTm6VFHaS2boyl+k4u+Qip4TSH7Uld5tyD8psQ8abGgt2uYYB8VwVfAHWHjHc0NWmGGbwO5h0sCPOGHHevefw==", "requires": { "@jest/get-type": "30.0.1", - "@jest/types": "30.0.1", + "@jest/types": "30.0.5", "camelcase": "^6.3.0", "chalk": "^4.1.2", "leven": "^3.1.0", - "pretty-format": "30.0.2" + "pretty-format": "30.0.5" }, "dependencies": { + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, "camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -15428,20 +16811,42 @@ } }, "jest-watcher": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.0.4.tgz", - "integrity": "sha512-YESbdHDs7aQOCSSKffG8jXqOKFqw4q4YqR+wHYpR5GWEQioGvL0BfbcjvKIvPEM0XGfsfJrka7jJz3Cc3gI4VQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.0.5.tgz", + "integrity": "sha512-z9slj/0vOwBDBjN3L4z4ZYaA+pG56d6p3kTUhFRYGvXbXMWhXmb/FIxREZCD06DYUwDKKnj2T80+Pb71CQ0KEg==", "requires": { - "@jest/test-result": "30.0.4", - "@jest/types": "30.0.1", + "@jest/test-result": "30.0.5", + "@jest/types": "30.0.5", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "emittery": "^0.13.1", - "jest-util": "30.0.2", + "jest-util": "30.0.5", "string-length": "^4.0.2" }, "dependencies": { + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -15454,13 +16859,13 @@ } }, "jest-worker": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.2.tgz", - "integrity": "sha512-RN1eQmx7qSLFA+o9pfJKlqViwL5wt+OL3Vff/A+/cPsmuw7NPwfgl33AP+/agRmHzPOFgXviRycR9kYwlcRQXg==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.5.tgz", + "integrity": "sha512-ojRXsWzEP16NdUuBw/4H/zkZdHOa7MMYCk4E430l+8fELeLg/mqmMlRhjL7UNZvQrDmnovWZV4DxX03fZF48fQ==", "requires": { "@types/node": "*", "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.0.2", + "jest-util": "30.0.5", "merge-stream": "^2.0.0", "supports-color": "^8.1.1" }, @@ -15675,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", @@ -15818,9 +17243,9 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" }, "napi-postinstall": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.0.tgz", - "integrity": "sha512-M7NqKyhODKV1gRLdkwE7pDsZP2/SC2a2vHkOYh9MCpKMbWVfyVfUw5MaH83Fv6XMjxr5jryUp3IDDL9rlxsTeA==" + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.2.tgz", + "integrity": "sha512-tWVJxJHmBWLy69PvO96TZMZDrzmw5KeiZBz3RHmiM2XZ9grBJ2WgMAFVVg25nqp3ZjTFUs2Ftw1JhscL3Teliw==" }, "natural-compare": { "version": "1.4.0", @@ -15996,21 +17421,6 @@ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==" }, - "oxlint": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.7.0.tgz", - "integrity": "sha512-krJN1fIRhs3xK1FyVyPtYIV9tkT4WDoIwI7eiMEKBuCjxqjQt5ZemQm1htPvHqNDOaWFRFt4btcwFdU8bbwgvA==", - "requires": { - "@oxlint/darwin-arm64": "1.7.0", - "@oxlint/darwin-x64": "1.7.0", - "@oxlint/linux-arm64-gnu": "1.7.0", - "@oxlint/linux-arm64-musl": "1.7.0", - "@oxlint/linux-x64-gnu": "1.7.0", - "@oxlint/linux-x64-musl": "1.7.0", - "@oxlint/win32-arm64": "1.7.0", - "@oxlint/win32-x64": "1.7.0" - } - }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -16451,21 +17861,24 @@ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" }, - "prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==" - }, "pretty-format": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz", - "integrity": "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", "requires": { - "@jest/schemas": "30.0.1", + "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" }, "dependencies": { + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, "ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", @@ -16653,13 +18066,9 @@ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==" }, "run-async": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.4.tgz", - "integrity": "sha512-2cgeRHnV11lSXBEhq7sN7a5UVjTKm9JTb9x8ApIT//16D7QL96AgnNeWSGoB4gIHc0iYw/Ha0Z+waBaCYZVNhg==", - "requires": { - "oxlint": "^1.2.0", - "prettier": "^3.5.3" - } + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.5.tgz", + "integrity": "sha512-oN9GTgxUNDBumHTTDmQ8dep6VIJbgj9S3dPP+9XylVLIK4xB9XTXtKWROd5pnhdXR9k0EgO1JRcNh0T+Ny2FsA==" }, "run-parallel": { "version": "1.2.0", @@ -17079,11 +18488,11 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" }, "synckit": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz", - "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==", + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", "requires": { - "@pkgr/core": "^0.2.4" + "@pkgr/core": "^0.2.9" } }, "systeminformation": { @@ -17257,9 +18666,9 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==" }, "undici": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.10.0.tgz", - "integrity": "sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==" + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.12.0.tgz", + "integrity": "sha512-GrKEsc3ughskmGA9jevVlIOPMiiAHJ4OFUtaAH+NhfTUSiZ1wMPIQqQvAJUrJspFXJt3EBWgpAeoHEDVT1IBug==" }, "undici-types": { "version": "7.8.0", @@ -17354,6 +18763,11 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==" + }, "uzip-module": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/uzip-module/-/uzip-module-1.0.3.tgz", diff --git a/package.json b/package.json index 78a1dda9..c8bed9f7 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "dependencies": { "@alex_neo/jest-expect-message": "^1.0.5", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.30.0", + "@eslint/js": "^9.32.0", "@freearhey/chronos": "^0.0.1", "@freearhey/core": "^0.10.2", "@freearhey/search-js": "^0.1.2", @@ -46,33 +46,37 @@ "@octokit/core": "^7.0.3", "@octokit/plugin-paginate-rest": "^13.1.1", "@octokit/plugin-rest-endpoint-methods": "^16.0.0", - "@swc/core": "^1.13.0", + "@stylistic/eslint-plugin": "^5.2.2", + "@swc/core": "^1.13.2", "@swc/jest": "^0.2.39", "@types/cli-progress": "^3.11.6", "@types/fs-extra": "^11.0.4", "@types/inquirer": "^9.0.8", "@types/jest": "^30.0.0", "@types/langs": "^2.0.5", - "@types/lodash": "^4.17.19", - "@types/node": "^24.0.14", + "@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", - "@typescript-eslint/eslint-plugin": "^8.35.0", - "@typescript-eslint/parser": "^8.35.0", - "axios": "^1.10.0", + "@typescript-eslint/eslint-plugin": "^8.38.0", + "@typescript-eslint/parser": "^8.38.0", + "axios": "^1.11.0", "axios-cookiejar-support": "^6.0.4", "chalk": "^5.4.1", - "cheerio": "^1.1.0", + "cheerio": "^1.1.2", "cli-progress": "^3.12.0", "commander": "^14.0.0", "consola": "^3.4.2", - "cross-env": "^7.0.3", + "cross-env": "^10.0.0", "csv-parser": "^3.2.0", "cwait": "^1.1.2", "dayjs": "^1.11.13", "epg-grabber": "^0.41.0", "epg-parser": "^0.3.1", - "eslint": "^9.30.0", + "eslint": "^9.32.0", "eslint-config-prettier": "^10.1.8", "form-data": "^4.0.4", "fs-extra": "^11.3.0", @@ -80,12 +84,15 @@ "globals": "^16.3.0", "husky": "^9.1.7", "iconv-lite": "^0.6.3", - "inquirer": "^12.7.0", - "jest": "^30.0.3", + "inquirer": "^12.8.2", + "jest": "^30.0.5", "jest-offline": "^1.0.1", "langs": "^2.0.0", "libxml2-wasm": "^0.5.0", - "lodash": "^4.17.21", + "lodash.orderby": "^4.6.0", + "lodash.sortby": "^4.7.0", + "lodash.startcase": "^4.4.0", + "lodash.uniqby": "^4.7.0", "luxon": "^3.7.1", "mockdate": "^3.0.5", "nedb-promises": "^6.2.3", @@ -110,6 +117,7 @@ "tsx": "^4.20.3", "typescript": "^5.8.3", "unzipit": "^1.4.3", + "uuid": "^11.1.0", "wildcard-match": "^5.1.4" } } 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/edit.ts b/scripts/commands/channels/edit.ts index 4a5e714a..3618fbff 100644 --- a/scripts/commands/channels/edit.ts +++ b/scripts/commands/channels/edit.ts @@ -1,216 +1,216 @@ -import { Storage, Collection, Logger, Dictionary } from '@freearhey/core' -import type { DataProcessorData } from '../../types/dataProcessor' -import type { DataLoaderData } from '../../types/dataLoader' -import { ChannelSearchableData } from '../../types/channel' -import { Channel, ChannelList, Feed } from '../../models' -import { DataProcessor, DataLoader } from '../../core' -import { select, input } from '@inquirer/prompts' -import { ChannelsParser } from '../../core' -import { DATA_DIR } from '../../constants' -import nodeCleanup from 'node-cleanup' -import sjs from '@freearhey/search-js' -import epgGrabber from 'epg-grabber' -import { Command } from 'commander' -import readline from 'readline' - -type ChoiceValue = { type: string; value?: Feed | Channel } -type Choice = { name: string; short?: string; value: ChoiceValue; default?: boolean } - -if (process.platform === 'win32') { - readline - .createInterface({ - input: process.stdin, - output: process.stdout - }) - .on('SIGINT', function () { - process.emit('SIGINT') - }) -} - -const program = new Command() - -program.argument('', 'Path to *.channels.xml file to edit').parse(process.argv) - -const filepath = program.args[0] -const logger = new Logger() -const storage = new Storage() -let channelList = new ChannelList({ channels: [] }) - -main(filepath) -nodeCleanup(() => { - save(filepath, channelList) -}) - -export default async function main(filepath: string) { - if (!(await storage.exists(filepath))) { - throw new Error(`File "${filepath}" does not exists`) - } - - logger.info('loading data from api...') - const processor = new DataProcessor() - const dataStorage = new Storage(DATA_DIR) - const loader = new DataLoader({ storage: dataStorage }) - const data: DataLoaderData = await loader.load() - const { channels, channelsKeyById, feedsGroupedByChannelId }: DataProcessorData = - processor.process(data) - - logger.info('loading channels...') - const parser = new ChannelsParser({ storage }) - channelList = await parser.parse(filepath) - const parsedChannelsWithoutId = channelList.channels.filter( - (channel: epgGrabber.Channel) => !channel.xmltv_id - ) - - logger.info( - `found ${channelList.channels.count()} channels (including ${parsedChannelsWithoutId.count()} without ID)` - ) - - logger.info('creating search index...') - const items = channels.map((channel: Channel) => channel.getSearchable()).all() - const searchIndex = sjs.createIndex(items, { - searchable: ['name', 'altNames', 'guideNames', 'streamNames', 'feedFullNames'] - }) - - logger.info('starting...\n') - - for (const channel of parsedChannelsWithoutId.all()) { - try { - channel.xmltv_id = await selectChannel( - channel, - searchIndex, - feedsGroupedByChannelId, - channelsKeyById - ) - } catch (err) { - logger.info(err.message) - break - } - } - - parsedChannelsWithoutId.forEach((channel: epgGrabber.Channel) => { - if (channel.xmltv_id === '-') { - channel.xmltv_id = '' - } - }) -} - -async function selectChannel( - channel: epgGrabber.Channel, - searchIndex, - feedsGroupedByChannelId: Dictionary, - channelsKeyById: Dictionary -): Promise { - const query = escapeRegex(channel.name) - const similarChannels = searchIndex - .search(query) - .map((item: ChannelSearchableData) => channelsKeyById.get(item.id)) - - const selected: ChoiceValue = await select({ - message: `Select channel ID for "${channel.name}" (${channel.site_id}):`, - choices: getChannelChoises(new Collection(similarChannels)), - pageSize: 10 - }) - - switch (selected.type) { - case 'skip': - return '-' - case 'type': { - const typedChannelId = await input({ message: ' Channel ID:' }) - if (!typedChannelId) return '' - const selectedFeedId = await selectFeed(typedChannelId, feedsGroupedByChannelId) - if (selectedFeedId === '-') return typedChannelId - return [typedChannelId, selectedFeedId].join('@') - } - case 'channel': { - const selectedChannel = selected.value - if (!selectedChannel) return '' - const selectedFeedId = await selectFeed(selectedChannel.id || '', feedsGroupedByChannelId) - if (selectedFeedId === '-') return selectedChannel.id || '' - return [selectedChannel.id, selectedFeedId].join('@') - } - } - - return '' -} - -async function selectFeed(channelId: string, feedsGroupedByChannelId: Dictionary): Promise { - const channelFeeds = feedsGroupedByChannelId.has(channelId) - ? new Collection(feedsGroupedByChannelId.get(channelId)) - : new Collection() - const choices = getFeedChoises(channelFeeds) - - const selected: ChoiceValue = await select({ - message: `Select feed ID for "${channelId}":`, - choices, - pageSize: 10 - }) - - switch (selected.type) { - case 'skip': - return '-' - case 'type': - return await input({ message: ' Feed ID:', default: 'SD' }) - case 'feed': - const selectedFeed = selected.value - if (!selectedFeed) return '' - return selectedFeed.id || '' - } - - return '' -} - -function getChannelChoises(channels: Collection): Choice[] { - const choises: Choice[] = [] - - channels.forEach((channel: Channel) => { - const names = new Collection([channel.name, ...channel.getAltNames().all()]).uniq().join(', ') - - choises.push({ - value: { - type: 'channel', - value: channel - }, - name: `${channel.id} (${names})`, - short: `${channel.id}` - }) - }) - - choises.push({ name: 'Type...', value: { type: 'type' } }) - choises.push({ name: 'Skip', value: { type: 'skip' } }) - - return choises -} - -function getFeedChoises(feeds: Collection): Choice[] { - const choises: Choice[] = [] - - feeds.forEach((feed: Feed) => { - let name = `${feed.id} (${feed.name})` - if (feed.isMain) name += ' [main]' - - choises.push({ - value: { - type: 'feed', - value: feed - }, - default: feed.isMain, - name, - short: feed.id - }) - }) - - choises.push({ name: 'Type...', value: { type: 'type' } }) - choises.push({ name: 'Skip', value: { type: 'skip' } }) - - return choises -} - -function save(filepath: string, channelList: ChannelList) { - if (!storage.existsSync(filepath)) return - storage.saveSync(filepath, channelList.toString()) - logger.info(`\nFile '${filepath}' successfully saved`) -} - -function escapeRegex(string: string) { - return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&') -} +import { Storage, Collection, Logger, Dictionary } from '@freearhey/core' +import type { DataProcessorData } from '../../types/dataProcessor' +import type { DataLoaderData } from '../../types/dataLoader' +import { ChannelSearchableData } from '../../types/channel' +import { Channel, ChannelList, Feed } from '../../models' +import { DataProcessor, DataLoader } from '../../core' +import { select, input } from '@inquirer/prompts' +import { ChannelsParser } from '../../core' +import { DATA_DIR } from '../../constants' +import nodeCleanup from 'node-cleanup' +import sjs from '@freearhey/search-js' +import epgGrabber from 'epg-grabber' +import { Command } from 'commander' +import readline from 'readline' + +interface ChoiceValue { type: string; value?: Feed | Channel } +interface Choice { name: string; short?: string; value: ChoiceValue; default?: boolean } + +if (process.platform === 'win32') { + readline + .createInterface({ + input: process.stdin, + output: process.stdout + }) + .on('SIGINT', function () { + process.emit('SIGINT') + }) +} + +const program = new Command() + +program.argument('', 'Path to *.channels.xml file to edit').parse(process.argv) + +const filepath = program.args[0] +const logger = new Logger() +const storage = new Storage() +let channelList = new ChannelList({ channels: [] }) + +main(filepath) +nodeCleanup(() => { + save(filepath, channelList) +}) + +export default async function main(filepath: string) { + if (!(await storage.exists(filepath))) { + throw new Error(`File "${filepath}" does not exists`) + } + + logger.info('loading data from api...') + const processor = new DataProcessor() + const dataStorage = new Storage(DATA_DIR) + const loader = new DataLoader({ storage: dataStorage }) + const data: DataLoaderData = await loader.load() + const { channels, channelsKeyById, feedsGroupedByChannelId }: DataProcessorData = + processor.process(data) + + logger.info('loading channels...') + const parser = new ChannelsParser({ storage }) + channelList = await parser.parse(filepath) + const parsedChannelsWithoutId = channelList.channels.filter( + (channel: epgGrabber.Channel) => !channel.xmltv_id + ) + + logger.info( + `found ${channelList.channels.count()} channels (including ${parsedChannelsWithoutId.count()} without ID)` + ) + + logger.info('creating search index...') + const items = channels.map((channel: Channel) => channel.getSearchable()).all() + const searchIndex = sjs.createIndex(items, { + searchable: ['name', 'altNames', 'guideNames', 'streamNames', 'feedFullNames'] + }) + + logger.info('starting...\n') + + for (const channel of parsedChannelsWithoutId.all()) { + try { + channel.xmltv_id = await selectChannel( + channel, + searchIndex, + feedsGroupedByChannelId, + channelsKeyById + ) + } catch (err) { + logger.info(err.message) + break + } + } + + parsedChannelsWithoutId.forEach((channel: epgGrabber.Channel) => { + if (channel.xmltv_id === '-') { + channel.xmltv_id = '' + } + }) +} + +async function selectChannel( + channel: epgGrabber.Channel, + searchIndex, + feedsGroupedByChannelId: Dictionary, + channelsKeyById: Dictionary +): Promise { + const query = escapeRegex(channel.name) + const similarChannels = searchIndex + .search(query) + .map((item: ChannelSearchableData) => channelsKeyById.get(item.id)) + + const selected: ChoiceValue = await select({ + message: `Select channel ID for "${channel.name}" (${channel.site_id}):`, + choices: getChannelChoises(new Collection(similarChannels)), + pageSize: 10 + }) + + switch (selected.type) { + case 'skip': + return '-' + case 'type': { + const typedChannelId = await input({ message: ' Channel ID:' }) + if (!typedChannelId) return '' + const selectedFeedId = await selectFeed(typedChannelId, feedsGroupedByChannelId) + if (selectedFeedId === '-') return typedChannelId + return [typedChannelId, selectedFeedId].join('@') + } + case 'channel': { + const selectedChannel = selected.value + if (!selectedChannel) return '' + const selectedFeedId = await selectFeed(selectedChannel.id || '', feedsGroupedByChannelId) + if (selectedFeedId === '-') return selectedChannel.id || '' + return [selectedChannel.id, selectedFeedId].join('@') + } + } + + return '' +} + +async function selectFeed(channelId: string, feedsGroupedByChannelId: Dictionary): Promise { + const channelFeeds = feedsGroupedByChannelId.has(channelId) + ? new Collection(feedsGroupedByChannelId.get(channelId)) + : new Collection() + const choices = getFeedChoises(channelFeeds) + + const selected: ChoiceValue = await select({ + message: `Select feed ID for "${channelId}":`, + choices, + pageSize: 10 + }) + + switch (selected.type) { + case 'skip': + return '-' + case 'type': + return await input({ message: ' Feed ID:', default: 'SD' }) + case 'feed': + const selectedFeed = selected.value + if (!selectedFeed) return '' + return selectedFeed.id || '' + } + + return '' +} + +function getChannelChoises(channels: Collection): Choice[] { + const choises: Choice[] = [] + + channels.forEach((channel: Channel) => { + const names = new Collection([channel.name, ...channel.getAltNames().all()]).uniq().join(', ') + + choises.push({ + value: { + type: 'channel', + value: channel + }, + name: `${channel.id} (${names})`, + short: `${channel.id}` + }) + }) + + choises.push({ name: 'Type...', value: { type: 'type' } }) + choises.push({ name: 'Skip', value: { type: 'skip' } }) + + return choises +} + +function getFeedChoises(feeds: Collection): Choice[] { + const choises: Choice[] = [] + + feeds.forEach((feed: Feed) => { + let name = `${feed.id} (${feed.name})` + if (feed.isMain) name += ' [main]' + + choises.push({ + value: { + type: 'feed', + value: feed + }, + default: feed.isMain, + name, + short: feed.id + }) + }) + + choises.push({ name: 'Type...', value: { type: 'type' } }) + choises.push({ name: 'Skip', value: { type: 'skip' } }) + + return choises +} + +function save(filepath: string, channelList: ChannelList) { + if (!storage.existsSync(filepath)) return + storage.saveSync(filepath, channelList.toString()) + logger.info(`\nFile '${filepath}' successfully saved`) +} + +function escapeRegex(string: string) { + return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&') +} 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/channels/parse.ts b/scripts/commands/channels/parse.ts index fb0e0447..58eccd68 100644 --- a/scripts/commands/channels/parse.ts +++ b/scripts/commands/channels/parse.ts @@ -1,88 +1,86 @@ -import { Logger, File, Storage } from '@freearhey/core' -import { ChannelsParser } from '../../core' -import { ChannelList } from '../../models' -import { pathToFileURL } from 'node:url' -import epgGrabber from 'epg-grabber' -import { Command } from 'commander' - -const program = new Command() -program - .requiredOption('-c, --config ', 'Config file') - .option('-s, --set [args...]', 'Set custom arguments') - .option('-o, --output ', 'Output file') - .parse(process.argv) - -type ParseOptions = { - config: string - set?: string - output?: string - clean?: boolean -} - -const options: ParseOptions = program.opts() - -async function main() { - function isPromise(promise: object[] | Promise) { - return ( - !!promise && - typeof promise === 'object' && - typeof (promise as Promise).then === 'function' - ) - } - - const storage = new Storage() - const logger = new Logger() - const parser = new ChannelsParser({ storage }) - const file = new File(options.config) - const dir = file.dirname() - const config = (await import(pathToFileURL(options.config).toString())).default - const outputFilepath = options.output || `${dir}/${config.site}.channels.xml` - - let channelList = new ChannelList({ channels: [] }) - if (await storage.exists(outputFilepath)) { - channelList = await parser.parse(outputFilepath) - } - - const args: { - [key: string]: string - } = {} - - if (Array.isArray(options.set)) { - options.set.forEach((arg: string) => { - const [key, value] = arg.split(':') - args[key] = value - }) - } - - let parsedChannels = config.channels(args) - if (isPromise(parsedChannels)) { - parsedChannels = await parsedChannels - } - parsedChannels = parsedChannels.map((channel: epgGrabber.Channel) => { - channel.site = config.site - - return channel - }) - - const newChannelList = new ChannelList({ channels: [] }) - parsedChannels.forEach((channel: epgGrabber.Channel) => { - if (!channel.site_id) return - - const found: epgGrabber.Channel | undefined = channelList.get(channel.site_id) - - if (found) { - channel.xmltv_id = found.xmltv_id - channel.lang = found.lang - } - - newChannelList.add(channel) - }) - - newChannelList.sort() - - await storage.save(outputFilepath, newChannelList.toString()) - - logger.info(`File '${outputFilepath}' successfully saved`) -} - -main() +import { Logger, File, Storage } from '@freearhey/core' +import { ChannelsParser } from '../../core' +import { ChannelList } from '../../models' +import { pathToFileURL } from 'node:url' +import epgGrabber from 'epg-grabber' +import { Command } from 'commander' + +const program = new Command() +program + .requiredOption('-c, --config ', 'Config file') + .option('-s, --set [args...]', 'Set custom arguments') + .option('-o, --output ', 'Output file') + .parse(process.argv) + +interface ParseOptions { + config: string + set?: string + output?: string + clean?: boolean +} + +const options: ParseOptions = program.opts() + +async function main() { + function isPromise(promise: object[] | Promise) { + return ( + !!promise && + typeof promise === 'object' && + typeof (promise as Promise).then === 'function' + ) + } + + const storage = new Storage() + const logger = new Logger() + const parser = new ChannelsParser({ storage }) + const file = new File(options.config) + const dir = file.dirname() + const config = (await import(pathToFileURL(options.config).toString())).default + const outputFilepath = options.output || `${dir}/${config.site}.channels.xml` + + let channelList = new ChannelList({ channels: [] }) + if (await storage.exists(outputFilepath)) { + channelList = await parser.parse(outputFilepath) + } + + const args: Record = {} + + if (Array.isArray(options.set)) { + options.set.forEach((arg: string) => { + const [key, value] = arg.split(':') + args[key] = value + }) + } + + let parsedChannels = config.channels(args) + if (isPromise(parsedChannels)) { + parsedChannels = await parsedChannels + } + parsedChannels = parsedChannels.map((channel: epgGrabber.Channel) => { + channel.site = config.site + + return channel + }) + + const newChannelList = new ChannelList({ channels: [] }) + parsedChannels.forEach((channel: epgGrabber.Channel) => { + if (!channel.site_id) return + + const found: epgGrabber.Channel | undefined = channelList.get(channel.site_id) + + if (found) { + channel.xmltv_id = found.xmltv_id + channel.lang = found.lang + } + + newChannelList.add(channel) + }) + + newChannelList.sort() + + await storage.save(outputFilepath, newChannelList.toString()) + + logger.info(`File '${outputFilepath}' successfully saved`) +} + +main() diff --git a/scripts/commands/channels/validate.ts b/scripts/commands/channels/validate.ts index 8bd023b7..93e6945e 100644 --- a/scripts/commands/channels/validate.ts +++ b/scripts/commands/channels/validate.ts @@ -1,100 +1,100 @@ -import { ChannelsParser, DataLoader, DataProcessor } from '../../core' -import { DataProcessorData } from '../../types/dataProcessor' -import { Storage, Dictionary, File } from '@freearhey/core' -import { DataLoaderData } from '../../types/dataLoader' -import { ChannelList } from '../../models' -import { DATA_DIR } from '../../constants' -import epgGrabber from 'epg-grabber' -import { program } from 'commander' -import chalk from 'chalk' -import langs from 'langs' - -program.argument('[filepath]', 'Path to *.channels.xml files to validate').parse(process.argv) - -type ValidationError = { - type: 'duplicate' | 'wrong_channel_id' | 'wrong_feed_id' | 'wrong_lang' - name: string - lang?: string - xmltv_id?: string - site_id?: string - logo?: string -} - -async function main() { - const processor = new DataProcessor() - const dataStorage = new Storage(DATA_DIR) - const loader = new DataLoader({ storage: dataStorage }) - const data: DataLoaderData = await loader.load() - const { channelsKeyById, feedsKeyByStreamId }: DataProcessorData = processor.process(data) - const parser = new ChannelsParser({ - storage: new Storage() - }) - - let totalFiles = 0 - let totalErrors = 0 - let totalWarnings = 0 - - const storage = new Storage() - 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 channelList: ChannelList = await parser.parse(filepath) - - const bufferBySiteId = new Dictionary() - const errors: ValidationError[] = [] - channelList.channels.forEach((channel: epgGrabber.Channel) => { - const bufferId: string = channel.site_id - if (bufferBySiteId.missing(bufferId)) { - bufferBySiteId.set(bufferId, true) - } else { - errors.push({ type: 'duplicate', ...channel }) - totalErrors++ - } - - if (!langs.where('1', channel.lang ?? '')) { - errors.push({ type: 'wrong_lang', ...channel }) - totalErrors++ - } - - if (!channel.xmltv_id) return - const [channelId, feedId] = channel.xmltv_id.split('@') - - const foundChannel = channelsKeyById.get(channelId) - if (!foundChannel) { - errors.push({ type: 'wrong_channel_id', ...channel }) - totalWarnings++ - } - - if (feedId) { - const foundFeed = feedsKeyByStreamId.get(channel.xmltv_id) - if (!foundFeed) { - errors.push({ type: 'wrong_feed_id', ...channel }) - totalWarnings++ - } - } - }) - - if (errors.length) { - console.log(chalk.underline(filepath)) - console.table(errors, ['type', 'lang', 'xmltv_id', 'site_id', 'name']) - console.log() - totalFiles++ - } - } - - const totalProblems = totalWarnings + totalErrors - if (totalProblems > 0) { - console.log( - chalk.red( - `${totalProblems} problems (${totalErrors} errors, ${totalWarnings} warnings) in ${totalFiles} file(s)` - ) - ) - if (totalErrors > 0) { - process.exit(1) - } - } -} - -main() +import { ChannelsParser, DataLoader, DataProcessor } from '../../core' +import { DataProcessorData } from '../../types/dataProcessor' +import { Storage, Dictionary, File } from '@freearhey/core' +import { DataLoaderData } from '../../types/dataLoader' +import { ChannelList } from '../../models' +import { DATA_DIR } from '../../constants' +import epgGrabber from 'epg-grabber' +import { program } from 'commander' +import chalk from 'chalk' +import langs from 'langs' + +program.argument('[filepath...]', 'Path to *.channels.xml files to validate').parse(process.argv) + +interface ValidationError { + type: 'duplicate' | 'wrong_channel_id' | 'wrong_feed_id' | 'wrong_lang' + name: string + lang?: string + xmltv_id?: string + site_id?: string + logo?: string +} + +async function main() { + const processor = new DataProcessor() + const dataStorage = new Storage(DATA_DIR) + const loader = new DataLoader({ storage: dataStorage }) + const data: DataLoaderData = await loader.load() + const { channelsKeyById, feedsKeyByStreamId }: DataProcessorData = processor.process(data) + const parser = new ChannelsParser({ + storage: new Storage() + }) + + let totalFiles = 0 + let totalErrors = 0 + let totalWarnings = 0 + + const storage = new Storage() + 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 channelList: ChannelList = await parser.parse(filepath) + + const bufferBySiteId = new Dictionary() + const errors: ValidationError[] = [] + channelList.channels.forEach((channel: epgGrabber.Channel) => { + const bufferId: string = channel.site_id + if (bufferBySiteId.missing(bufferId)) { + bufferBySiteId.set(bufferId, true) + } else { + errors.push({ type: 'duplicate', ...channel }) + totalErrors++ + } + + if (!langs.where('1', channel.lang ?? '')) { + errors.push({ type: 'wrong_lang', ...channel }) + totalErrors++ + } + + if (!channel.xmltv_id) return + const [channelId, feedId] = channel.xmltv_id.split('@') + + const foundChannel = channelsKeyById.get(channelId) + if (!foundChannel) { + errors.push({ type: 'wrong_channel_id', ...channel }) + totalWarnings++ + } + + if (feedId) { + const foundFeed = feedsKeyByStreamId.get(channel.xmltv_id) + if (!foundFeed) { + errors.push({ type: 'wrong_feed_id', ...channel }) + totalWarnings++ + } + } + }) + + if (errors.length) { + console.log(chalk.underline(filepath)) + console.table(errors, ['type', 'lang', 'xmltv_id', 'site_id', 'name']) + console.log() + totalFiles++ + } + } + + const totalProblems = totalWarnings + totalErrors + if (totalProblems > 0) { + console.log( + chalk.red( + `${totalProblems} problems (${totalErrors} errors, ${totalWarnings} warnings) in ${totalFiles} file(s)` + ) + ) + if (totalErrors > 0) { + process.exit(1) + } + } +} + +main() diff --git a/scripts/commands/epg/grab.ts b/scripts/commands/epg/grab.ts index ebf94e70..9e111294 100644 --- a/scripts/commands/epg/grab.ts +++ b/scripts/commands/epg/grab.ts @@ -1,133 +1,133 @@ -import { Logger, Timer, Storage, Collection } from '@freearhey/core' -import { QueueCreator, Job, ChannelsParser } from '../../core' -import { Option, program } from 'commander' -import { SITES_DIR } from '../../constants' -import { Channel } from 'epg-grabber' -import path from 'path' -import { ChannelList } from '../../models' - -program - .addOption(new Option('-s, --site ', 'Name of the site to parse')) - .addOption( - new Option( - '-c, --channels ', - 'Path to *.channels.xml file (required if the "--site" attribute is not specified)' - ) - ) - .addOption(new Option('-o, --output ', 'Path to output file').default('guide.xml')) - .addOption(new Option('-l, --lang ', 'Filter channels by languages (ISO 639-1 codes)')) - .addOption( - new Option('-t, --timeout ', 'Override the default timeout for each request').env( - 'TIMEOUT' - ) - ) - .addOption( - new Option('-d, --delay ', 'Override the default delay between request').env( - 'DELAY' - ) - ) - .addOption(new Option('-x, --proxy ', 'Use the specified proxy').env('PROXY')) - .addOption( - new Option( - '--days ', - 'Override the number of days for which the program will be loaded (defaults to the value from the site config)' - ) - .argParser(value => parseInt(value)) - .env('DAYS') - ) - .addOption( - new Option('--maxConnections ', 'Limit on the number of concurrent requests') - .default(1) - .env('MAX_CONNECTIONS') - ) - .addOption( - new Option('--gzip', 'Create a compressed version of the guide as well') - .default(false) - .env('GZIP') - ) - .addOption(new Option('--curl', 'Display each request as CURL').default(false).env('CURL')) - .parse() - -export type GrabOptions = { - site?: string - channels?: string - output: string - gzip: boolean - curl: boolean - maxConnections: number - timeout?: string - delay?: string - lang?: string - days?: number - proxy?: string -} - -const options: GrabOptions = program.opts() - -async function main() { - if (!options.site && !options.channels) - throw new Error('One of the arguments must be presented: `--site` or `--channels`') - - const logger = new Logger() - - logger.start('starting...') - - logger.info('config:') - logger.tree(options) - - logger.info('loading channels...') - const storage = new Storage() - const parser = new ChannelsParser({ storage }) - - let files: string[] = [] - if (options.site) { - let pattern = path.join(SITES_DIR, options.site, '*.channels.xml') - pattern = pattern.replace(/\\/g, '/') - files = await storage.list(pattern) - } else if (options.channels) { - files = await storage.list(options.channels) - } - - let channels = new Collection() - for (const filepath of files) { - const channelList: ChannelList = await parser.parse(filepath) - - channels = channels.concat(channelList.channels) - } - - if (options.lang) { - channels = channels.filter((channel: Channel) => { - if (!options.lang || !channel.lang) return true - - return options.lang.includes(channel.lang) - }) - } - - logger.info(` found ${channels.count()} channel(s)`) - - logger.info('run:') - runJob({ logger, channels }) -} - -main() - -async function runJob({ logger, channels }: { logger: Logger; channels: Collection }) { - const timer = new Timer() - timer.start() - - const queueCreator = new QueueCreator({ - channels, - logger, - options - }) - const queue = await queueCreator.create() - const job = new Job({ - queue, - logger, - options - }) - - await job.run() - - logger.success(` done in ${timer.format('HH[h] mm[m] ss[s]')}`) -} +import { Logger, Timer, Storage, Collection } from '@freearhey/core' +import { QueueCreator, Job, ChannelsParser } from '../../core' +import { Option, program } from 'commander' +import { SITES_DIR } from '../../constants' +import { Channel } from 'epg-grabber' +import path from 'path' +import { ChannelList } from '../../models' + +program + .addOption(new Option('-s, --site ', 'Name of the site to parse')) + .addOption( + new Option( + '-c, --channels ', + 'Path to *.channels.xml file (required if the "--site" attribute is not specified)' + ) + ) + .addOption(new Option('-o, --output ', 'Path to output file').default('guide.xml')) + .addOption(new Option('-l, --lang ', 'Filter channels by languages (ISO 639-1 codes)')) + .addOption( + new Option('-t, --timeout ', 'Override the default timeout for each request').env( + 'TIMEOUT' + ) + ) + .addOption( + new Option('-d, --delay ', 'Override the default delay between request').env( + 'DELAY' + ) + ) + .addOption(new Option('-x, --proxy ', 'Use the specified proxy').env('PROXY')) + .addOption( + new Option( + '--days ', + 'Override the number of days for which the program will be loaded (defaults to the value from the site config)' + ) + .argParser(value => parseInt(value)) + .env('DAYS') + ) + .addOption( + new Option('--maxConnections ', 'Limit on the number of concurrent requests') + .default(1) + .env('MAX_CONNECTIONS') + ) + .addOption( + new Option('--gzip', 'Create a compressed version of the guide as well') + .default(false) + .env('GZIP') + ) + .addOption(new Option('--curl', 'Display each request as CURL').default(false).env('CURL')) + .parse() + +export interface GrabOptions { + site?: string + channels?: string + output: string + gzip: boolean + curl: boolean + maxConnections: number + timeout?: string + delay?: string + lang?: string + days?: number + proxy?: string +} + +const options: GrabOptions = program.opts() + +async function main() { + if (!options.site && !options.channels) + throw new Error('One of the arguments must be presented: `--site` or `--channels`') + + const logger = new Logger() + + logger.start('starting...') + + logger.info('config:') + logger.tree(options) + + logger.info('loading channels...') + const storage = new Storage() + const parser = new ChannelsParser({ storage }) + + let files: string[] = [] + if (options.site) { + let pattern = path.join(SITES_DIR, options.site, '*.channels.xml') + pattern = pattern.replace(/\\/g, '/') + files = await storage.list(pattern) + } else if (options.channels) { + files = await storage.list(options.channels) + } + + let channels = new Collection() + for (const filepath of files) { + const channelList: ChannelList = await parser.parse(filepath) + + channels = channels.concat(channelList.channels) + } + + if (options.lang) { + channels = channels.filter((channel: Channel) => { + if (!options.lang || !channel.lang) return true + + return options.lang.includes(channel.lang) + }) + } + + logger.info(` found ${channels.count()} channel(s)`) + + logger.info('run:') + runJob({ logger, channels }) +} + +main() + +async function runJob({ logger, channels }: { logger: Logger; channels: Collection }) { + const timer = new Timer() + timer.start() + + const queueCreator = new QueueCreator({ + channels, + logger, + options + }) + const queue = await queueCreator.create() + const job = new Job({ + queue, + logger, + options + }) + + await job.run() + + logger.success(` done in ${timer.format('HH[h] mm[m] ss[s]')}`) +} diff --git a/scripts/commands/sites/init.ts b/scripts/commands/sites/init.ts index 44df0c95..9d3a34a8 100644 --- a/scripts/commands/sites/init.ts +++ b/scripts/commands/sites/init.ts @@ -1,45 +1,45 @@ -import { Logger, Storage } from '@freearhey/core' -import { SITES_DIR } from '../../constants' -import { pathToFileURL } from 'node:url' -import { program } from 'commander' -import fs from 'fs-extra' - -program.argument('', 'Domain name of the site').parse(process.argv) - -const domain = program.args[0] - -async function main() { - const storage = new Storage(SITES_DIR) - const logger = new Logger() - - logger.info(`Initializing "${domain}"...\r\n`) - - const dir = domain - if (await storage.exists(dir)) { - throw new Error(`Folder "${dir}" already exists`) - } - - await storage.createDir(dir) - - logger.info(`Creating "${dir}/${domain}.test.js"...`) - const testTemplate = fs.readFileSync(pathToFileURL('scripts/templates/_test.js'), { - encoding: 'utf8' - }) - await storage.save(`${dir}/${domain}.test.js`, testTemplate.replace(//g, domain)) - - logger.info(`Creating "${dir}/${domain}.config.js"...`) - const configTemplate = fs.readFileSync(pathToFileURL('scripts/templates/_config.js'), { - encoding: 'utf8' - }) - await storage.save(`${dir}/${domain}.config.js`, configTemplate.replace(//g, domain)) - - logger.info(`Creating "${dir}/readme.md"...`) - const readmeTemplate = fs.readFileSync(pathToFileURL('scripts/templates/_readme.md'), { - encoding: 'utf8' - }) - await storage.save(`${dir}/readme.md`, readmeTemplate.replace(//g, domain)) - - logger.info('\r\nDone') -} - -main() +import { Logger, Storage } from '@freearhey/core' +import { SITES_DIR } from '../../constants' +import { pathToFileURL } from 'node:url' +import { program } from 'commander' +import fs from 'fs-extra' + +program.argument('', 'Domain name of the site').parse(process.argv) + +const domain = program.args[0] + +async function main() { + const storage = new Storage(SITES_DIR) + const logger = new Logger() + + logger.info(`Initializing "${domain}"...\r\n`) + + const dir = domain + if (await storage.exists(dir)) { + throw new Error(`Folder "${dir}" already exists`) + } + + await storage.createDir(dir) + + logger.info(`Creating "${dir}/${domain}.test.js"...`) + const testTemplate = fs.readFileSync(pathToFileURL('scripts/templates/_test.js'), { + encoding: 'utf8' + }) + await storage.save(`${dir}/${domain}.test.js`, testTemplate.replace(//g, domain)) + + logger.info(`Creating "${dir}/${domain}.config.js"...`) + const configTemplate = fs.readFileSync(pathToFileURL('scripts/templates/_config.js'), { + encoding: 'utf8' + }) + await storage.save(`${dir}/${domain}.config.js`, configTemplate.replace(//g, domain)) + + logger.info(`Creating "${dir}/readme.md"...`) + const readmeTemplate = fs.readFileSync(pathToFileURL('scripts/templates/_readme.md'), { + encoding: 'utf8' + }) + await storage.save(`${dir}/readme.md`, readmeTemplate.replace(//g, domain)) + + logger.info('\r\nDone') +} + +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/constants.ts b/scripts/constants.ts index 8af78b1d..52c5d798 100644 --- a/scripts/constants.ts +++ b/scripts/constants.ts @@ -1,9 +1,9 @@ -export const ROOT_DIR = process.env.ROOT_DIR || '.' -export const SITES_DIR = process.env.SITES_DIR || './sites' -export const GUIDES_DIR = process.env.GUIDES_DIR || './guides' -export const DATA_DIR = process.env.DATA_DIR || './temp/data' -export const API_DIR = process.env.API_DIR || '.api' -export const DOT_SITES_DIR = process.env.DOT_SITES_DIR || './.sites' -export const TESTING = process.env.NODE_ENV === 'test' ? true : false -export const OWNER = 'iptv-org' -export const REPO = 'epg' +export const ROOT_DIR = process.env.ROOT_DIR || '.' +export const SITES_DIR = process.env.SITES_DIR || './sites' +export const GUIDES_DIR = process.env.GUIDES_DIR || './guides' +export const DATA_DIR = process.env.DATA_DIR || './temp/data' +export const API_DIR = process.env.API_DIR || '.api' +export const DOT_SITES_DIR = process.env.DOT_SITES_DIR || './.sites' +export const TESTING = process.env.NODE_ENV === 'test' ? true : false +export const OWNER = 'iptv-org' +export const REPO = 'epg' diff --git a/scripts/core/apiClient.ts b/scripts/core/apiClient.ts index 931a9b14..e4815a81 100644 --- a/scripts/core/apiClient.ts +++ b/scripts/core/apiClient.ts @@ -1,16 +1,16 @@ -import axios, { AxiosInstance, AxiosResponse, AxiosRequestConfig } from 'axios' - -export class ApiClient { - instance: AxiosInstance - - constructor() { - this.instance = axios.create({ - baseURL: 'https://iptv-org.github.io/api', - responseType: 'stream' - }) - } - - get(url: string, options: AxiosRequestConfig): Promise { - return this.instance.get(url, options) - } -} +import axios, { AxiosInstance, AxiosResponse, AxiosRequestConfig } from 'axios' + +export class ApiClient { + instance: AxiosInstance + + constructor() { + this.instance = axios.create({ + baseURL: 'https://iptv-org.github.io/api', + responseType: 'stream' + }) + } + + get(url: string, options: AxiosRequestConfig): Promise { + return this.instance.get(url, options) + } +} diff --git a/scripts/core/channelsParser.ts b/scripts/core/channelsParser.ts index 43a5f28b..d47aee7d 100644 --- a/scripts/core/channelsParser.ts +++ b/scripts/core/channelsParser.ts @@ -1,22 +1,22 @@ -import { parseChannels } from 'epg-grabber' -import { Storage } from '@freearhey/core' -import { ChannelList } from '../models' - -type ChannelsParserProps = { - storage: Storage -} - -export class ChannelsParser { - storage: Storage - - constructor({ storage }: ChannelsParserProps) { - this.storage = storage - } - - async parse(filepath: string): Promise { - const content = await this.storage.load(filepath) - const parsed = parseChannels(content) - - return new ChannelList({ channels: parsed }) - } -} +import { parseChannels } from 'epg-grabber' +import { Storage } from '@freearhey/core' +import { ChannelList } from '../models' + +interface ChannelsParserProps { + storage: Storage +} + +export class ChannelsParser { + storage: Storage + + constructor({ storage }: ChannelsParserProps) { + this.storage = storage + } + + async parse(filepath: string): Promise { + const content = await this.storage.load(filepath) + const parsed = parseChannels(content) + + return new ChannelList({ channels: parsed }) + } +} diff --git a/scripts/core/configLoader.ts b/scripts/core/configLoader.ts index 1beb3703..a49aee5a 100644 --- a/scripts/core/configLoader.ts +++ b/scripts/core/configLoader.ts @@ -1,33 +1,32 @@ -import { SiteConfig } from 'epg-grabber' -import _ from 'lodash' -import { pathToFileURL } from 'url' - -export class ConfigLoader { - async load(filepath: string): Promise { - const fileUrl = pathToFileURL(filepath).toString() - const config = (await import(fileUrl)).default - const defaultConfig = { - days: 1, - delay: 0, - output: 'guide.xml', - request: { - method: 'GET', - maxContentLength: 5242880, - timeout: 30000, - withCredentials: true, - jar: null, - responseType: 'arraybuffer', - cache: false, - headers: null, - data: null - }, - maxConnections: 1, - site: undefined, - url: undefined, - parser: undefined, - channels: undefined - } - - return _.merge(defaultConfig, config) - } -} +import { SiteConfig } from 'epg-grabber' +import { pathToFileURL } from 'url' + +export class ConfigLoader { + async load(filepath: string): Promise { + const fileUrl = pathToFileURL(filepath).toString() + const config = (await import(fileUrl)).default + const defaultConfig = { + days: 1, + delay: 0, + output: 'guide.xml', + request: { + method: 'GET', + maxContentLength: 5242880, + timeout: 30000, + withCredentials: true, + jar: null, + responseType: 'arraybuffer', + cache: false, + headers: null, + data: null + }, + maxConnections: 1, + site: undefined, + url: undefined, + parser: undefined, + channels: undefined + } + + return { ...defaultConfig, ...config } as SiteConfig + } +} 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/dataProcessor.ts b/scripts/core/dataProcessor.ts index 1f0252fc..a104e675 100644 --- a/scripts/core/dataProcessor.ts +++ b/scripts/core/dataProcessor.ts @@ -1,56 +1,55 @@ -import { Channel, Feed, GuideChannel, Logo, Stream } from '../models' -import { DataLoaderData } from '../types/dataLoader' -import { Collection } from '@freearhey/core' - -export class DataProcessor { - constructor() {} - - process(data: DataLoaderData) { - let channels = new Collection(data.channels).map(data => new Channel(data)) - const channelsKeyById = channels.keyBy((channel: Channel) => channel.id) - - const guideChannels = new Collection(data.guides).map(data => new GuideChannel(data)) - const guideChannelsGroupedByStreamId = guideChannels.groupBy((channel: GuideChannel) => - channel.getStreamId() - ) - - const streams = new Collection(data.streams).map(data => new Stream(data)) - const streamsGroupedById = streams.groupBy((stream: Stream) => stream.getId()) - - let feeds = new Collection(data.feeds).map(data => - new Feed(data) - .withGuideChannels(guideChannelsGroupedByStreamId) - .withStreams(streamsGroupedById) - .withChannel(channelsKeyById) - ) - const feedsKeyByStreamId = feeds.keyBy((feed: Feed) => feed.getStreamId()) - - const logos = new Collection(data.logos).map(data => - new Logo(data).withFeed(feedsKeyByStreamId) - ) - const logosGroupedByChannelId = logos.groupBy((logo: Logo) => logo.channelId) - const logosGroupedByStreamId = logos.groupBy((logo: Logo) => logo.getStreamId()) - - feeds = feeds.map((feed: Feed) => feed.withLogos(logosGroupedByStreamId)) - const feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => feed.channelId) - - channels = channels.map((channel: Channel) => - channel.withFeeds(feedsGroupedByChannelId).withLogos(logosGroupedByChannelId) - ) - - return { - guideChannelsGroupedByStreamId, - feedsGroupedByChannelId, - logosGroupedByChannelId, - logosGroupedByStreamId, - streamsGroupedById, - feedsKeyByStreamId, - channelsKeyById, - guideChannels, - channels, - streams, - feeds, - logos - } - } -} +import { Channel, Feed, GuideChannel, Logo, Stream } from '../models' +import { DataLoaderData } from '../types/dataLoader' +import { Collection } from '@freearhey/core' + +export class DataProcessor { + + process(data: DataLoaderData) { + let channels = new Collection(data.channels).map(data => new Channel(data)) + const channelsKeyById = channels.keyBy((channel: Channel) => channel.id) + + const guideChannels = new Collection(data.guides).map(data => new GuideChannel(data)) + const guideChannelsGroupedByStreamId = guideChannels.groupBy((channel: GuideChannel) => + channel.getStreamId() + ) + + const streams = new Collection(data.streams).map(data => new Stream(data)) + const streamsGroupedById = streams.groupBy((stream: Stream) => stream.getId()) + + let feeds = new Collection(data.feeds).map(data => + new Feed(data) + .withGuideChannels(guideChannelsGroupedByStreamId) + .withStreams(streamsGroupedById) + .withChannel(channelsKeyById) + ) + const feedsKeyByStreamId = feeds.keyBy((feed: Feed) => feed.getStreamId()) + + const logos = new Collection(data.logos).map(data => + new Logo(data).withFeed(feedsKeyByStreamId) + ) + const logosGroupedByChannelId = logos.groupBy((logo: Logo) => logo.channelId) + const logosGroupedByStreamId = logos.groupBy((logo: Logo) => logo.getStreamId()) + + feeds = feeds.map((feed: Feed) => feed.withLogos(logosGroupedByStreamId)) + const feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => feed.channelId) + + channels = channels.map((channel: Channel) => + channel.withFeeds(feedsGroupedByChannelId).withLogos(logosGroupedByChannelId) + ) + + return { + guideChannelsGroupedByStreamId, + feedsGroupedByChannelId, + logosGroupedByChannelId, + logosGroupedByStreamId, + streamsGroupedById, + feedsKeyByStreamId, + channelsKeyById, + guideChannels, + channels, + streams, + feeds, + logos + } + } +} diff --git a/scripts/core/date.js b/scripts/core/date.js index 08c4d938..777f9c33 100644 --- a/scripts/core/date.js +++ b/scripts/core/date.js @@ -1,14 +1,14 @@ -import dayjs from 'dayjs' -import utc from 'dayjs/plugin/utc' - -dayjs.extend(utc) - -const date = {} - -date.getUTC = function (d = null) { - if (typeof d === 'string') return dayjs.utc(d).startOf('d') - - return dayjs.utc().startOf('d') -} - -export default date +import dayjs from 'dayjs' +import utc from 'dayjs/plugin/utc' + +dayjs.extend(utc) + +const date = {} + +date.getUTC = function (d = null) { + if (typeof d === 'string') return dayjs.utc(d).startOf('d') + + return dayjs.utc().startOf('d') +} + +export default date diff --git a/scripts/core/grabber.ts b/scripts/core/grabber.ts index 57bd322d..d1881af7 100644 --- a/scripts/core/grabber.ts +++ b/scripts/core/grabber.ts @@ -1,105 +1,105 @@ -import { EPGGrabber, GrabCallbackData, EPGGrabberMock, SiteConfig, Channel } from 'epg-grabber' -import { Logger, Collection } from '@freearhey/core' -import { Queue, ProxyParser } from './' -import { GrabOptions } from '../commands/epg/grab' -import { TaskQueue, PromisyClass } from 'cwait' -import { SocksProxyAgent } from 'socks-proxy-agent' - -type GrabberProps = { - logger: Logger - queue: Queue - options: GrabOptions -} - -export class Grabber { - logger: Logger - queue: Queue - options: GrabOptions - grabber: EPGGrabber | EPGGrabberMock - - constructor({ logger, queue, options }: GrabberProps) { - this.logger = logger - this.queue = queue - this.options = options - this.grabber = process.env.NODE_ENV === 'test' ? new EPGGrabberMock() : new EPGGrabber() - } - - async grab(): Promise<{ channels: Collection; programs: Collection }> { - const proxyParser = new ProxyParser() - const taskQueue = new TaskQueue(Promise as PromisyClass, this.options.maxConnections) - - const total = this.queue.size() - - const channels = new Collection() - let programs = new Collection() - let i = 1 - - await Promise.all( - this.queue.items().map( - taskQueue.wrap( - async (queueItem: { channel: Channel; config: SiteConfig; date: string }) => { - const { channel, config, date } = queueItem - - channels.add(channel) - - if (this.options.timeout !== undefined) { - const timeout = parseInt(this.options.timeout) - config.request = { ...config.request, ...{ timeout } } - } - - if (this.options.delay !== undefined) { - const delay = parseInt(this.options.delay) - config.delay = delay - } - - if (this.options.proxy !== undefined) { - const proxy = proxyParser.parse(this.options.proxy) - - if ( - proxy.protocol && - ['socks', 'socks5', 'socks5h', 'socks4', 'socks4a'].includes(String(proxy.protocol)) - ) { - const socksProxyAgent = new SocksProxyAgent(this.options.proxy) - - config.request = { - ...config.request, - ...{ httpAgent: socksProxyAgent, httpsAgent: socksProxyAgent } - } - } else { - config.request = { ...config.request, ...{ proxy } } - } - } - - if (this.options.curl === true) { - config.curl = true - } - - const _programs = await this.grabber.grab( - channel, - date, - config, - (data: GrabCallbackData, error: Error | null) => { - const { programs, date } = data - - this.logger.info( - ` [${i}/${total}] ${channel.site} (${channel.lang}) - ${ - channel.xmltv_id - } - ${date.format('MMM D, YYYY')} (${programs.length} programs)` - ) - if (i < total) i++ - - if (error) { - this.logger.info(` ERR: ${error.message}`) - } - } - ) - - programs = programs.concat(new Collection(_programs)) - } - ) - ) - ) - - return { channels, programs } - } -} +import { EPGGrabber, GrabCallbackData, EPGGrabberMock, SiteConfig, Channel } from 'epg-grabber' +import { Logger, Collection } from '@freearhey/core' +import { Queue, ProxyParser } from './' +import { GrabOptions } from '../commands/epg/grab' +import { TaskQueue, PromisyClass } from 'cwait' +import { SocksProxyAgent } from 'socks-proxy-agent' + +interface GrabberProps { + logger: Logger + queue: Queue + options: GrabOptions +} + +export class Grabber { + logger: Logger + queue: Queue + options: GrabOptions + grabber: EPGGrabber | EPGGrabberMock + + constructor({ logger, queue, options }: GrabberProps) { + this.logger = logger + this.queue = queue + this.options = options + this.grabber = process.env.NODE_ENV === 'test' ? new EPGGrabberMock() : new EPGGrabber() + } + + async grab(): Promise<{ channels: Collection; programs: Collection }> { + const proxyParser = new ProxyParser() + const taskQueue = new TaskQueue(Promise as PromisyClass, this.options.maxConnections) + + const total = this.queue.size() + + const channels = new Collection() + let programs = new Collection() + let i = 1 + + await Promise.all( + this.queue.items().map( + taskQueue.wrap( + async (queueItem: { channel: Channel; config: SiteConfig; date: string }) => { + const { channel, config, date } = queueItem + + channels.add(channel) + + if (this.options.timeout !== undefined) { + const timeout = parseInt(this.options.timeout) + config.request = { ...config.request, ...{ timeout } } + } + + if (this.options.delay !== undefined) { + const delay = parseInt(this.options.delay) + config.delay = delay + } + + if (this.options.proxy !== undefined) { + const proxy = proxyParser.parse(this.options.proxy) + + if ( + proxy.protocol && + ['socks', 'socks5', 'socks5h', 'socks4', 'socks4a'].includes(String(proxy.protocol)) + ) { + const socksProxyAgent = new SocksProxyAgent(this.options.proxy) + + config.request = { + ...config.request, + ...{ httpAgent: socksProxyAgent, httpsAgent: socksProxyAgent } + } + } else { + config.request = { ...config.request, ...{ proxy } } + } + } + + if (this.options.curl === true) { + config.curl = true + } + + const _programs = await this.grabber.grab( + channel, + date, + config, + (data: GrabCallbackData, error: Error | null) => { + const { programs, date } = data + + this.logger.info( + ` [${i}/${total}] ${channel.site} (${channel.lang}) - ${ + channel.xmltv_id + } - ${date.format('MMM D, YYYY')} (${programs.length} programs)` + ) + if (i < total) i++ + + if (error) { + this.logger.info(` ERR: ${error.message}`) + } + } + ) + + programs = programs.concat(new Collection(_programs)) + } + ) + ) + ) + + return { channels, programs } + } +} diff --git a/scripts/core/guideManager.ts b/scripts/core/guideManager.ts index aee2f666..ea97d631 100644 --- a/scripts/core/guideManager.ts +++ b/scripts/core/guideManager.ts @@ -1,111 +1,111 @@ -import { Collection, Logger, Zip, Storage, StringTemplate } from '@freearhey/core' -import epgGrabber from 'epg-grabber' -import { OptionValues } from 'commander' -import { Channel, Feed, Guide } from '../models' -import path from 'path' -import { DataLoader, DataProcessor } from '.' -import { DataLoaderData } from '../types/dataLoader' -import { DataProcessorData } from '../types/dataProcessor' -import { DATA_DIR } from '../constants' - -type GuideManagerProps = { - options: OptionValues - logger: Logger - channels: Collection - programs: Collection -} - -export class GuideManager { - options: OptionValues - logger: Logger - channels: Collection - programs: Collection - - constructor({ channels, programs, logger, options }: GuideManagerProps) { - this.options = options - this.logger = logger - this.channels = channels - this.programs = programs - } - - async createGuides() { - const pathTemplate = new StringTemplate(this.options.output) - - const processor = new DataProcessor() - const dataStorage = new Storage(DATA_DIR) - const loader = new DataLoader({ storage: dataStorage }) - const data: DataLoaderData = await loader.load() - const { feedsKeyByStreamId, channelsKeyById }: DataProcessorData = processor.process(data) - - const groupedChannels = this.channels - .map((channel: epgGrabber.Channel) => { - if (channel.xmltv_id && !channel.icon) { - const foundFeed: Feed = feedsKeyByStreamId.get(channel.xmltv_id) - if (foundFeed && foundFeed.hasLogo()) { - channel.icon = foundFeed.getLogoUrl() - } else { - const [channelId] = channel.xmltv_id.split('@') - const foundChannel: Channel = channelsKeyById.get(channelId) - if (foundChannel && foundChannel.hasLogo()) { - channel.icon = foundChannel.getLogoUrl() - } - } - } - - return channel - }) - .orderBy([ - (channel: epgGrabber.Channel) => channel.index, - (channel: epgGrabber.Channel) => channel.xmltv_id - ]) - .uniqBy( - (channel: epgGrabber.Channel) => `${channel.xmltv_id}:${channel.site}:${channel.lang}` - ) - .groupBy((channel: epgGrabber.Channel) => { - return pathTemplate.format({ lang: channel.lang || 'en', site: channel.site || '' }) - }) - - const groupedPrograms = this.programs - .orderBy([ - (program: epgGrabber.Program) => program.channel, - (program: epgGrabber.Program) => program.start - ]) - .groupBy((program: epgGrabber.Program) => { - const lang = - program.titles && program.titles.length && program.titles[0].lang - ? program.titles[0].lang - : 'en' - - return pathTemplate.format({ lang, site: program.site || '' }) - }) - - for (const groupKey of groupedPrograms.keys()) { - const guide = new Guide({ - filepath: groupKey, - gzip: this.options.gzip, - channels: new Collection(groupedChannels.get(groupKey)), - programs: new Collection(groupedPrograms.get(groupKey)) - }) - - await this.save(guide) - } - } - - async save(guide: Guide) { - const storage = new Storage(path.dirname(guide.filepath)) - const xmlFilepath = guide.filepath - const xmlFilename = path.basename(xmlFilepath) - this.logger.info(` saving to "${xmlFilepath}"...`) - const xmltv = guide.toString() - await storage.save(xmlFilename, xmltv) - - if (guide.gzip) { - const zip = new Zip() - const compressed = zip.compress(xmltv) - const gzFilepath = `${guide.filepath}.gz` - const gzFilename = path.basename(gzFilepath) - this.logger.info(` saving to "${gzFilepath}"...`) - await storage.save(gzFilename, compressed) - } - } -} +import { Collection, Logger, Zip, Storage, StringTemplate } from '@freearhey/core' +import epgGrabber from 'epg-grabber' +import { OptionValues } from 'commander' +import { Channel, Feed, Guide } from '../models' +import path from 'path' +import { DataLoader, DataProcessor } from '.' +import { DataLoaderData } from '../types/dataLoader' +import { DataProcessorData } from '../types/dataProcessor' +import { DATA_DIR } from '../constants' + +interface GuideManagerProps { + options: OptionValues + logger: Logger + channels: Collection + programs: Collection +} + +export class GuideManager { + options: OptionValues + logger: Logger + channels: Collection + programs: Collection + + constructor({ channels, programs, logger, options }: GuideManagerProps) { + this.options = options + this.logger = logger + this.channels = channels + this.programs = programs + } + + async createGuides() { + const pathTemplate = new StringTemplate(this.options.output) + + const processor = new DataProcessor() + const dataStorage = new Storage(DATA_DIR) + const loader = new DataLoader({ storage: dataStorage }) + const data: DataLoaderData = await loader.load() + const { feedsKeyByStreamId, channelsKeyById }: DataProcessorData = processor.process(data) + + const groupedChannels = this.channels + .map((channel: epgGrabber.Channel) => { + if (channel.xmltv_id && !channel.icon) { + const foundFeed: Feed = feedsKeyByStreamId.get(channel.xmltv_id) + if (foundFeed && foundFeed.hasLogo()) { + channel.icon = foundFeed.getLogoUrl() + } else { + const [channelId] = channel.xmltv_id.split('@') + const foundChannel: Channel = channelsKeyById.get(channelId) + if (foundChannel && foundChannel.hasLogo()) { + channel.icon = foundChannel.getLogoUrl() + } + } + } + + return channel + }) + .orderBy([ + (channel: epgGrabber.Channel) => channel.index, + (channel: epgGrabber.Channel) => channel.xmltv_id + ]) + .uniqBy( + (channel: epgGrabber.Channel) => `${channel.xmltv_id}:${channel.site}:${channel.lang}` + ) + .groupBy((channel: epgGrabber.Channel) => { + return pathTemplate.format({ lang: channel.lang || 'en', site: channel.site || '' }) + }) + + const groupedPrograms = this.programs + .orderBy([ + (program: epgGrabber.Program) => program.channel, + (program: epgGrabber.Program) => program.start + ]) + .groupBy((program: epgGrabber.Program) => { + const lang = + program.titles && program.titles.length && program.titles[0].lang + ? program.titles[0].lang + : 'en' + + return pathTemplate.format({ lang, site: program.site || '' }) + }) + + for (const groupKey of groupedPrograms.keys()) { + const guide = new Guide({ + filepath: groupKey, + gzip: this.options.gzip, + channels: new Collection(groupedChannels.get(groupKey)), + programs: new Collection(groupedPrograms.get(groupKey)) + }) + + await this.save(guide) + } + } + + async save(guide: Guide) { + const storage = new Storage(path.dirname(guide.filepath)) + const xmlFilepath = guide.filepath + const xmlFilename = path.basename(xmlFilepath) + this.logger.info(` saving to "${xmlFilepath}"...`) + const xmltv = guide.toString() + await storage.save(xmlFilename, xmltv) + + if (guide.gzip) { + const zip = new Zip() + const compressed = zip.compress(xmltv) + const gzFilepath = `${guide.filepath}.gz` + const gzFilename = path.basename(gzFilepath) + this.logger.info(` saving to "${gzFilepath}"...`) + await storage.save(gzFilename, compressed) + } + } +} diff --git a/scripts/core/htmlTable.ts b/scripts/core/htmlTable.ts index 144ba01b..6b917d6b 100644 --- a/scripts/core/htmlTable.ts +++ b/scripts/core/htmlTable.ts @@ -1,55 +1,55 @@ -type Column = { - name: string - nowrap?: boolean - align?: string - colspan?: number -} - -type DataItem = { - value: string - nowrap?: boolean - align?: string - colspan?: number -}[] - -export class HTMLTable { - data: DataItem[] - columns: Column[] - - constructor(data: DataItem[], columns: Column[]) { - this.data = data - this.columns = columns - } - - toString() { - let output = '\r\n' - - output += ' \r\n ' - for (const column of this.columns) { - const nowrap = column.nowrap ? ' nowrap' : '' - const align = column.align ? ` align="${column.align}"` : '' - const colspan = column.colspan ? ` colspan="${column.colspan}"` : '' - - output += `${column.name}` - } - output += '\r\n \r\n' - - output += ' \r\n' - for (const row of this.data) { - output += ' ' - for (const item of row) { - const nowrap = item.nowrap ? ' nowrap' : '' - const align = item.align ? ` align="${item.align}"` : '' - const colspan = item.colspan ? ` colspan="${item.colspan}"` : '' - - output += `${item.value}` - } - output += '\r\n' - } - output += ' \r\n' - - output += '
' - - return output - } -} +interface Column { + name: string + nowrap?: boolean + align?: string + colspan?: number +} + +type DataItem = { + value: string + nowrap?: boolean + align?: string + colspan?: number +}[] + +export class HTMLTable { + data: DataItem[] + columns: Column[] + + constructor(data: DataItem[], columns: Column[]) { + this.data = data + this.columns = columns + } + + toString() { + let output = '\r\n' + + output += ' \r\n ' + for (const column of this.columns) { + const nowrap = column.nowrap ? ' nowrap' : '' + const align = column.align ? ` align="${column.align}"` : '' + const colspan = column.colspan ? ` colspan="${column.colspan}"` : '' + + output += `${column.name}` + } + output += '\r\n \r\n' + + output += ' \r\n' + for (const row of this.data) { + output += ' ' + for (const item of row) { + const nowrap = item.nowrap ? ' nowrap' : '' + const align = item.align ? ` align="${item.align}"` : '' + const colspan = item.colspan ? ` colspan="${item.colspan}"` : '' + + output += `${item.value}` + } + output += '\r\n' + } + output += ' \r\n' + + output += '
' + + return output + } +} 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/core/issueParser.ts b/scripts/core/issueParser.ts index e4626514..2db0f9dd 100644 --- a/scripts/core/issueParser.ts +++ b/scripts/core/issueParser.ts @@ -1,34 +1,34 @@ -import { Dictionary } from '@freearhey/core' -import { Issue } from '../models' - -const FIELDS = new Dictionary({ - Site: 'site' -}) - -export class IssueParser { - parse(issue: { number: number; body: string; labels: { name: string }[] }): Issue { - const fields = issue.body.split('###') - - const data = new Dictionary() - fields.forEach((field: string) => { - const parsed = field.split(/\r?\n/).filter(Boolean) - let _label = parsed.shift() - _label = _label ? _label.trim() : '' - let _value = parsed.join('\r\n') - _value = _value ? _value.trim() : '' - - if (!_label || !_value) return data - - const id: string = FIELDS.get(_label) - const value: string = _value === '_No response_' || _value === 'None' ? '' : _value - - if (!id) return - - data.set(id, value) - }) - - const labels = issue.labels.map(label => label.name) - - return new Issue({ number: issue.number, labels, data }) - } -} +import { Dictionary } from '@freearhey/core' +import { Issue } from '../models' + +const FIELDS = new Dictionary({ + Site: 'site' +}) + +export class IssueParser { + parse(issue: { number: number; body: string; labels: { name: string }[] }): Issue { + const fields = issue.body.split('###') + + const data = new Dictionary() + fields.forEach((field: string) => { + const parsed = field.split(/\r?\n/).filter(Boolean) + let _label = parsed.shift() + _label = _label ? _label.trim() : '' + let _value = parsed.join('\r\n') + _value = _value ? _value.trim() : '' + + if (!_label || !_value) return data + + const id: string = FIELDS.get(_label) + const value: string = _value === '_No response_' || _value === 'None' ? '' : _value + + if (!id) return + + data.set(id, value) + }) + + const labels = issue.labels.map(label => label.name) + + return new Issue({ number: issue.number, labels, data }) + } +} diff --git a/scripts/core/job.ts b/scripts/core/job.ts index 9dbf7308..db37ba15 100644 --- a/scripts/core/job.ts +++ b/scripts/core/job.ts @@ -1,34 +1,34 @@ -import { Logger } from '@freearhey/core' -import { Queue, Grabber, GuideManager } from '.' -import { GrabOptions } from '../commands/epg/grab' - -type JobProps = { - options: GrabOptions - logger: Logger - queue: Queue -} - -export class Job { - options: GrabOptions - logger: Logger - grabber: Grabber - - constructor({ queue, logger, options }: JobProps) { - this.options = options - this.logger = logger - this.grabber = new Grabber({ logger, queue, options }) - } - - async run() { - const { channels, programs } = await this.grabber.grab() - - const manager = new GuideManager({ - channels, - programs, - options: this.options, - logger: this.logger - }) - - await manager.createGuides() - } -} +import { Logger } from '@freearhey/core' +import { Queue, Grabber, GuideManager } from '.' +import { GrabOptions } from '../commands/epg/grab' + +interface JobProps { + options: GrabOptions + logger: Logger + queue: Queue +} + +export class Job { + options: GrabOptions + logger: Logger + grabber: Grabber + + constructor({ queue, logger, options }: JobProps) { + this.options = options + this.logger = logger + this.grabber = new Grabber({ logger, queue, options }) + } + + async run() { + const { channels, programs } = await this.grabber.grab() + + const manager = new GuideManager({ + channels, + programs, + options: this.options, + logger: this.logger + }) + + await manager.createGuides() + } +} diff --git a/scripts/core/proxyParser.ts b/scripts/core/proxyParser.ts index 3e316ab2..9cede1af 100644 --- a/scripts/core/proxyParser.ts +++ b/scripts/core/proxyParser.ts @@ -1,31 +1,31 @@ -import { URL } from 'node:url' - -type ProxyParserResult = { - protocol: string | null - auth?: { - username?: string - password?: string - } - host: string - port: number | null -} - -export class ProxyParser { - parse(_url: string): ProxyParserResult { - const parsed = new URL(_url) - - const result: ProxyParserResult = { - protocol: parsed.protocol.replace(':', '') || null, - host: parsed.hostname, - port: parsed.port ? parseInt(parsed.port) : null - } - - if (parsed.username || parsed.password) { - result.auth = {} - if (parsed.username) result.auth.username = parsed.username - if (parsed.password) result.auth.password = parsed.password - } - - return result - } -} +import { URL } from 'node:url' + +interface ProxyParserResult { + protocol: string | null + auth?: { + username?: string + password?: string + } + host: string + port: number | null +} + +export class ProxyParser { + parse(_url: string): ProxyParserResult { + const parsed = new URL(_url) + + const result: ProxyParserResult = { + protocol: parsed.protocol.replace(':', '') || null, + host: parsed.hostname, + port: parsed.port ? parseInt(parsed.port) : null + } + + if (parsed.username || parsed.password) { + result.auth = {} + if (parsed.username) result.auth.username = parsed.username + if (parsed.password) result.auth.password = parsed.password + } + + return result + } +} diff --git a/scripts/core/queue.ts b/scripts/core/queue.ts index 06ef3961..106c111e 100644 --- a/scripts/core/queue.ts +++ b/scripts/core/queue.ts @@ -1,45 +1,45 @@ -import { Dictionary } from '@freearhey/core' -import { SiteConfig, Channel } from 'epg-grabber' - -export type QueueItem = { - channel: Channel - date: string - config: SiteConfig - error: string | null -} - -export class Queue { - _data: Dictionary - - constructor() { - this._data = new Dictionary() - } - - missing(key: string): boolean { - return this._data.missing(key) - } - - add( - key: string, - { channel, config, date }: { channel: Channel; date: string | null; config: SiteConfig } - ) { - this._data.set(key, { - channel, - date, - config, - error: null - }) - } - - size(): number { - return Object.values(this._data.data()).length - } - - items(): QueueItem[] { - return Object.values(this._data.data()) as QueueItem[] - } - - isEmpty(): boolean { - return this.size() === 0 - } -} +import { Dictionary } from '@freearhey/core' +import { SiteConfig, Channel } from 'epg-grabber' + +export interface QueueItem { + channel: Channel + date: string + config: SiteConfig + error: string | null +} + +export class Queue { + _data: Dictionary + + constructor() { + this._data = new Dictionary() + } + + missing(key: string): boolean { + return this._data.missing(key) + } + + add( + key: string, + { channel, config, date }: { channel: Channel; date: string | null; config: SiteConfig } + ) { + this._data.set(key, { + channel, + date, + config, + error: null + }) + } + + size(): number { + return Object.values(this._data.data()).length + } + + items(): QueueItem[] { + return Object.values(this._data.data()) as QueueItem[] + } + + isEmpty(): boolean { + return this.size() === 0 + } +} diff --git a/scripts/core/queueCreator.ts b/scripts/core/queueCreator.ts index 71213630..56333dcb 100644 --- a/scripts/core/queueCreator.ts +++ b/scripts/core/queueCreator.ts @@ -1,63 +1,63 @@ -import { Storage, Collection, DateTime, Logger } from '@freearhey/core' -import { SITES_DIR, DATA_DIR } from '../constants' -import { GrabOptions } from '../commands/epg/grab' -import { ConfigLoader, Queue } from './' -import { SiteConfig } from 'epg-grabber' -import path from 'path' - -type QueueCreatorProps = { - logger: Logger - options: GrabOptions - channels: Collection -} - -export class QueueCreator { - configLoader: ConfigLoader - logger: Logger - sitesStorage: Storage - dataStorage: Storage - channels: Collection - options: GrabOptions - - constructor({ channels, logger, options }: QueueCreatorProps) { - this.channels = channels - this.logger = logger - this.sitesStorage = new Storage() - this.dataStorage = new Storage(DATA_DIR) - this.options = options - this.configLoader = new ConfigLoader() - } - - async create(): Promise { - let index = 0 - const queue = new Queue() - for (const channel of this.channels.all()) { - channel.index = index++ - if (!channel.site || !channel.site_id || !channel.name) continue - - const configPath = path.resolve(SITES_DIR, `${channel.site}/${channel.site}.config.js`) - const config: SiteConfig = await this.configLoader.load(configPath) - - if (!channel.xmltv_id) { - channel.xmltv_id = channel.site_id - } - - const days = this.options.days || config.days || 1 - const currDate = new DateTime(process.env.CURR_DATE || new Date().toISOString()) - const dates = Array.from({ length: days }, (_, day) => currDate.add(day, 'd')) - dates.forEach((date: DateTime) => { - const dateString = date.toJSON() - const key = `${channel.site}:${channel.lang}:${channel.xmltv_id}:${dateString}` - if (queue.missing(key)) { - queue.add(key, { - channel, - date: dateString, - config - }) - } - }) - } - - return queue - } -} +import { Storage, Collection, DateTime, Logger } from '@freearhey/core' +import { SITES_DIR, DATA_DIR } from '../constants' +import { GrabOptions } from '../commands/epg/grab' +import { ConfigLoader, Queue } from './' +import { SiteConfig } from 'epg-grabber' +import path from 'path' + +interface QueueCreatorProps { + logger: Logger + options: GrabOptions + channels: Collection +} + +export class QueueCreator { + configLoader: ConfigLoader + logger: Logger + sitesStorage: Storage + dataStorage: Storage + channels: Collection + options: GrabOptions + + constructor({ channels, logger, options }: QueueCreatorProps) { + this.channels = channels + this.logger = logger + this.sitesStorage = new Storage() + this.dataStorage = new Storage(DATA_DIR) + this.options = options + this.configLoader = new ConfigLoader() + } + + async create(): Promise { + let index = 0 + const queue = new Queue() + for (const channel of this.channels.all()) { + channel.index = index++ + if (!channel.site || !channel.site_id || !channel.name) continue + + const configPath = path.resolve(SITES_DIR, `${channel.site}/${channel.site}.config.js`) + const config: SiteConfig = await this.configLoader.load(configPath) + + if (!channel.xmltv_id) { + channel.xmltv_id = channel.site_id + } + + const days = this.options.days || config.days || 1 + const currDate = new DateTime(process.env.CURR_DATE || new Date().toISOString()) + const dates = Array.from({ length: days }, (_, day) => currDate.add(day, 'd')) + dates.forEach((date: DateTime) => { + const dateString = date.toJSON() + const key = `${channel.site}:${channel.lang}:${channel.xmltv_id}:${dateString}` + if (queue.missing(key)) { + queue.add(key, { + channel, + date: dateString, + config + }) + } + }) + } + + return queue + } +} diff --git a/scripts/models/channel.ts b/scripts/models/channel.ts index fb62a3fc..e5bbde5e 100644 --- a/scripts/models/channel.ts +++ b/scripts/models/channel.ts @@ -1,164 +1,164 @@ -import { ChannelData, ChannelSearchableData } from '../types/channel' -import { Collection, Dictionary } from '@freearhey/core' -import { Stream, Feed, Logo, GuideChannel } from './' - -export class Channel { - id?: string - name?: string - altNames?: Collection - network?: string - owners?: Collection - countryCode?: string - subdivisionCode?: string - cityName?: string - categoryIds?: Collection - isNSFW: boolean = false - launched?: string - closed?: string - replacedBy?: string - website?: string - feeds?: Collection - logos: Collection = new Collection() - - constructor(data?: ChannelData) { - if (!data) return - - this.id = data.id - this.name = data.name - this.altNames = new Collection(data.alt_names) - this.network = data.network || undefined - this.owners = new Collection(data.owners) - this.countryCode = data.country - this.subdivisionCode = data.subdivision || undefined - this.cityName = data.city || undefined - this.categoryIds = new Collection(data.categories) - this.isNSFW = data.is_nsfw - this.launched = data.launched || undefined - this.closed = data.closed || undefined - this.replacedBy = data.replaced_by || undefined - this.website = data.website || undefined - } - - withFeeds(feedsGroupedByChannelId: Dictionary): this { - if (this.id) this.feeds = new Collection(feedsGroupedByChannelId.get(this.id)) - - return this - } - - withLogos(logosGroupedByChannelId: Dictionary): this { - if (this.id) this.logos = new Collection(logosGroupedByChannelId.get(this.id)) - - return this - } - - getFeeds(): Collection { - if (!this.feeds) return new Collection() - - return this.feeds - } - - getGuideChannels(): Collection { - let channels = new Collection() - - this.getFeeds().forEach((feed: Feed) => { - channels = channels.concat(feed.getGuideChannels()) - }) - - return channels - } - - getGuideChannelNames(): Collection { - return this.getGuideChannels() - .map((channel: GuideChannel) => channel.siteName) - .uniq() - } - - getStreams(): Collection { - let streams = new Collection() - - this.getFeeds().forEach((feed: Feed) => { - streams = streams.concat(feed.getStreams()) - }) - - return streams - } - - getStreamNames(): Collection { - return this.getStreams() - .map((stream: Stream) => stream.getName()) - .uniq() - } - - getFeedFullNames(): Collection { - return this.getFeeds() - .map((feed: Feed) => feed.getFullName()) - .uniq() - } - - getName(): string { - return this.name || '' - } - - getId(): string { - return this.id || '' - } - - getAltNames(): Collection { - return this.altNames || new Collection() - } - - getLogos(): Collection { - function feed(logo: Logo): number { - if (!logo.feed) return 1 - if (logo.feed.isMain) return 1 - - return 0 - } - - function format(logo: Logo): number { - const levelByFormat: { [key: string]: number } = { - SVG: 0, - PNG: 3, - APNG: 1, - WebP: 1, - AVIF: 1, - JPEG: 2, - GIF: 1 - } - - return logo.format ? levelByFormat[logo.format] : 0 - } - - function size(logo: Logo): number { - return Math.abs(512 - logo.width) + Math.abs(512 - logo.height) - } - - return this.logos.orderBy([feed, format, size], ['desc', 'desc', 'asc'], false) - } - - getLogo(): Logo | undefined { - return this.getLogos().first() - } - - hasLogo(): boolean { - return this.getLogos().notEmpty() - } - - getLogoUrl(): string { - const logo = this.getLogo() - if (!logo) return '' - - return logo.url || '' - } - - getSearchable(): ChannelSearchableData { - return { - id: this.getId(), - name: this.getName(), - altNames: this.getAltNames().all(), - guideNames: this.getGuideChannelNames().all(), - streamNames: this.getStreamNames().all(), - feedFullNames: this.getFeedFullNames().all() - } - } -} +import { ChannelData, ChannelSearchableData } from '../types/channel' +import { Collection, Dictionary } from '@freearhey/core' +import { Stream, Feed, Logo, GuideChannel } from './' + +export class Channel { + id?: string + name?: string + altNames?: Collection + network?: string + owners?: Collection + countryCode?: string + subdivisionCode?: string + cityName?: string + categoryIds?: Collection + isNSFW = false + launched?: string + closed?: string + replacedBy?: string + website?: string + feeds?: Collection + logos: Collection = new Collection() + + constructor(data?: ChannelData) { + if (!data) return + + this.id = data.id + this.name = data.name + this.altNames = new Collection(data.alt_names) + this.network = data.network || undefined + this.owners = new Collection(data.owners) + this.countryCode = data.country + this.subdivisionCode = data.subdivision || undefined + this.cityName = data.city || undefined + this.categoryIds = new Collection(data.categories) + this.isNSFW = data.is_nsfw + this.launched = data.launched || undefined + this.closed = data.closed || undefined + this.replacedBy = data.replaced_by || undefined + this.website = data.website || undefined + } + + withFeeds(feedsGroupedByChannelId: Dictionary): this { + if (this.id) this.feeds = new Collection(feedsGroupedByChannelId.get(this.id)) + + return this + } + + withLogos(logosGroupedByChannelId: Dictionary): this { + if (this.id) this.logos = new Collection(logosGroupedByChannelId.get(this.id)) + + return this + } + + getFeeds(): Collection { + if (!this.feeds) return new Collection() + + return this.feeds + } + + getGuideChannels(): Collection { + let channels = new Collection() + + this.getFeeds().forEach((feed: Feed) => { + channels = channels.concat(feed.getGuideChannels()) + }) + + return channels + } + + getGuideChannelNames(): Collection { + return this.getGuideChannels() + .map((channel: GuideChannel) => channel.siteName) + .uniq() + } + + getStreams(): Collection { + let streams = new Collection() + + this.getFeeds().forEach((feed: Feed) => { + streams = streams.concat(feed.getStreams()) + }) + + return streams + } + + getStreamNames(): Collection { + return this.getStreams() + .map((stream: Stream) => stream.getName()) + .uniq() + } + + getFeedFullNames(): Collection { + return this.getFeeds() + .map((feed: Feed) => feed.getFullName()) + .uniq() + } + + getName(): string { + return this.name || '' + } + + getId(): string { + return this.id || '' + } + + getAltNames(): Collection { + return this.altNames || new Collection() + } + + getLogos(): Collection { + function feed(logo: Logo): number { + if (!logo.feed) return 1 + if (logo.feed.isMain) return 1 + + return 0 + } + + function format(logo: Logo): number { + const levelByFormat: Record = { + SVG: 0, + PNG: 3, + APNG: 1, + WebP: 1, + AVIF: 1, + JPEG: 2, + GIF: 1 + } + + return logo.format ? levelByFormat[logo.format] : 0 + } + + function size(logo: Logo): number { + return Math.abs(512 - logo.width) + Math.abs(512 - logo.height) + } + + return this.logos.orderBy([feed, format, size], ['desc', 'desc', 'asc'], false) + } + + getLogo(): Logo | undefined { + return this.getLogos().first() + } + + hasLogo(): boolean { + return this.getLogos().notEmpty() + } + + getLogoUrl(): string { + const logo = this.getLogo() + if (!logo) return '' + + return logo.url || '' + } + + getSearchable(): ChannelSearchableData { + return { + id: this.getId(), + name: this.getName(), + altNames: this.getAltNames().all(), + guideNames: this.getGuideChannelNames().all(), + streamNames: this.getStreamNames().all(), + feedFullNames: this.getFeedFullNames().all() + } + } +} diff --git a/scripts/models/channelList.ts b/scripts/models/channelList.ts index d312e71c..951e0a90 100644 --- a/scripts/models/channelList.ts +++ b/scripts/models/channelList.ts @@ -1,77 +1,77 @@ -import { Collection } from '@freearhey/core' -import epgGrabber from 'epg-grabber' - -export class ChannelList { - channels: Collection = new Collection() - - constructor(data: { channels: epgGrabber.Channel[] }) { - this.channels = new Collection(data.channels) - } - - add(channel: epgGrabber.Channel): this { - this.channels.add(channel) - - return this - } - - get(siteId: string): epgGrabber.Channel | undefined { - return this.channels.find((channel: epgGrabber.Channel) => channel.site_id == siteId) - } - - sort(): this { - this.channels = this.channels.orderBy([ - (channel: epgGrabber.Channel) => channel.lang || '_', - (channel: epgGrabber.Channel) => (channel.xmltv_id ? channel.xmltv_id.toLowerCase() : '0'), - (channel: epgGrabber.Channel) => channel.site_id - ]) - - return this - } - - toString() { - function escapeString(value: string, defaultValue: string = '') { - if (!value) return defaultValue - - const regex = new RegExp( - '((?:[\0-\x08\x0B\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))|([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|(?:\\uD83F[\\uDFFE\\uDFFF])|(?:\\uD87F[\\uDF' + - 'FE\\uDFFF])|(?:\\uD8BF[\\uDFFE\\uDFFF])|(?:\\uD8FF[\\uDFFE\\uDFFF])|(?:\\uD93F[\\uDFFE\\uD' + - 'FFF])|(?:\\uD97F[\\uDFFE\\uDFFF])|(?:\\uD9BF[\\uDFFE\\uDFFF])|(?:\\uD9FF[\\uDFFE\\uDFFF])' + - '|(?:\\uDA3F[\\uDFFE\\uDFFF])|(?:\\uDA7F[\\uDFFE\\uDFFF])|(?:\\uDABF[\\uDFFE\\uDFFF])|(?:\\' + - 'uDAFF[\\uDFFE\\uDFFF])|(?:\\uDB3F[\\uDFFE\\uDFFF])|(?:\\uDB7F[\\uDFFE\\uDFFF])|(?:\\uDBBF' + - '[\\uDFFE\\uDFFF])|(?:\\uDBFF[\\uDFFE\\uDFFF])(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\' + - 'uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|' + - '(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))', - 'g' - ) - - value = String(value || '').replace(regex, '') - - return value - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(/\n|\r/g, ' ') - .replace(/ +/g, ' ') - .trim() - } - - let output = '\r\n\r\n' - - this.channels.forEach((channel: epgGrabber.Channel) => { - const logo = channel.logo ? ` logo="${channel.logo}"` : '' - const xmltv_id = channel.xmltv_id ? escapeString(channel.xmltv_id) : '' - const lang = channel.lang || '' - const site_id = channel.site_id || '' - const site = channel.site || '' - const displayName = channel.name ? escapeString(channel.name) : '' - - output += ` ${displayName}\r\n` - }) - - output += '\r\n' - - return output - } -} +import { Collection } from '@freearhey/core' +import epgGrabber from 'epg-grabber' + +export class ChannelList { + channels: Collection = new Collection() + + constructor(data: { channels: epgGrabber.Channel[] }) { + this.channels = new Collection(data.channels) + } + + add(channel: epgGrabber.Channel): this { + this.channels.add(channel) + + return this + } + + get(siteId: string): epgGrabber.Channel | undefined { + return this.channels.find((channel: epgGrabber.Channel) => channel.site_id == siteId) + } + + sort(): this { + this.channels = this.channels.orderBy([ + (channel: epgGrabber.Channel) => channel.lang || '_', + (channel: epgGrabber.Channel) => (channel.xmltv_id ? channel.xmltv_id.toLowerCase() : '0'), + (channel: epgGrabber.Channel) => channel.site_id + ]) + + return this + } + + toString() { + function escapeString(value: string, defaultValue = '') { + if (!value) return defaultValue + + const regex = new RegExp( + '((?:[\0-\x08\x0B\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))|([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|(?:\\uD83F[\\uDFFE\\uDFFF])|(?:\\uD87F[\\uDF' + + 'FE\\uDFFF])|(?:\\uD8BF[\\uDFFE\\uDFFF])|(?:\\uD8FF[\\uDFFE\\uDFFF])|(?:\\uD93F[\\uDFFE\\uD' + + 'FFF])|(?:\\uD97F[\\uDFFE\\uDFFF])|(?:\\uD9BF[\\uDFFE\\uDFFF])|(?:\\uD9FF[\\uDFFE\\uDFFF])' + + '|(?:\\uDA3F[\\uDFFE\\uDFFF])|(?:\\uDA7F[\\uDFFE\\uDFFF])|(?:\\uDABF[\\uDFFE\\uDFFF])|(?:\\' + + 'uDAFF[\\uDFFE\\uDFFF])|(?:\\uDB3F[\\uDFFE\\uDFFF])|(?:\\uDB7F[\\uDFFE\\uDFFF])|(?:\\uDBBF' + + '[\\uDFFE\\uDFFF])|(?:\\uDBFF[\\uDFFE\\uDFFF])(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\' + + 'uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|' + + '(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))', + 'g' + ) + + value = String(value || '').replace(regex, '') + + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/\n|\r/g, ' ') + .replace(/ +/g, ' ') + .trim() + } + + let output = '\r\n\r\n' + + this.channels.forEach((channel: epgGrabber.Channel) => { + const logo = channel.logo ? ` logo="${channel.logo}"` : '' + const xmltv_id = channel.xmltv_id ? escapeString(channel.xmltv_id) : '' + const lang = channel.lang || '' + const site_id = channel.site_id || '' + const site = channel.site || '' + const displayName = channel.name ? escapeString(channel.name) : '' + + output += ` ${displayName}\r\n` + }) + + output += '\r\n' + + return output + } +} diff --git a/scripts/models/feed.ts b/scripts/models/feed.ts index bb2c7020..7e91e305 100644 --- a/scripts/models/feed.ts +++ b/scripts/models/feed.ts @@ -1,124 +1,124 @@ -import { Collection, Dictionary } from '@freearhey/core' -import { FeedData } from '../types/feed' -import { Logo, Channel } from '.' - -export class Feed { - channelId: string - channel?: Channel - id: string - name: string - isMain: boolean - broadcastAreaCodes: Collection - languageCodes: Collection - timezoneIds: Collection - videoFormat: string - guideChannels?: Collection - streams?: Collection - logos: Collection = new Collection() - - constructor(data: FeedData) { - this.channelId = data.channel - this.id = data.id - this.name = data.name - this.isMain = data.is_main - this.broadcastAreaCodes = new Collection(data.broadcast_area) - this.languageCodes = new Collection(data.languages) - this.timezoneIds = new Collection(data.timezones) - this.videoFormat = data.video_format - } - - withChannel(channelsKeyById: Dictionary): this { - this.channel = channelsKeyById.get(this.channelId) - - return this - } - - withStreams(streamsGroupedById: Dictionary): this { - this.streams = new Collection(streamsGroupedById.get(`${this.channelId}@${this.id}`)) - - if (this.isMain) { - this.streams = this.streams.concat(new Collection(streamsGroupedById.get(this.channelId))) - } - - return this - } - - withGuideChannels(guideChannelsGroupedByStreamId: Dictionary): this { - this.guideChannels = new Collection( - guideChannelsGroupedByStreamId.get(`${this.channelId}@${this.id}`) - ) - - if (this.isMain) { - this.guideChannels = this.guideChannels.concat( - new Collection(guideChannelsGroupedByStreamId.get(this.channelId)) - ) - } - - return this - } - - withLogos(logosGroupedByStreamId: Dictionary): this { - this.logos = new Collection(logosGroupedByStreamId.get(this.getStreamId())) - - return this - } - - getGuideChannels(): Collection { - if (!this.guideChannels) return new Collection() - - return this.guideChannels - } - - getStreams(): Collection { - if (!this.streams) return new Collection() - - return this.streams - } - - getFullName(): string { - if (!this.channel) return '' - - return `${this.channel.name} ${this.name}` - } - - getStreamId(): string { - return `${this.channelId}@${this.id}` - } - - getLogos(): Collection { - function format(logo: Logo): number { - const levelByFormat: { [key: string]: number } = { - SVG: 0, - PNG: 3, - APNG: 1, - WebP: 1, - AVIF: 1, - JPEG: 2, - GIF: 1 - } - - return logo.format ? levelByFormat[logo.format] : 0 - } - - function size(logo: Logo): number { - return Math.abs(512 - logo.width) + Math.abs(512 - logo.height) - } - - return this.logos.orderBy([format, size], ['desc', 'asc'], false) - } - - getLogo(): Logo | undefined { - return this.getLogos().first() - } - - hasLogo(): boolean { - return this.getLogos().notEmpty() - } - - getLogoUrl(): string { - const logo = this.getLogo() - if (!logo) return '' - - return logo.url || '' - } -} +import { Collection, Dictionary } from '@freearhey/core' +import { FeedData } from '../types/feed' +import { Logo, Channel } from '.' + +export class Feed { + channelId: string + channel?: Channel + id: string + name: string + isMain: boolean + broadcastAreaCodes: Collection + languageCodes: Collection + timezoneIds: Collection + videoFormat: string + guideChannels?: Collection + streams?: Collection + logos: Collection = new Collection() + + constructor(data: FeedData) { + this.channelId = data.channel + this.id = data.id + this.name = data.name + this.isMain = data.is_main + this.broadcastAreaCodes = new Collection(data.broadcast_area) + this.languageCodes = new Collection(data.languages) + this.timezoneIds = new Collection(data.timezones) + this.videoFormat = data.video_format + } + + withChannel(channelsKeyById: Dictionary): this { + this.channel = channelsKeyById.get(this.channelId) + + return this + } + + withStreams(streamsGroupedById: Dictionary): this { + this.streams = new Collection(streamsGroupedById.get(`${this.channelId}@${this.id}`)) + + if (this.isMain) { + this.streams = this.streams.concat(new Collection(streamsGroupedById.get(this.channelId))) + } + + return this + } + + withGuideChannels(guideChannelsGroupedByStreamId: Dictionary): this { + this.guideChannels = new Collection( + guideChannelsGroupedByStreamId.get(`${this.channelId}@${this.id}`) + ) + + if (this.isMain) { + this.guideChannels = this.guideChannels.concat( + new Collection(guideChannelsGroupedByStreamId.get(this.channelId)) + ) + } + + return this + } + + withLogos(logosGroupedByStreamId: Dictionary): this { + this.logos = new Collection(logosGroupedByStreamId.get(this.getStreamId())) + + return this + } + + getGuideChannels(): Collection { + if (!this.guideChannels) return new Collection() + + return this.guideChannels + } + + getStreams(): Collection { + if (!this.streams) return new Collection() + + return this.streams + } + + getFullName(): string { + if (!this.channel) return '' + + return `${this.channel.name} ${this.name}` + } + + getStreamId(): string { + return `${this.channelId}@${this.id}` + } + + getLogos(): Collection { + function format(logo: Logo): number { + const levelByFormat: Record = { + SVG: 0, + PNG: 3, + APNG: 1, + WebP: 1, + AVIF: 1, + JPEG: 2, + GIF: 1 + } + + return logo.format ? levelByFormat[logo.format] : 0 + } + + function size(logo: Logo): number { + return Math.abs(512 - logo.width) + Math.abs(512 - logo.height) + } + + return this.logos.orderBy([format, size], ['desc', 'asc'], false) + } + + getLogo(): Logo | undefined { + return this.getLogos().first() + } + + hasLogo(): boolean { + return this.getLogos().notEmpty() + } + + getLogoUrl(): string { + const logo = this.getLogo() + if (!logo) return '' + + return logo.url || '' + } +} diff --git a/scripts/models/guide.ts b/scripts/models/guide.ts index b6026743..4072c8bc 100644 --- a/scripts/models/guide.ts +++ b/scripts/models/guide.ts @@ -1,35 +1,35 @@ -import { Collection, DateTime } from '@freearhey/core' -import { generateXMLTV } from 'epg-grabber' - -type GuideData = { - channels: Collection - programs: Collection - filepath: string - gzip: boolean -} - -export class Guide { - channels: Collection - programs: Collection - filepath: string - gzip: boolean - - constructor({ channels, programs, filepath, gzip }: GuideData) { - this.channels = channels - this.programs = programs - this.filepath = filepath - this.gzip = gzip || false - } - - toString() { - const currDate = new DateTime(process.env.CURR_DATE || new Date().toISOString(), { - timezone: 'UTC' - }) - - return generateXMLTV({ - channels: this.channels.all(), - programs: this.programs.all(), - date: currDate.toJSON() - }) - } -} +import { Collection, DateTime } from '@freearhey/core' +import { generateXMLTV } from 'epg-grabber' + +interface GuideData { + channels: Collection + programs: Collection + filepath: string + gzip: boolean +} + +export class Guide { + channels: Collection + programs: Collection + filepath: string + gzip: boolean + + constructor({ channels, programs, filepath, gzip }: GuideData) { + this.channels = channels + this.programs = programs + this.filepath = filepath + this.gzip = gzip || false + } + + toString() { + const currDate = new DateTime(process.env.CURR_DATE || new Date().toISOString(), { + timezone: 'UTC' + }) + + return generateXMLTV({ + channels: this.channels.all(), + programs: this.programs.all(), + date: currDate.toJSON() + }) + } +} 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/scripts/models/issue.ts b/scripts/models/issue.ts index d9653a4f..a26e71ff 100644 --- a/scripts/models/issue.ts +++ b/scripts/models/issue.ts @@ -1,24 +1,24 @@ -import { Dictionary } from '@freearhey/core' -import { OWNER, REPO } from '../constants' - -type IssueProps = { - number: number - labels: string[] - data: Dictionary -} - -export class Issue { - number: number - labels: string[] - data: Dictionary - - constructor({ number, labels, data }: IssueProps) { - this.number = number - this.labels = labels - this.data = data - } - - getURL() { - return `https://github.com/${OWNER}/${REPO}/issues/${this.number}` - } -} +import { Dictionary } from '@freearhey/core' +import { OWNER, REPO } from '../constants' + +interface IssueProps { + number: number + labels: string[] + data: Dictionary +} + +export class Issue { + number: number + labels: string[] + data: Dictionary + + constructor({ number, labels, data }: IssueProps) { + this.number = number + this.labels = labels + this.data = data + } + + getURL() { + return `https://github.com/${OWNER}/${REPO}/issues/${this.number}` + } +} diff --git a/scripts/models/logo.ts b/scripts/models/logo.ts index d864a3fb..e08f443e 100644 --- a/scripts/models/logo.ts +++ b/scripts/models/logo.ts @@ -1,41 +1,41 @@ -import { Collection, type Dictionary } from '@freearhey/core' -import type { LogoData } from '../types/logo' -import { type Feed } from './feed' - -export class Logo { - channelId?: string - feedId?: string - feed?: Feed - tags: Collection = new Collection() - width: number = 0 - height: number = 0 - format?: string - url?: string - - constructor(data?: LogoData) { - if (!data) return - - this.channelId = data.channel - this.feedId = data.feed || undefined - this.tags = new Collection(data.tags) - this.width = data.width - this.height = data.height - this.format = data.format || undefined - this.url = data.url - } - - withFeed(feedsKeyByStreamId: Dictionary): this { - if (!this.feedId) return this - - 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}` - } -} +import { Collection, type Dictionary } from '@freearhey/core' +import type { LogoData } from '../types/logo' +import { type Feed } from './feed' + +export class Logo { + channelId?: string + feedId?: string + feed?: Feed + tags: Collection = new Collection() + width = 0 + height = 0 + format?: string + url?: string + + constructor(data?: LogoData) { + if (!data) return + + this.channelId = data.channel + this.feedId = data.feed || undefined + this.tags = new Collection(data.tags) + this.width = data.width + this.height = data.height + this.format = data.format || undefined + this.url = data.url + } + + withFeed(feedsKeyByStreamId: Dictionary): this { + if (!this.feedId) return this + + 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}` + } +} diff --git a/scripts/models/site.ts b/scripts/models/site.ts index fa95165f..d4ddfdaa 100644 --- a/scripts/models/site.ts +++ b/scripts/models/site.ts @@ -1,63 +1,63 @@ -import { Collection } from '@freearhey/core' -import { Issue } from './' - -enum StatusCode { - DOWN = 'down', - WARNING = 'warning', - OK = 'ok' -} - -type Status = { - code: StatusCode - emoji: string -} - -type SiteProps = { - domain: string - totalChannels?: number - markedChannels?: number - issues: Collection -} - -export class Site { - domain: string - totalChannels: number - markedChannels: number - issues: Collection - - constructor({ domain, totalChannels = 0, markedChannels = 0, issues }: SiteProps) { - this.domain = domain - this.totalChannels = totalChannels - this.markedChannels = markedChannels - this.issues = issues - } - - getStatus(): Status { - const issuesWithStatusDown = this.issues.filter((issue: Issue) => - issue.labels.find(label => label === 'status:down') - ) - if (issuesWithStatusDown.notEmpty()) - return { - code: StatusCode.DOWN, - emoji: '🔴' - } - - const issuesWithStatusWarning = this.issues.filter((issue: Issue) => - issue.labels.find(label => label === 'status:warning') - ) - if (issuesWithStatusWarning.notEmpty()) - return { - code: StatusCode.WARNING, - emoji: '🟡' - } - - return { - code: StatusCode.OK, - emoji: '🟢' - } - } - - getIssues(): Collection { - return this.issues.map((issue: Issue) => issue.getURL()) - } -} +import { Collection } from '@freearhey/core' +import { Issue } from './' + +enum StatusCode { + DOWN = 'down', + WARNING = 'warning', + OK = 'ok' +} + +interface Status { + code: StatusCode + emoji: string +} + +interface SiteProps { + domain: string + totalChannels?: number + markedChannels?: number + issues: Collection +} + +export class Site { + domain: string + totalChannels: number + markedChannels: number + issues: Collection + + constructor({ domain, totalChannels = 0, markedChannels = 0, issues }: SiteProps) { + this.domain = domain + this.totalChannels = totalChannels + this.markedChannels = markedChannels + this.issues = issues + } + + getStatus(): Status { + const issuesWithStatusDown = this.issues.filter((issue: Issue) => + issue.labels.find(label => label === 'status:down') + ) + if (issuesWithStatusDown.notEmpty()) + return { + code: StatusCode.DOWN, + emoji: '🔴' + } + + const issuesWithStatusWarning = this.issues.filter((issue: Issue) => + issue.labels.find(label => label === 'status:warning') + ) + if (issuesWithStatusWarning.notEmpty()) + return { + code: StatusCode.WARNING, + emoji: '🟡' + } + + return { + code: StatusCode.OK, + emoji: '🟢' + } + } + + getIssues(): Collection { + return this.issues.map((issue: Issue) => issue.getURL()) + } +} diff --git a/scripts/models/stream.ts b/scripts/models/stream.ts index 6ac1636b..c519bdfb 100644 --- a/scripts/models/stream.ts +++ b/scripts/models/stream.ts @@ -1,58 +1,58 @@ -import type { StreamData } from '../types/stream' -import { Feed, Channel } from './index' - -export class Stream { - name?: string - url: string - id?: string - channelId?: string - channel?: Channel - feedId?: string - feed?: Feed - filepath?: string - line?: number - label?: string - verticalResolution?: number - isInterlaced?: boolean - referrer?: string - userAgent?: string - groupTitle: string = 'Undefined' - removed: boolean = false - - constructor(data: StreamData) { - const id = data.channel && data.feed ? [data.channel, data.feed].join('@') : data.channel - const { verticalResolution, isInterlaced } = parseQuality(data.quality) - - this.id = id || undefined - this.channelId = data.channel || undefined - this.feedId = data.feed || undefined - this.name = data.name || undefined - this.url = data.url - this.referrer = data.referrer || undefined - this.userAgent = data.user_agent || undefined - this.verticalResolution = verticalResolution || undefined - this.isInterlaced = isInterlaced || undefined - this.label = data.label || undefined - } - - getId(): string { - return this.id || '' - } - - getName(): string { - return this.name || '' - } -} - -function parseQuality(quality: string | null): { - verticalResolution: number | null - isInterlaced: boolean | null -} { - if (!quality) return { verticalResolution: null, isInterlaced: null } - const [, verticalResolutionString] = quality.match(/^(\d+)/) || [null, undefined] - const isInterlaced = /i$/i.test(quality) - let verticalResolution = 0 - if (verticalResolutionString) verticalResolution = parseInt(verticalResolutionString) - - return { verticalResolution, isInterlaced } -} +import type { StreamData } from '../types/stream' +import { Feed, Channel } from './index' + +export class Stream { + name?: string + url: string + id?: string + channelId?: string + channel?: Channel + feedId?: string + feed?: Feed + filepath?: string + line?: number + label?: string + verticalResolution?: number + isInterlaced?: boolean + referrer?: string + userAgent?: string + groupTitle = 'Undefined' + removed = false + + constructor(data: StreamData) { + const id = data.channel && data.feed ? [data.channel, data.feed].join('@') : data.channel + const { verticalResolution, isInterlaced } = parseQuality(data.quality) + + this.id = id || undefined + this.channelId = data.channel || undefined + this.feedId = data.feed || undefined + this.name = data.name || undefined + this.url = data.url + this.referrer = data.referrer || undefined + this.userAgent = data.user_agent || undefined + this.verticalResolution = verticalResolution || undefined + this.isInterlaced = isInterlaced || undefined + this.label = data.label || undefined + } + + getId(): string { + return this.id || '' + } + + getName(): string { + return this.name || '' + } +} + +function parseQuality(quality: string | null): { + verticalResolution: number | null + isInterlaced: boolean | null +} { + if (!quality) return { verticalResolution: null, isInterlaced: null } + const [, verticalResolutionString] = quality.match(/^(\d+)/) || [null, undefined] + const isInterlaced = /i$/i.test(quality) + let verticalResolution = 0 + if (verticalResolutionString) verticalResolution = parseInt(verticalResolutionString) + + return { verticalResolution, isInterlaced } +} diff --git a/scripts/templates/_config.js b/scripts/templates/_config.js index 2e40921d..b4eb9b46 100644 --- a/scripts/templates/_config.js +++ b/scripts/templates/_config.js @@ -1,16 +1,16 @@ -module.exports = { - site: '', - url({ channel, date }) { - return `https://example.com/api/${channel.site_id}/${date.format('YYYY-MM-DD')}` - }, - parser({ content }) { - try { - return JSON.parse(content) - } catch { - return [] - } - }, - channels() { - return [] - } -} +module.exports = { + site: '', + url({ channel, date }) { + return `https://example.com/api/${channel.site_id}/${date.format('YYYY-MM-DD')}` + }, + parser({ content }) { + try { + return JSON.parse(content) + } catch { + return [] + } + }, + channels() { + return [] + } +} diff --git a/scripts/templates/_test.js b/scripts/templates/_test.js index 6375d7e7..b02a2648 100644 --- a/scripts/templates/_test.js +++ b/scripts/templates/_test.js @@ -1,38 +1,38 @@ -const { parser, url } = require('./.config.js') -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-12', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'bbc1' } - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://example.com/api/bbc1/2025-01-12') -}) - -it('can parse response', () => { - const content = - '[{"title":"Program 1","start":"2025-01-12T00:00:00.000Z","stop":"2025-01-12T00:30:00.000Z"},{"title":"Program 2","start":"2025-01-12T00:30:00.000Z","stop":"2025-01-12T01:00:00.000Z"}]' - - const results = parser({ content }) - - expect(results.length).toBe(2) - expect(results[0]).toMatchObject({ - title: 'Program 1', - start: '2025-01-12T00:00:00.000Z', - stop: '2025-01-12T00:30:00.000Z' - }) - expect(results[1]).toMatchObject({ - title: 'Program 2', - start: '2025-01-12T00:30:00.000Z', - stop: '2025-01-12T01:00:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ content: '' }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./.config.js') +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-12', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'bbc1' } + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://example.com/api/bbc1/2025-01-12') +}) + +it('can parse response', () => { + const content = + '[{"title":"Program 1","start":"2025-01-12T00:00:00.000Z","stop":"2025-01-12T00:30:00.000Z"},{"title":"Program 2","start":"2025-01-12T00:30:00.000Z","stop":"2025-01-12T01:00:00.000Z"}]' + + const results = parser({ content }) + + expect(results.length).toBe(2) + expect(results[0]).toMatchObject({ + title: 'Program 1', + start: '2025-01-12T00:00:00.000Z', + stop: '2025-01-12T00:30:00.000Z' + }) + expect(results[1]).toMatchObject({ + title: 'Program 2', + start: '2025-01-12T00:30:00.000Z', + stop: '2025-01-12T01:00:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ content: '' }) + + expect(results).toMatchObject([]) +}) diff --git a/scripts/types/channel.d.ts b/scripts/types/channel.d.ts index b1d2237c..b2c709e1 100644 --- a/scripts/types/channel.d.ts +++ b/scripts/types/channel.d.ts @@ -1,27 +1,27 @@ -import { Collection } from '@freearhey/core' - -export type ChannelData = { - id: string - name: string - alt_names: string[] - network: string - owners: Collection - country: string - subdivision: string - city: string - categories: Collection - is_nsfw: boolean - launched: string - closed: string - replaced_by: string - website: string -} - -export type ChannelSearchableData = { - id: string - name: string - altNames: string[] - guideNames: string[] - streamNames: string[] - feedFullNames: string[] -} +import { Collection } from '@freearhey/core' + +export interface ChannelData { + id: string + name: string + alt_names: string[] + network: string + owners: Collection + country: string + subdivision: string + city: string + categories: Collection + is_nsfw: boolean + launched: string + closed: string + replaced_by: string + website: string +} + +export interface ChannelSearchableData { + id: string + name: string + altNames: string[] + guideNames: string[] + streamNames: string[] + feedFullNames: string[] +} diff --git a/scripts/types/dataLoader.d.ts b/scripts/types/dataLoader.d.ts index 135340e9..98d3d911 100644 --- a/scripts/types/dataLoader.d.ts +++ b/scripts/types/dataLoader.d.ts @@ -1,20 +1,20 @@ -import { Storage } from '@freearhey/core' - -export type DataLoaderProps = { - storage: Storage -} - -export type DataLoaderData = { - countries: object | object[] - regions: object | object[] - subdivisions: object | object[] - languages: object | object[] - categories: object | object[] - blocklist: object | object[] - channels: object | object[] - feeds: object | object[] - timezones: object | object[] - guides: object | object[] - streams: object | object[] - logos: object | object[] -} +import { Storage } from '@freearhey/core' + +export interface DataLoaderProps { + storage: Storage +} + +export interface DataLoaderData { + countries: object | object[] + regions: object | object[] + subdivisions: object | object[] + languages: object | object[] + categories: object | object[] + blocklist: object | object[] + channels: object | object[] + feeds: object | object[] + timezones: object | object[] + guides: object | object[] + streams: object | object[] + logos: object | object[] +} diff --git a/scripts/types/dataProcessor.d.ts b/scripts/types/dataProcessor.d.ts index f158f16e..e50915d1 100644 --- a/scripts/types/dataProcessor.d.ts +++ b/scripts/types/dataProcessor.d.ts @@ -1,16 +1,16 @@ -import { Collection, Dictionary } from '@freearhey/core' - -export type DataProcessorData = { - guideChannelsGroupedByStreamId: Dictionary - feedsGroupedByChannelId: Dictionary - logosGroupedByChannelId: Dictionary - logosGroupedByStreamId: Dictionary - feedsKeyByStreamId: Dictionary - streamsGroupedById: Dictionary - channelsKeyById: Dictionary - guideChannels: Collection - channels: Collection - streams: Collection - feeds: Collection - logos: Collection -} +import { Collection, Dictionary } from '@freearhey/core' + +export interface DataProcessorData { + guideChannelsGroupedByStreamId: Dictionary + feedsGroupedByChannelId: Dictionary + logosGroupedByChannelId: Dictionary + logosGroupedByStreamId: Dictionary + feedsKeyByStreamId: Dictionary + streamsGroupedById: Dictionary + channelsKeyById: Dictionary + guideChannels: Collection + channels: Collection + streams: Collection + feeds: Collection + logos: Collection +} diff --git a/scripts/types/feed.d.ts b/scripts/types/feed.d.ts index 00663a1b..00c49260 100644 --- a/scripts/types/feed.d.ts +++ b/scripts/types/feed.d.ts @@ -1,12 +1,12 @@ -import { Collection } from '@freearhey/core' - -export type FeedData = { - channel: string - id: string - name: string - is_main: boolean - broadcast_area: Collection - languages: Collection - timezones: Collection - video_format: string -} +import { Collection } from '@freearhey/core' + +export interface FeedData { + channel: string + id: string + name: string + is_main: boolean + broadcast_area: Collection + languages: Collection + timezones: Collection + video_format: string +} diff --git a/scripts/types/guide.d.ts b/scripts/types/guide.d.ts index 61ff6233..9fbe4da3 100644 --- a/scripts/types/guide.d.ts +++ b/scripts/types/guide.d.ts @@ -1,8 +1,8 @@ -export type GuideData = { - channel: string - feed: string - site: string - site_id: string - site_name: string - lang: string -} +export interface GuideData { + channel: string + feed: string + site: string + site_id: string + site_name: string + lang: string +} diff --git a/scripts/types/langs.d.ts b/scripts/types/langs.d.ts index 74921c68..60fb498a 100644 --- a/scripts/types/langs.d.ts +++ b/scripts/types/langs.d.ts @@ -1 +1 @@ -declare module 'langs' +declare module 'langs' diff --git a/scripts/types/logo.d.ts b/scripts/types/logo.d.ts index c77f4799..d8c54b04 100644 --- a/scripts/types/logo.d.ts +++ b/scripts/types/logo.d.ts @@ -1,9 +1,9 @@ -export type LogoData = { - channel: string - feed: string | null - tags: string[] - width: number - height: number - format: string | null - url: string -} +export interface LogoData { + channel: string + feed: string | null + tags: string[] + width: number + height: number + format: string | null + url: string +} diff --git a/scripts/types/stream.d.ts b/scripts/types/stream.d.ts index adae13cf..c3365889 100644 --- a/scripts/types/stream.d.ts +++ b/scripts/types/stream.d.ts @@ -1,10 +1,10 @@ -export type StreamData = { - channel: string | null - feed: string | null - name?: string - url: string - referrer: string | null - user_agent: string | null - quality: string | null - label: string | null -} +export interface StreamData { + channel: string | null + feed: string | null + name?: string + url: string + referrer: string | null + user_agent: string | null + quality: string | null + label: string | null +} diff --git a/sites/9tv.co.il/9tv.co.il.config.js b/sites/9tv.co.il/9tv.co.il.config.js index b55f6a10..02384c5f 100644 --- a/sites/9tv.co.il/9tv.co.il.config.js +++ b/sites/9tv.co.il/9tv.co.il.config.js @@ -1,69 +1,69 @@ -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: '9tv.co.il', - days: 2, - url: function ({ date }) { - return `https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=${date.format( - 'DD/MM/YYYY 00:00:00' - )}` - }, - parser: function ({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - const start = parseStart($item, date) - if (prev) prev.stop = start - const stop = start.add(1, 'h') - programs.push({ - title: parseTitle($item), - image: parseImage($item), - description: parseDescription($item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart($item, date) { - let time = $item('a > div.guide_list_time').text().trim() - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Jerusalem') -} - -function parseImage($item) { - const backgroundImage = $item('a > div.guide_info_group > div.guide_info_pict').css( - 'background-image' - ) - if (!backgroundImage) return null - const [, relativePath] = backgroundImage.match(/url\((.*)\)/) || [null, null] - - return relativePath ? `https://www.9tv.co.il${relativePath}` : null -} - -function parseDescription($item) { - return $item('a > div.guide_info_group > div.guide_txt_group > div').text().trim() -} - -function parseTitle($item) { - return $item('a > div.guide_info_group > div.guide_txt_group > h3').text().trim() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('li').toArray() -} +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: '9tv.co.il', + days: 2, + url: function ({ date }) { + return `https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=${date.format( + 'DD/MM/YYYY 00:00:00' + )}` + }, + parser: function ({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + const start = parseStart($item, date) + if (prev) prev.stop = start + const stop = start.add(1, 'h') + programs.push({ + title: parseTitle($item), + image: parseImage($item), + description: parseDescription($item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart($item, date) { + let time = $item('a > div.guide_list_time').text().trim() + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Jerusalem') +} + +function parseImage($item) { + const backgroundImage = $item('a > div.guide_info_group > div.guide_info_pict').css( + 'background-image' + ) + if (!backgroundImage) return null + const [, relativePath] = backgroundImage.match(/url\((.*)\)/) || [null, null] + + return relativePath ? `https://www.9tv.co.il${relativePath}` : null +} + +function parseDescription($item) { + return $item('a > div.guide_info_group > div.guide_txt_group > div').text().trim() +} + +function parseTitle($item) { + return $item('a > div.guide_info_group > div.guide_txt_group > h3').text().trim() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('li').toArray() +} diff --git a/sites/9tv.co.il/9tv.co.il.test.js b/sites/9tv.co.il/9tv.co.il.test.js index 0672f1dd..7a1e9658 100644 --- a/sites/9tv.co.il/9tv.co.il.test.js +++ b/sites/9tv.co.il/9tv.co.il.test.js @@ -1,55 +1,56 @@ -const { parser, url } = require('./9tv.co.il.config.js') -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('2022-03-06', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '#', - xmltv_id: 'Channel9.il' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=06/03/2022 00:00:00' - ) -}) - -it('can parse response', () => { - const content = - '
  • 06:30

    Слепая

    Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы. 
  • 09:10

    Орел и решка. Морской сезон

    Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.
  • ' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-06T04:30:00.000Z', - stop: '2022-03-06T07:10:00.000Z', - title: 'Слепая', - image: 'https://www.9tv.co.il/download/pictures/img_id=8484.jpg', - description: - 'Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы.' - }, - { - start: '2022-03-06T07:10:00.000Z', - stop: '2022-03-06T08:10:00.000Z', - image: 'https://www.9tv.co.il/download/pictures/img_id=23694.jpg', - title: 'Орел и решка. Морской сезон', - description: 'Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./9tv.co.il.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('2022-03-06', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '#', + xmltv_id: 'Channel9.il' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=06/03/2022 00:00:00' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-06T04:30:00.000Z', + stop: '2022-03-06T07:10:00.000Z', + title: 'Слепая', + image: 'https://www.9tv.co.il/download/pictures/img_id=8484.jpg', + description: + 'Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы.' + }, + { + start: '2022-03-06T07:10:00.000Z', + stop: '2022-03-06T08:10:00.000Z', + image: 'https://www.9tv.co.il/download/pictures/img_id=23694.jpg', + title: 'Орел и решка. Морской сезон', + description: 'Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/9tv.co.il/__data__/content.html b/sites/9tv.co.il/__data__/content.html new file mode 100644 index 00000000..290eb76e --- /dev/null +++ b/sites/9tv.co.il/__data__/content.html @@ -0,0 +1 @@ +
  • 06:30

    Слепая

    Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы. 
  • 09:10

    Орел и решка. Морской сезон

    Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.
  • \ No newline at end of file diff --git a/sites/9tv.co.il/__data__/no_content.html b/sites/9tv.co.il/__data__/no_content.html new file mode 100644 index 00000000..6fedfd4c --- /dev/null +++ b/sites/9tv.co.il/__data__/no_content.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sites/abc.net.au/abc.net.au.config.js b/sites/abc.net.au/abc.net.au.config.js index a7e2a393..eb8e3a87 100644 --- a/sites/abc.net.au/abc.net.au.config.js +++ b/sites/abc.net.au/abc.net.au.config.js @@ -1,122 +1,122 @@ -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') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'abc.net.au', - days: 3, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url({ date, channel }) { - const [region] = channel.site_id.split('#') - - return `https://cdn.iview.abc.net.au/epg/processed/${region}_${date.format('YYYY-MM-DD')}.json` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.title, - sub_title: item.episode_title, - category: item.genres, - description: item.description, - season: parseSeason(item), - episode: parseEpisode(item), - rating: parseRating(item), - image: parseImage(item), - start: parseTime(item.start_time), - stop: parseTime(item.end_time) - }) - }) - - return programs - }, - async channels({ region = 'syd' }) { - const now = dayjs() - const regions = { - syd: 'Sydney', - mel: 'Melbourne', - bri: 'Brisbane', - gc: 'GoldCoast', - per: 'Perth', - adl: 'Adelaide', - hbr: 'Hobart', - drw: 'Darwin', - cbr: 'Canberra', - nsw: 'New South Wales', - vic: 'Victoria', - tsv: 'Townsville', - qld: 'Queensland', - wa: 'Western Australia', - sa: 'South Australia', - tas: 'Tasmania', - nt: 'Northern Territory' - } - - let channels = [] - const regionName = regions[region] - const data = await axios - .get( - `https://cdn.iview.abc.net.au/epg/processed/${regionName}_${now.format('YYYY-MM-DD')}.json` - ) - .then(r => r.data) - .catch(console.log) - - for (let item of data.schedule) { - channels.push({ - lang: 'en', - site_id: `${regionName}#${item.channel}`, - name: item.channel - }) - } - - return channels - } -} - -function parseItems(content, channel) { - try { - const data = JSON.parse(content) - if (!data) return [] - if (!Array.isArray(data.schedule)) return [] - - const [, channelId] = channel.site_id.split('#') - const channelData = data.schedule.find(i => i.channel == channelId) - return channelData.listing && Array.isArray(channelData.listing) ? channelData.listing : [] - } catch { - return [] - } -} - -function parseSeason(item) { - return item.series_num || null -} -function parseEpisode(item) { - return item.episode_num || null -} -function parseTime(time) { - return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'Australia/Sydney') -} -function parseImage(item) { - return item.image_file - ? `https://www.abc.net.au/tv/common/images/publicity/${item.image_file}` - : null -} -function parseRating(item) { - return item.rating - ? { - system: 'ACB', - value: item.rating - } - : null -} +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') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'abc.net.au', + days: 3, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url({ date, channel }) { + const [region] = channel.site_id.split('#') + + return `https://cdn.iview.abc.net.au/epg/processed/${region}_${date.format('YYYY-MM-DD')}.json` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.title, + sub_title: item.episode_title, + category: item.genres, + description: item.description, + season: parseSeason(item), + episode: parseEpisode(item), + rating: parseRating(item), + image: parseImage(item), + start: parseTime(item.start_time), + stop: parseTime(item.end_time) + }) + }) + + return programs + }, + async channels({ region = 'syd' }) { + const now = dayjs() + const regions = { + syd: 'Sydney', + mel: 'Melbourne', + bri: 'Brisbane', + gc: 'GoldCoast', + per: 'Perth', + adl: 'Adelaide', + hbr: 'Hobart', + drw: 'Darwin', + cbr: 'Canberra', + nsw: 'New South Wales', + vic: 'Victoria', + tsv: 'Townsville', + qld: 'Queensland', + wa: 'Western Australia', + sa: 'South Australia', + tas: 'Tasmania', + nt: 'Northern Territory' + } + + let channels = [] + const regionName = regions[region] + const data = await axios + .get( + `https://cdn.iview.abc.net.au/epg/processed/${regionName}_${now.format('YYYY-MM-DD')}.json` + ) + .then(r => r.data) + .catch(console.log) + + for (let item of data.schedule) { + channels.push({ + lang: 'en', + site_id: `${regionName}#${item.channel}`, + name: item.channel + }) + } + + return channels + } +} + +function parseItems(content, channel) { + try { + const data = JSON.parse(content) + if (!data) return [] + if (!Array.isArray(data.schedule)) return [] + + const [, channelId] = channel.site_id.split('#') + const channelData = data.schedule.find(i => i.channel == channelId) + return channelData.listing && Array.isArray(channelData.listing) ? channelData.listing : [] + } catch { + return [] + } +} + +function parseSeason(item) { + return item.series_num || null +} +function parseEpisode(item) { + return item.episode_num || null +} +function parseTime(time) { + return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'Australia/Sydney') +} +function parseImage(item) { + return item.image_file + ? `https://www.abc.net.au/tv/common/images/publicity/${item.image_file}` + : null +} +function parseRating(item) { + return item.rating + ? { + system: 'ACB', + value: item.rating + } + : null +} diff --git a/sites/abc.net.au/abc.net.au.test.js b/sites/abc.net.au/abc.net.au.test.js index e4f87531..abb4e631 100644 --- a/sites/abc.net.au/abc.net.au.test.js +++ b/sites/abc.net.au/abc.net.au.test.js @@ -1,51 +1,51 @@ -const { parser, url } = require('./abc.net.au.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2025-02-04', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'Sydney#ABC1' } - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://cdn.iview.abc.net.au/epg/processed/Sydney_2025-02-04.json' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const results = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(30) - expect(results[0]).toMatchObject({ - title: "Julia Zemiro's Home Delivery", - sub_title: 'Maggie Beer', - description: - "The kitchen Maggie Beer made famous in The Cook and the Chef may be in the heart of the Barossa Valley, but our most beloved foodie meets up with Julia where she grew up in Sydney's Lakemba.", - category: ['Entertainment', 'Factual'], - rating: { - system: 'ACB', - value: 'G' - }, - season: null, - episode: null, - image: 'https://www.abc.net.au/tv/common/images/publicity/LE1761H002S00_460.jpg', - start: '2025-02-03T12:40:00.000Z', - stop: '2025-02-03T13:09:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')), - channel - }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./abc.net.au.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2025-02-04', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'Sydney#ABC1' } + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://cdn.iview.abc.net.au/epg/processed/Sydney_2025-02-04.json' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(30) + expect(results[0]).toMatchObject({ + title: "Julia Zemiro's Home Delivery", + sub_title: 'Maggie Beer', + description: + "The kitchen Maggie Beer made famous in The Cook and the Chef may be in the heart of the Barossa Valley, but our most beloved foodie meets up with Julia where she grew up in Sydney's Lakemba.", + category: ['Entertainment', 'Factual'], + rating: { + system: 'ACB', + value: 'G' + }, + season: null, + episode: null, + image: 'https://www.abc.net.au/tv/common/images/publicity/LE1761H002S00_460.jpg', + start: '2025-02-03T12:40:00.000Z', + stop: '2025-02-03T13:09:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')), + channel + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/allente.dk/__data__/content.json b/sites/allente.dk/__data__/content.json new file mode 100644 index 00000000..bdac9a55 --- /dev/null +++ b/sites/allente.dk/__data__/content.json @@ -0,0 +1 @@ +{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]} \ No newline at end of file diff --git a/sites/allente.dk/__data__/no_content.json b/sites/allente.dk/__data__/no_content.json new file mode 100644 index 00000000..793e8706 --- /dev/null +++ b/sites/allente.dk/__data__/no_content.json @@ -0,0 +1 @@ +{"date":"2001-11-17","categories":[],"channels":[]} \ No newline at end of file diff --git a/sites/allente.dk/allente.dk.config.js b/sites/allente.dk/allente.dk.config.js index adab26ca..5e80b7b0 100644 --- a/sites/allente.dk/allente.dk.config.js +++ b/sites/allente.dk/allente.dk.config.js @@ -1,65 +1,65 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'allente.dk', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url({ date }) { - return `https://cs-vcb.allente.dk/epg/events?date=${date.format('YYYY-MM-DD')}` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - if (!item.details) return - const start = dayjs(item.time) - const stop = start.add(item.details.duration, 'm') - programs.push({ - title: item.title, - category: item.details.categories, - description: item.details.description, - image: item.details.image, - season: parseSeason(item), - episode: parseEpisode(item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - const data = await axios - .get(`https://cs-vcb.allente.dk/epg/events?date=${dayjs().format('YYYY-MM-DD')}`) - .then(r => r.data) - .catch(console.log) - - return data.channels.map(item => { - return { - lang: 'da', - site_id: item.id, - name: item.name - } - }) - } -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.channels)) return [] - const channelData = data.channels.find(i => i.id === channel.site_id) - - return channelData && Array.isArray(channelData.events) ? channelData.events : [] -} - -function parseSeason(item) { - return item.details.season || null -} -function parseEpisode(item) { - return item.details.episode || null -} +const dayjs = require('dayjs') + +module.exports = { + site: 'allente.dk', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url({ date }) { + return `https://cs-vcb.allente.dk/epg/events?date=${date.format('YYYY-MM-DD')}` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + if (!item.details) return + const start = dayjs(item.time) + const stop = start.add(item.details.duration, 'm') + programs.push({ + title: item.title, + category: item.details.categories, + description: item.details.description, + image: item.details.image, + season: parseSeason(item), + episode: parseEpisode(item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const axios = require('axios') + const data = await axios + .get(`https://cs-vcb.allente.dk/epg/events?date=${dayjs().format('YYYY-MM-DD')}`) + .then(r => r.data) + .catch(console.log) + + return data.channels.map(item => { + return { + lang: 'da', + site_id: item.id, + name: item.name + } + }) + } +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.channels)) return [] + const channelData = data.channels.find(i => i.id === channel.site_id) + + return channelData && Array.isArray(channelData.events) ? channelData.events : [] +} + +function parseSeason(item) { + return item.details.season || null +} +function parseEpisode(item) { + return item.details.episode || null +} diff --git a/sites/allente.dk/allente.dk.test.js b/sites/allente.dk/allente.dk.test.js index c3594d9d..e6f0caa9 100644 --- a/sites/allente.dk/allente.dk.test.js +++ b/sites/allente.dk/allente.dk.test.js @@ -1,50 +1,51 @@ -const { parser, url } = require('./allente.dk.config.js') -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('2021-11-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '0148', - xmltv_id: 'SVT1.se' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe('https://cs-vcb.allente.dk/epg/events?date=2021-11-17') -}) - -it('can parse response', () => { - const content = - '{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-08-22T07:10:00.000Z', - stop: '2022-08-22T07:30:00.000Z', - title: 'Hemmagympa med Sofia', - category: ['other'], - description: - 'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.', - image: - 'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440', - season: 4, - episode: 1 - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"date":"2001-11-17","categories":[],"channels":[]}' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./allente.dk.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const { readFileSync } = require('fs') +const { resolve } = require('path') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '0148', + xmltv_id: 'SVT1.se' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe('https://cs-vcb.allente.dk/epg/events?date=2021-11-17') +}) + +it('can parse response', () => { + const content = readFileSync(resolve(__dirname, '__data__/content.json')) + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-08-22T07:10:00.000Z', + stop: '2022-08-22T07:30:00.000Z', + title: 'Hemmagympa med Sofia', + category: ['other'], + description: + 'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.', + image: + 'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440', + season: 4, + episode: 1 + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"date":"2001-11-17","categories":[],"channels":[]}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/allente.fi/__data__/content.json b/sites/allente.fi/__data__/content.json new file mode 100644 index 00000000..bdac9a55 --- /dev/null +++ b/sites/allente.fi/__data__/content.json @@ -0,0 +1 @@ +{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]} \ No newline at end of file diff --git a/sites/allente.fi/__data__/no_content.json b/sites/allente.fi/__data__/no_content.json new file mode 100644 index 00000000..793e8706 --- /dev/null +++ b/sites/allente.fi/__data__/no_content.json @@ -0,0 +1 @@ +{"date":"2001-11-17","categories":[],"channels":[]} \ No newline at end of file diff --git a/sites/allente.fi/allente.fi.config.js b/sites/allente.fi/allente.fi.config.js index 470dca85..cebe9364 100644 --- a/sites/allente.fi/allente.fi.config.js +++ b/sites/allente.fi/allente.fi.config.js @@ -1,65 +1,65 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'allente.fi', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url({ date }) { - return `https://cs-vcb.allente.fi/epg/events?date=${date.format('YYYY-MM-DD')}` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - if (!item.details) return - const start = dayjs(item.time) - const stop = start.add(item.details.duration, 'm') - programs.push({ - title: item.title, - category: item.details.categories, - description: item.details.description, - image: item.details.image, - season: parseSeason(item), - episode: parseEpisode(item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - const data = await axios - .get(`https://cs-vcb.allente.fi/epg/events?date=${dayjs().format('YYYY-MM-DD')}`) - .then(r => r.data) - .catch(console.log) - - return data.channels.map(item => { - return { - lang: 'fi', - site_id: item.id, - name: item.name - } - }) - } -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.channels)) return [] - const channelData = data.channels.find(i => i.id === channel.site_id) - - return channelData && Array.isArray(channelData.events) ? channelData.events : [] -} - -function parseSeason(item) { - return item.details.season || null -} -function parseEpisode(item) { - return item.details.episode || null -} +const dayjs = require('dayjs') + +module.exports = { + site: 'allente.fi', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url({ date }) { + return `https://cs-vcb.allente.fi/epg/events?date=${date.format('YYYY-MM-DD')}` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + if (!item.details) return + const start = dayjs(item.time) + const stop = start.add(item.details.duration, 'm') + programs.push({ + title: item.title, + category: item.details.categories, + description: item.details.description, + image: item.details.image, + season: parseSeason(item), + episode: parseEpisode(item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const axios = require('axios') + const data = await axios + .get(`https://cs-vcb.allente.fi/epg/events?date=${dayjs().format('YYYY-MM-DD')}`) + .then(r => r.data) + .catch(console.log) + + return data.channels.map(item => { + return { + lang: 'fi', + site_id: item.id, + name: item.name + } + }) + } +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.channels)) return [] + const channelData = data.channels.find(i => i.id === channel.site_id) + + return channelData && Array.isArray(channelData.events) ? channelData.events : [] +} + +function parseSeason(item) { + return item.details.season || null +} +function parseEpisode(item) { + return item.details.episode || null +} diff --git a/sites/allente.fi/allente.fi.test.js b/sites/allente.fi/allente.fi.test.js index 6e069a94..2601571d 100644 --- a/sites/allente.fi/allente.fi.test.js +++ b/sites/allente.fi/allente.fi.test.js @@ -1,50 +1,51 @@ -const { parser, url } = require('./allente.fi.config.js') -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('2021-11-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '0148', - xmltv_id: 'SVT1.se' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe('https://cs-vcb.allente.fi/epg/events?date=2021-11-17') -}) - -it('can parse response', () => { - const content = - '{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-08-22T07:10:00.000Z', - stop: '2022-08-22T07:30:00.000Z', - title: 'Hemmagympa med Sofia', - category: ['other'], - description: - 'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.', - image: - 'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440', - season: 4, - episode: 1 - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"date":"2001-11-17","categories":[],"channels":[]}' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./allente.fi.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('2021-11-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '0148', + xmltv_id: 'SVT1.se' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe('https://cs-vcb.allente.fi/epg/events?date=2021-11-17') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-08-22T07:10:00.000Z', + stop: '2022-08-22T07:30:00.000Z', + title: 'Hemmagympa med Sofia', + category: ['other'], + description: + 'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.', + image: + 'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440', + season: 4, + episode: 1 + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/allente.no/__data__/content.json b/sites/allente.no/__data__/content.json new file mode 100644 index 00000000..bdac9a55 --- /dev/null +++ b/sites/allente.no/__data__/content.json @@ -0,0 +1 @@ +{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]} \ No newline at end of file diff --git a/sites/allente.no/__data__/no_content.json b/sites/allente.no/__data__/no_content.json new file mode 100644 index 00000000..793e8706 --- /dev/null +++ b/sites/allente.no/__data__/no_content.json @@ -0,0 +1 @@ +{"date":"2001-11-17","categories":[],"channels":[]} \ No newline at end of file diff --git a/sites/allente.no/allente.no.config.js b/sites/allente.no/allente.no.config.js index 3406d64f..348b521c 100644 --- a/sites/allente.no/allente.no.config.js +++ b/sites/allente.no/allente.no.config.js @@ -1,65 +1,65 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'allente.no', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url({ date }) { - return `https://cs-vcb.allente.no/epg/events?date=${date.format('YYYY-MM-DD')}` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - if (!item.details) return - const start = dayjs(item.time) - const stop = start.add(item.details.duration, 'm') - programs.push({ - title: item.title, - category: item.details.categories, - description: item.details.description, - image: item.details.image, - season: parseSeason(item), - episode: parseEpisode(item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - const data = await axios - .get(`https://cs-vcb.allente.no/epg/events?date=${dayjs().format('YYYY-MM-DD')}`) - .then(r => r.data) - .catch(console.log) - - return data.channels.map(item => { - return { - lang: 'no', - site_id: item.id, - name: item.name - } - }) - } -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.channels)) return [] - const channelData = data.channels.find(i => i.id === channel.site_id) - - return channelData && Array.isArray(channelData.events) ? channelData.events : [] -} - -function parseSeason(item) { - return item.details.season || null -} -function parseEpisode(item) { - return item.details.episode || null -} +const dayjs = require('dayjs') + +module.exports = { + site: 'allente.no', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url({ date }) { + return `https://cs-vcb.allente.no/epg/events?date=${date.format('YYYY-MM-DD')}` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + if (!item.details) return + const start = dayjs(item.time) + const stop = start.add(item.details.duration, 'm') + programs.push({ + title: item.title, + category: item.details.categories, + description: item.details.description, + image: item.details.image, + season: parseSeason(item), + episode: parseEpisode(item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const axios = require('axios') + const data = await axios + .get(`https://cs-vcb.allente.no/epg/events?date=${dayjs().format('YYYY-MM-DD')}`) + .then(r => r.data) + .catch(console.log) + + return data.channels.map(item => { + return { + lang: 'no', + site_id: item.id, + name: item.name + } + }) + } +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.channels)) return [] + const channelData = data.channels.find(i => i.id === channel.site_id) + + return channelData && Array.isArray(channelData.events) ? channelData.events : [] +} + +function parseSeason(item) { + return item.details.season || null +} +function parseEpisode(item) { + return item.details.episode || null +} diff --git a/sites/allente.no/allente.no.test.js b/sites/allente.no/allente.no.test.js index 3ca1fbfc..f12e8978 100644 --- a/sites/allente.no/allente.no.test.js +++ b/sites/allente.no/allente.no.test.js @@ -1,50 +1,51 @@ -const { parser, url } = require('./allente.no.config.js') -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('2021-11-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '0148', - xmltv_id: 'SVT1.se' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe('https://cs-vcb.allente.no/epg/events?date=2021-11-17') -}) - -it('can parse response', () => { - const content = - '{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-08-22T07:10:00.000Z', - stop: '2022-08-22T07:30:00.000Z', - title: 'Hemmagympa med Sofia', - category: ['other'], - description: - 'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.', - image: - 'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440', - season: 4, - episode: 1 - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"date":"2001-11-17","categories":[],"channels":[]}' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./allente.no.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('2021-11-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '0148', + xmltv_id: 'SVT1.se' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe('https://cs-vcb.allente.no/epg/events?date=2021-11-17') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-08-22T07:10:00.000Z', + stop: '2022-08-22T07:30:00.000Z', + title: 'Hemmagympa med Sofia', + category: ['other'], + description: + 'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.', + image: + 'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440', + season: 4, + episode: 1 + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/allente.se/__data__/content.json b/sites/allente.se/__data__/content.json new file mode 100644 index 00000000..bdac9a55 --- /dev/null +++ b/sites/allente.se/__data__/content.json @@ -0,0 +1 @@ +{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]} \ No newline at end of file diff --git a/sites/allente.se/__data__/no_content.json b/sites/allente.se/__data__/no_content.json new file mode 100644 index 00000000..793e8706 --- /dev/null +++ b/sites/allente.se/__data__/no_content.json @@ -0,0 +1 @@ +{"date":"2001-11-17","categories":[],"channels":[]} \ No newline at end of file diff --git a/sites/allente.se/allente.se.config.js b/sites/allente.se/allente.se.config.js index 972d6ee3..f8666ef3 100644 --- a/sites/allente.se/allente.se.config.js +++ b/sites/allente.se/allente.se.config.js @@ -1,65 +1,65 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'allente.se', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url({ date }) { - return `https://cs-vcb.allente.se/epg/events?date=${date.format('YYYY-MM-DD')}` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - if (!item.details) return - const start = dayjs(item.time) - const stop = start.add(item.details.duration, 'm') - programs.push({ - title: item.title, - category: item.details.categories, - description: item.details.description, - image: item.details.image, - season: parseSeason(item), - episode: parseEpisode(item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - const data = await axios - .get(`https://cs-vcb.allente.se/epg/events?date=${dayjs().format('YYYY-MM-DD')}`) - .then(r => r.data) - .catch(console.log) - - return data.channels.map(item => { - return { - lang: 'sv', - site_id: item.id, - name: item.name - } - }) - } -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.channels)) return [] - const channelData = data.channels.find(i => i.id === channel.site_id) - - return channelData && Array.isArray(channelData.events) ? channelData.events : [] -} - -function parseSeason(item) { - return item.details.season || null -} -function parseEpisode(item) { - return item.details.episode || null -} +const dayjs = require('dayjs') + +module.exports = { + site: 'allente.se', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url({ date }) { + return `https://cs-vcb.allente.se/epg/events?date=${date.format('YYYY-MM-DD')}` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + if (!item.details) return + const start = dayjs(item.time) + const stop = start.add(item.details.duration, 'm') + programs.push({ + title: item.title, + category: item.details.categories, + description: item.details.description, + image: item.details.image, + season: parseSeason(item), + episode: parseEpisode(item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const axios = require('axios') + const data = await axios + .get(`https://cs-vcb.allente.se/epg/events?date=${dayjs().format('YYYY-MM-DD')}`) + .then(r => r.data) + .catch(console.log) + + return data.channels.map(item => { + return { + lang: 'sv', + site_id: item.id, + name: item.name + } + }) + } +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.channels)) return [] + const channelData = data.channels.find(i => i.id === channel.site_id) + + return channelData && Array.isArray(channelData.events) ? channelData.events : [] +} + +function parseSeason(item) { + return item.details.season || null +} +function parseEpisode(item) { + return item.details.episode || null +} diff --git a/sites/allente.se/allente.se.test.js b/sites/allente.se/allente.se.test.js index 5c6e90cd..2944c58a 100644 --- a/sites/allente.se/allente.se.test.js +++ b/sites/allente.se/allente.se.test.js @@ -1,50 +1,51 @@ -const { parser, url } = require('./allente.se.config.js') -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('2021-11-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '0148', - xmltv_id: 'SVT1.se' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe('https://cs-vcb.allente.se/epg/events?date=2021-11-17') -}) - -it('can parse response', () => { - const content = - '{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-08-22T07:10:00.000Z', - stop: '2022-08-22T07:30:00.000Z', - title: 'Hemmagympa med Sofia', - category: ['other'], - description: - 'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.', - image: - 'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440', - season: 4, - episode: 1 - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"date":"2001-11-17","categories":[],"channels":[]}' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./allente.se.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('2021-11-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '0148', + xmltv_id: 'SVT1.se' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe('https://cs-vcb.allente.se/epg/events?date=2021-11-17') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-08-22T07:10:00.000Z', + stop: '2022-08-22T07:30:00.000Z', + title: 'Hemmagympa med Sofia', + category: ['other'], + description: + 'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.', + image: + 'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440', + season: 4, + episode: 1 + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/andorradifusio.ad/andorradifusio.ad.config.js b/sites/andorradifusio.ad/andorradifusio.ad.config.js index cf3e5a5d..c20b2900 100644 --- a/sites/andorradifusio.ad/andorradifusio.ad.config.js +++ b/sites/andorradifusio.ad/andorradifusio.ad.config.js @@ -1,59 +1,59 @@ -const cheerio = require('cheerio') -const { DateTime } = require('luxon') - -module.exports = { - site: 'andorradifusio.ad', - days: 2, - url({ channel }) { - return `https://www.andorradifusio.ad/programacio/${channel.site_id}` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ hours: 1 }) - programs.push({ - title: item.title, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item, date) { - const dateString = `${date.format('MM/DD/YYYY')} ${item.time}` - - return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH:mm', { zone: 'Europe/Madrid' }).toUTC() -} - -function parseItems(content, date) { - const $ = cheerio.load(content) - const day = DateTime.fromMillis(date.valueOf()).setLocale('ca').toFormat('dd LLLL').toLowerCase() - const column = $('.programacio-dia > h3 > .dia') - .filter((i, el) => $(el).text() === day.slice(0, 6) + '.') - .first() - .parent() - .parent() - const items = [] - const titles = column.find('p').toArray() - column.find('h4').each((i, time) => { - items.push({ - time: $(time).text(), - title: $(titles[i]).text() - }) - }) - - return items -} +const cheerio = require('cheerio') +const { DateTime } = require('luxon') + +module.exports = { + site: 'andorradifusio.ad', + days: 2, + url({ channel }) { + return `https://www.andorradifusio.ad/programacio/${channel.site_id}` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ hours: 1 }) + programs.push({ + title: item.title, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item, date) { + const dateString = `${date.format('MM/DD/YYYY')} ${item.time}` + + return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH:mm', { zone: 'Europe/Madrid' }).toUTC() +} + +function parseItems(content, date) { + const $ = cheerio.load(content) + const day = DateTime.fromMillis(date.valueOf()).setLocale('ca').toFormat('dd LLLL').toLowerCase() + const column = $('.programacio-dia > h3 > .dia') + .filter((i, el) => $(el).text() === day.slice(0, 6) + '.') + .first() + .parent() + .parent() + const items = [] + const titles = column.find('p').toArray() + column.find('h4').each((i, time) => { + items.push({ + time: $(time).text(), + title: $(titles[i]).text() + }) + }) + + return items +} diff --git a/sites/andorradifusio.ad/andorradifusio.ad.test.js b/sites/andorradifusio.ad/andorradifusio.ad.test.js index f63c40a9..aaf8e089 100644 --- a/sites/andorradifusio.ad/andorradifusio.ad.test.js +++ b/sites/andorradifusio.ad/andorradifusio.ad.test.js @@ -1,47 +1,47 @@ -const { parser, url } = require('./andorradifusio.ad.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('2023-06-07', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'atv', - xmltv_id: 'AndorraTV.ad' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://www.andorradifusio.ad/programacio/atv') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-06-07T05:00:00.000Z', - stop: '2023-06-07T06:00:00.000Z', - title: 'Club Piolet' - }) - - expect(results[20]).toMatchObject({ - start: '2023-06-07T23:00:00.000Z', - stop: '2023-06-08T00:00:00.000Z', - title: 'Àrea Andorra Difusió' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./andorradifusio.ad.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('2023-06-07', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'atv', + xmltv_id: 'AndorraTV.ad' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://www.andorradifusio.ad/programacio/atv') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-06-07T05:00:00.000Z', + stop: '2023-06-07T06:00:00.000Z', + title: 'Club Piolet' + }) + + expect(results[20]).toMatchObject({ + start: '2023-06-07T23:00:00.000Z', + stop: '2023-06-08T00:00:00.000Z', + title: 'Àrea Andorra Difusió' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/anteltv.com.uy/anteltv.com.uy.config.js b/sites/anteltv.com.uy/anteltv.com.uy.config.js index c0133ea9..2c799e4c 100644 --- a/sites/anteltv.com.uy/anteltv.com.uy.config.js +++ b/sites/anteltv.com.uy/anteltv.com.uy.config.js @@ -1,108 +1,108 @@ -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') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -const API_ENDPOINT = 'https://cds-frontend.vera.com.uy/api-contenidos' - -module.exports = { - site: 'anteltv.com.uy', - days: 2, - async url({ date, channel }) { - const session = await loadSessionDetails() - if (!session || !session.token) return null - - return `${API_ENDPOINT}/canales/epg/${ - channel.site_id - }?limit=500&dias_siguientes=0&fecha=${date.format('YYYY-MM-DD')}&token=${session.token}` - }, - request: { - async headers() { - const session = await loadSessionDetails() - if (!session || !session.jwt) return null - - return { - authorization: `Bearer ${session.jwt}`, - 'x-frontend-id': 1196, - 'x-service-id': 3, - 'x-system-id': 1 - } - } - }, - parser({ content }) { - let programs = [] - let items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.nombre_programa, - sub_title: item.subtitle, - description: item.descripcion_programa, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const session = await loadSessionDetails() - if (!session || !session.jwt || !session.token) return null - - const data = await axios - .get(`${API_ENDPOINT}/listas/68?token=${session.token}`, { - headers: { - authorization: `Bearer ${session.jwt}`, - 'x-frontend-id': 1196, - 'x-service-id': 3, - 'x-system-id': 1 - } - }) - .then(r => r.data) - .catch(console.error) - - return data.contenidos.map(c => { - return { - lang: 'es', - site_id: c.public_id, - name: c.nombre - } - }) - } -} - -function parseStart(item) { - return dayjs.tz(item.fecha_hora_inicio, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo') -} - -function parseStop(item) { - return dayjs.tz(item.fecha_hora_fin, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo') -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.data)) return [] - - return data.data -} - -function loadSessionDetails() { - return axios - .post( - 'https://veratv-be.vera.com.uy/api/sesiones', - { - tipo: 'anonima' - }, - { - 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') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +const API_ENDPOINT = 'https://cds-frontend.vera.com.uy/api-contenidos' + +module.exports = { + site: 'anteltv.com.uy', + days: 2, + async url({ date, channel }) { + const session = await loadSessionDetails() + if (!session || !session.token) return null + + return `${API_ENDPOINT}/canales/epg/${ + channel.site_id + }?limit=500&dias_siguientes=0&fecha=${date.format('YYYY-MM-DD')}&token=${session.token}` + }, + request: { + async headers() { + const session = await loadSessionDetails() + if (!session || !session.jwt) return null + + return { + authorization: `Bearer ${session.jwt}`, + 'x-frontend-id': 1196, + 'x-service-id': 3, + 'x-system-id': 1 + } + } + }, + parser({ content }) { + let programs = [] + let items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.nombre_programa, + sub_title: item.subtitle, + description: item.descripcion_programa, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const session = await loadSessionDetails() + if (!session || !session.jwt || !session.token) return null + + const data = await axios + .get(`${API_ENDPOINT}/listas/68?token=${session.token}`, { + headers: { + authorization: `Bearer ${session.jwt}`, + 'x-frontend-id': 1196, + 'x-service-id': 3, + 'x-system-id': 1 + } + }) + .then(r => r.data) + .catch(console.error) + + return data.contenidos.map(c => { + return { + lang: 'es', + site_id: c.public_id, + name: c.nombre + } + }) + } +} + +function parseStart(item) { + return dayjs.tz(item.fecha_hora_inicio, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo') +} + +function parseStop(item) { + return dayjs.tz(item.fecha_hora_fin, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo') +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.data)) return [] + + return data.data +} + +function loadSessionDetails() { + return axios + .post( + 'https://veratv-be.vera.com.uy/api/sesiones', + { + tipo: 'anonima' + }, + { + headers: { + 'Content-Type': 'application/json' + } + } + ) + .then(r => r.data) + .catch(console.log) +} diff --git a/sites/anteltv.com.uy/anteltv.com.uy.test.js b/sites/anteltv.com.uy/anteltv.com.uy.test.js index 51ea965f..9e4398d2 100644 --- a/sites/anteltv.com.uy/anteltv.com.uy.test.js +++ b/sites/anteltv.com.uy/anteltv.com.uy.test.js @@ -1,85 +1,85 @@ -const { parser, url, request } = require('./anteltv.com.uy.config.js') -const fs = require('fs') -const axios = require('axios') -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) - -jest.mock('axios') - -axios.post.mockImplementation((url, data, opts) => { - if ( - url === 'https://veratv-be.vera.com.uy/api/sesiones' && - JSON.stringify(opts.headers) === - JSON.stringify({ - 'Content-Type': 'application/json' - }) && - JSON.stringify(data) === - JSON.stringify({ - tipo: 'anonima' - }) - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/session.json'))) - }) - } else { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/no_session.json'))) - }) - } -}) - -const date = dayjs.utc('2023-02-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2s6nd', - xmltv_id: 'Canal5.uy' -} - -it('can generate valid url', async () => { - const result = await url({ date, channel }) - - expect(result).toBe( - 'https://cds-frontend.vera.com.uy/api-contenidos/canales/epg/2s6nd?limit=500&dias_siguientes=0&fecha=2023-02-11&token=MpDY52p1V6g511VSABp1015B' - ) -}) - -it('can generate valid request headers', async () => { - const result = await request.headers() - - expect(result).toMatchObject({ - authorization: - 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOnsidGlwbyI6ImFub25pbWEifSwic3ViIjoiTXBEWTUycDFWNmc1MTFWU0FCcDEwMTVCIiwicHJuIjp7ImlkX3NlcnZpY2lvIjozLCJpZF9mcm9udGVuZCI6MTE5NiwiaXAiOiIxNzkuMjcuMTU0LjI0MiIsImlwX3JlZmVyZW5jaWFkYSI6IjE4OC4yNDIuNDguOTMiLCJpZF9kaXNwb3NpdGl2byI6MH0sImF1ZCI6IkFwcHNcL1dlYnMgRnJvbnRlbmRzIiwiaWF0IjoxNjc1ODI3NDU2LCJleHAiOjE2NzU4NDkwNTZ9.8bAQciQl5DOIZF7GgCl6ad-KJUSpqQREetozGv_IH5s', - 'x-frontend-id': 1196, - 'x-service-id': 3, - 'x-system-id': 1 - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') - let results = parser({ content }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-02-11T02:30:00.000Z', - stop: '2023-02-11T04:00:00.000Z', - title: 'Canal 5 Noticias rep.', - sub_title: '', - description: '' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8') - }) - - expect(results).toMatchObject([]) -}) +const { parser, url, request } = require('./anteltv.com.uy.config.js') +const fs = require('fs') +const axios = require('axios') +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) + +jest.mock('axios') + +axios.post.mockImplementation((url, data, opts) => { + if ( + url === 'https://veratv-be.vera.com.uy/api/sesiones' && + JSON.stringify(opts.headers) === + JSON.stringify({ + 'Content-Type': 'application/json' + }) && + JSON.stringify(data) === + JSON.stringify({ + tipo: 'anonima' + }) + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/session.json'))) + }) + } else { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/no_session.json'))) + }) + } +}) + +const date = dayjs.utc('2023-02-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2s6nd', + xmltv_id: 'Canal5.uy' +} + +it('can generate valid url', async () => { + const result = await url({ date, channel }) + + expect(result).toBe( + 'https://cds-frontend.vera.com.uy/api-contenidos/canales/epg/2s6nd?limit=500&dias_siguientes=0&fecha=2023-02-11&token=MpDY52p1V6g511VSABp1015B' + ) +}) + +it('can generate valid request headers', async () => { + const result = await request.headers() + + expect(result).toMatchObject({ + authorization: + 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOnsidGlwbyI6ImFub25pbWEifSwic3ViIjoiTXBEWTUycDFWNmc1MTFWU0FCcDEwMTVCIiwicHJuIjp7ImlkX3NlcnZpY2lvIjozLCJpZF9mcm9udGVuZCI6MTE5NiwiaXAiOiIxNzkuMjcuMTU0LjI0MiIsImlwX3JlZmVyZW5jaWFkYSI6IjE4OC4yNDIuNDguOTMiLCJpZF9kaXNwb3NpdGl2byI6MH0sImF1ZCI6IkFwcHNcL1dlYnMgRnJvbnRlbmRzIiwiaWF0IjoxNjc1ODI3NDU2LCJleHAiOjE2NzU4NDkwNTZ9.8bAQciQl5DOIZF7GgCl6ad-KJUSpqQREetozGv_IH5s', + 'x-frontend-id': 1196, + 'x-service-id': 3, + 'x-system-id': 1 + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') + let results = parser({ content }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-02-11T02:30:00.000Z', + stop: '2023-02-11T04:00:00.000Z', + title: 'Canal 5 Noticias rep.', + sub_title: '', + description: '' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8') + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/antennaeurope.gr/antennaeurope.gr.config.js b/sites/antennaeurope.gr/antennaeurope.gr.config.js index 982685d9..aa5f0146 100644 --- a/sites/antennaeurope.gr/antennaeurope.gr.config.js +++ b/sites/antennaeurope.gr/antennaeurope.gr.config.js @@ -1,59 +1,59 @@ -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: 'antennaeurope.gr', - days: 2, - url({ date }) { - return `https://www.antennaeurope.gr/el/tvguide.html?date=${date.format('YYYY-MM-DD')}` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('.title').text().trim() -} - -function parseStart($item, date) { - const time = $item('dt.col-time').clone().children().remove().end().text().trim() - - return time - ? dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Athens') - : null -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('dl.show').toArray() -} +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: 'antennaeurope.gr', + days: 2, + url({ date }) { + return `https://www.antennaeurope.gr/el/tvguide.html?date=${date.format('YYYY-MM-DD')}` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('.title').text().trim() +} + +function parseStart($item, date) { + const time = $item('dt.col-time').clone().children().remove().end().text().trim() + + return time + ? dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Athens') + : null +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('dl.show').toArray() +} diff --git a/sites/antennaeurope.gr/antennaeurope.gr.test.js b/sites/antennaeurope.gr/antennaeurope.gr.test.js index 7deb858e..e61d8de7 100644 --- a/sites/antennaeurope.gr/antennaeurope.gr.test.js +++ b/sites/antennaeurope.gr/antennaeurope.gr.test.js @@ -1,46 +1,46 @@ -const { parser, url } = require('./antennaeurope.gr.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-21', 'YYYY-MM-DD').startOf('d') - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://www.antennaeurope.gr/el/tvguide.html?date=2025-01-21') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(16) - - expect(results[0]).toMatchObject({ - start: '2025-01-21T03:45:00.000Z', - stop: '2025-01-21T07:50:00.000Z', - title: 'ΚΑΛΗΜΕΡΑ ΕΛΛΑΔΑ' - }) - - expect(results[15]).toMatchObject({ - start: '2025-01-22T01:30:00.000Z', - stop: '2025-01-22T02:00:00.000Z', - title: 'ΤΟ ΠΡΩΙΝΟ' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - }) - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./antennaeurope.gr.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-21', 'YYYY-MM-DD').startOf('d') + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://www.antennaeurope.gr/el/tvguide.html?date=2025-01-21') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(16) + + expect(results[0]).toMatchObject({ + start: '2025-01-21T03:45:00.000Z', + stop: '2025-01-21T07:50:00.000Z', + title: 'ΚΑΛΗΜΕΡΑ ΕΛΛΑΔΑ' + }) + + expect(results[15]).toMatchObject({ + start: '2025-01-22T01:30:00.000Z', + stop: '2025-01-22T02:00:00.000Z', + title: 'ΤΟ ΠΡΩΙΝΟ' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + expect(results).toMatchObject([]) +}) diff --git a/sites/antennapacific.gr/antennapacific.gr.config.js b/sites/antennapacific.gr/antennapacific.gr.config.js index a0b06e29..5b3aaec1 100644 --- a/sites/antennapacific.gr/antennapacific.gr.config.js +++ b/sites/antennapacific.gr/antennapacific.gr.config.js @@ -1,59 +1,59 @@ -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: 'antennapacific.gr', - days: 2, - url({ date }) { - return `https://www.antennapacific.gr/el/tvguide.html?date=${date.format('YYYY-MM-DD')}` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('.title').text().trim() -} - -function parseStart($item, date) { - const time = $item('dt.col-time').clone().children().remove().end().text().trim() - - return time - ? dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Athens') - : null -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('dl.show').toArray() -} +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: 'antennapacific.gr', + days: 2, + url({ date }) { + return `https://www.antennapacific.gr/el/tvguide.html?date=${date.format('YYYY-MM-DD')}` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('.title').text().trim() +} + +function parseStart($item, date) { + const time = $item('dt.col-time').clone().children().remove().end().text().trim() + + return time + ? dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Athens') + : null +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('dl.show').toArray() +} diff --git a/sites/antennapacific.gr/antennapacific.gr.test.js b/sites/antennapacific.gr/antennapacific.gr.test.js index 2047519a..a1b2e368 100644 --- a/sites/antennapacific.gr/antennapacific.gr.test.js +++ b/sites/antennapacific.gr/antennapacific.gr.test.js @@ -1,46 +1,46 @@ -const { parser, url } = require('./antennapacific.gr.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-21', 'YYYY-MM-DD').startOf('d') - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://www.antennapacific.gr/el/tvguide.html?date=2025-01-21') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(17) - - expect(results[0]).toMatchObject({ - start: '2025-01-21T05:00:00.000Z', - stop: '2025-01-21T06:00:00.000Z', - title: 'ANT1 NEWS - ΚΕΝΤΡΙΚΟ ΔΕΛΤΙΟ' - }) - - expect(results[16]).toMatchObject({ - start: '2025-01-22T02:45:00.000Z', - stop: '2025-01-22T03:15:00.000Z', - title: 'ΚΑΛΗΜΕΡΑ ΕΛΛΑΔΑ' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - }) - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./antennapacific.gr.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-21', 'YYYY-MM-DD').startOf('d') + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://www.antennapacific.gr/el/tvguide.html?date=2025-01-21') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(17) + + expect(results[0]).toMatchObject({ + start: '2025-01-21T05:00:00.000Z', + stop: '2025-01-21T06:00:00.000Z', + title: 'ANT1 NEWS - ΚΕΝΤΡΙΚΟ ΔΕΛΤΙΟ' + }) + + expect(results[16]).toMatchObject({ + start: '2025-01-22T02:45:00.000Z', + stop: '2025-01-22T03:15:00.000Z', + title: 'ΚΑΛΗΜΕΡΑ ΕΛΛΑΔΑ' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + expect(results).toMatchObject([]) +}) diff --git a/sites/antennasatellite.gr/antennasatellite.gr.config.js b/sites/antennasatellite.gr/antennasatellite.gr.config.js index d3a35bde..a604d9be 100644 --- a/sites/antennasatellite.gr/antennasatellite.gr.config.js +++ b/sites/antennasatellite.gr/antennasatellite.gr.config.js @@ -1,59 +1,59 @@ -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: 'antennasatellite.gr', - days: 2, - url({ date }) { - return `https://www.antennasatellite.gr/el/tvguide.html?date=${date.format('YYYY-MM-DD')}` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('.title').text().trim() -} - -function parseStart($item, date) { - const time = $item('dt.col-time').clone().children().remove().end().text().trim() - - return time - ? dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Athens') - : null -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('dl.show').toArray() -} +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: 'antennasatellite.gr', + days: 2, + url({ date }) { + return `https://www.antennasatellite.gr/el/tvguide.html?date=${date.format('YYYY-MM-DD')}` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('.title').text().trim() +} + +function parseStart($item, date) { + const time = $item('dt.col-time').clone().children().remove().end().text().trim() + + return time + ? dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Athens') + : null +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('dl.show').toArray() +} diff --git a/sites/antennasatellite.gr/antennasatellite.gr.test.js b/sites/antennasatellite.gr/antennasatellite.gr.test.js index 9923108e..59ada99e 100644 --- a/sites/antennasatellite.gr/antennasatellite.gr.test.js +++ b/sites/antennasatellite.gr/antennasatellite.gr.test.js @@ -1,46 +1,46 @@ -const { parser, url } = require('./antennasatellite.gr.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-21', 'YYYY-MM-DD').startOf('d') - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://www.antennasatellite.gr/el/tvguide.html?date=2025-01-21') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(16) - - expect(results[0]).toMatchObject({ - start: '2025-01-21T04:00:00.000Z', - stop: '2025-01-21T04:40:00.000Z', - title: 'ANT1 NEWS' - }) - - expect(results[15]).toMatchObject({ - start: '2025-01-22T00:50:00.000Z', - stop: '2025-01-22T01:20:00.000Z', - title: 'ΤΟ ΠΡΩΙΝΟ' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - }) - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./antennasatellite.gr.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-21', 'YYYY-MM-DD').startOf('d') + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://www.antennasatellite.gr/el/tvguide.html?date=2025-01-21') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(16) + + expect(results[0]).toMatchObject({ + start: '2025-01-21T04:00:00.000Z', + stop: '2025-01-21T04:40:00.000Z', + title: 'ANT1 NEWS' + }) + + expect(results[15]).toMatchObject({ + start: '2025-01-22T00:50:00.000Z', + stop: '2025-01-22T01:20:00.000Z', + title: 'ΤΟ ΠΡΩΙΝΟ' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + expect(results).toMatchObject([]) +}) diff --git a/sites/arianaafgtv.com/arianaafgtv.com.channels.xml b/sites/arianaafgtv.com/arianaafgtv.com.channels.xml deleted file mode 100644 index fa678c98..00000000 --- a/sites/arianaafgtv.com/arianaafgtv.com.channels.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - Ariana Afghanistan Television - \ No newline at end of file diff --git a/sites/arianaafgtv.com/arianaafgtv.com.config.js b/sites/arianaafgtv.com/arianaafgtv.com.config.js deleted file mode 100644 index 17e7011c..00000000 --- a/sites/arianaafgtv.com/arianaafgtv.com.config.js +++ /dev/null @@ -1,82 +0,0 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'arianaafgtv.com', - days: 2, - url: 'https://www.arianaafgtv.com/index.html', - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const title = item.title - const start = parseStart(item, date) - const stop = parseStop(item, date) - programs.push({ - title, - start, - stop - }) - }) - - return programs - } -} - -function parseStop(item, date) { - const time = `${date.format('MM/DD/YYYY')} ${item.end.toUpperCase()}` - - return dayjs.tz(time, 'MM/DD/YYYY hh:mm A', 'Asia/Kabul') -} - -function parseStart(item, date) { - const time = `${date.format('MM/DD/YYYY')} ${item.start.toUpperCase()}` - - return dayjs.tz(time, 'MM/DD/YYYY hh:mm A', 'Asia/Kabul') -} - -function parseItems(content, date) { - const $ = cheerio.load(content) - const dayOfWeek = date.format('dddd') - const column = $('.H4') - .filter((i, el) => { - return $(el).text() === dayOfWeek - }) - .first() - .parent() - - const rows = column - .find('.Paragraph') - .map((i, el) => { - return $(el).html() - }) - .toArray() - .map(r => (r === ' ' ? '|' : r)) - .join(' ') - .split('|') - - const items = [] - rows.forEach(row => { - row = row.trim() - if (row) { - const found = row.match(/(\d+(|:\d+)(a|p)m-\d+(|:\d+)(a|p)m)/gi) - if (!found) return - const time = found[0] - let start = time.match(/(\d+(|:\d+)(a|p)m)-/i)[1] - start = dayjs(start.toUpperCase(), ['hh:mmA', 'h:mmA', 'hA']).format('hh:mm A') - let end = time.match(/-(\d+(|:\d+)(a|p)m)/i)[1] - end = dayjs(end.toUpperCase(), ['hh:mmA', 'h:mmA', 'hA']).format('hh:mm A') - const title = row.replace(time, '').replace(' ', '').trim() - items.push({ start, end, title }) - } - }) - - return items -} diff --git a/sites/arianaafgtv.com/readme.md b/sites/arianaafgtv.com/readme.md deleted file mode 100644 index a75d4db1..00000000 --- a/sites/arianaafgtv.com/readme.md +++ /dev/null @@ -1,15 +0,0 @@ -# arianaafgtv.com - -https://arianaafgtv.com/#ariana-afghanistan-television-tv-guide - -### Download the guide - -```sh -npm run grab --- --site=arianaafgtv.com -``` - -### Test - -```sh -npm test --- arianaafgtv.com -``` diff --git a/sites/arianatelevision.com/__data__/content.html b/sites/arianatelevision.com/__data__/content.html new file mode 100644 index 00000000..66bfcfb7 --- /dev/null +++ b/sites/arianatelevision.com/__data__/content.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sites/arianatelevision.com/__data__/no_content.html b/sites/arianatelevision.com/__data__/no_content.html new file mode 100644 index 00000000..42addf67 --- /dev/null +++ b/sites/arianatelevision.com/__data__/no_content.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sites/arianatelevision.com/arianatelevision.com.config.js b/sites/arianatelevision.com/arianatelevision.com.config.js index d6284e46..40bc9b79 100644 --- a/sites/arianatelevision.com/arianatelevision.com.config.js +++ b/sites/arianatelevision.com/arianatelevision.com.config.js @@ -1,60 +1,60 @@ -const cheerio = require('cheerio') -const { DateTime } = require('luxon') - -module.exports = { - site: 'arianatelevision.com', - days: 2, - url: 'https://www.arianatelevision.com/program-schedule/', - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ minutes: 30 }) - programs.push({ - title: item.title, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item, date) { - const time = `${date.format('YYYY-MM-DD')} ${item.start}` - - return DateTime.fromFormat(time, 'yyyy-MM-dd H:mm', { zone: 'Asia/Kabul' }).toUTC() -} - -function parseItems(content, date) { - const $ = cheerio.load(content) - const settings = $('#jtrt_table_settings_508').text() - if (!settings) return [] - const data = JSON.parse(settings) - if (!data || !Array.isArray(data)) return [] - - let rows = data[0] - rows.shift() - const output = [] - rows.forEach(row => { - let day = date.day() + 2 - if (day > 7) day = 1 - if (!row[0] || !row[day]) return - output.push({ - start: row[0].trim(), - title: row[day].trim() - }) - }) - - return output -} +const cheerio = require('cheerio') +const { DateTime } = require('luxon') + +module.exports = { + site: 'arianatelevision.com', + days: 2, + url: 'https://www.arianatelevision.com/program-schedule/', + parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ minutes: 30 }) + programs.push({ + title: item.title, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item, date) { + const time = `${date.format('YYYY-MM-DD')} ${item.start}` + + return DateTime.fromFormat(time, 'yyyy-MM-dd H:mm', { zone: 'Asia/Kabul' }).toUTC() +} + +function parseItems(content, date) { + const $ = cheerio.load(content) + const settings = $('#jtrt_table_settings_508').text() + if (!settings) return [] + const data = JSON.parse(settings) + if (!data || !Array.isArray(data)) return [] + + let rows = data[0] + rows.shift() + const output = [] + rows.forEach(row => { + let day = date.day() + 2 + if (day > 7) day = 1 + if (!row[0] || !row[day]) return + output.push({ + start: row[0].trim(), + title: row[day].trim() + }) + }) + + return output +} diff --git a/sites/arianatelevision.com/arianatelevision.com.test.js b/sites/arianatelevision.com/arianatelevision.com.test.js index 901a5507..dad80a9a 100644 --- a/sites/arianatelevision.com/arianatelevision.com.test.js +++ b/sites/arianatelevision.com/arianatelevision.com.test.js @@ -1,59 +1,59 @@ -const { parser, url } = require('./arianatelevision.com.config.js') -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('2021-11-27', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '#', - xmltv_id: 'ArianaTVNational.af' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.arianatelevision.com/program-schedule/') -}) - -it('can parse response', () => { - const content = - '' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-27T02:30:00.000Z', - stop: '2021-11-27T03:00:00.000Z', - title: 'City Report' - }, - { - start: '2021-11-27T03:00:00.000Z', - stop: '2021-11-27T10:30:00.000Z', - title: 'ICC T20 Highlights' - }, - { - start: '2021-11-27T10:30:00.000Z', - stop: '2021-11-28T02:00:00.000Z', - title: 'ICC T20 World Cup' - }, - { - start: '2021-11-28T02:00:00.000Z', - stop: '2021-11-28T02:30:00.000Z', - title: 'Quran and Hadis' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./arianatelevision.com.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('2021-11-27', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '#', + xmltv_id: 'ArianaTVNational.af' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.arianatelevision.com/program-schedule/') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-27T02:30:00.000Z', + stop: '2021-11-27T03:00:00.000Z', + title: 'City Report' + }, + { + start: '2021-11-27T03:00:00.000Z', + stop: '2021-11-27T10:30:00.000Z', + title: 'ICC T20 Highlights' + }, + { + start: '2021-11-27T10:30:00.000Z', + stop: '2021-11-28T02:00:00.000Z', + title: 'ICC T20 World Cup' + }, + { + start: '2021-11-28T02:00:00.000Z', + stop: '2021-11-28T02:30:00.000Z', + title: 'Quran and Hadis' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/arirang.com/arirang.com.config.js b/sites/arirang.com/arirang.com.config.js index 918a6275..7d20b912 100644 --- a/sites/arirang.com/arirang.com.config.js +++ b/sites/arirang.com/arirang.com.config.js @@ -1,163 +1,163 @@ -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') -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'arirang.com', - output: 'arirang.com.guide.xml', - channels: 'arirang.com.channels.xml', - lang: 'en', - days: 7, - delay: 5000, - url: 'https://www.arirang.com/v1.0/open/external/proxy', - - request: { - method: 'POST', - timeout: 5000, - cache: { ttl: 60 * 60 * 1000 }, - headers: { - Accept: 'application/json, text/plain, */*', - 'Content-Type': 'application/json', - Origin: 'https://www.arirang.com', - Referer: 'https://www.arirang.com/schedule', - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36' - }, - data: function (context) { - const { channel, date } = context - return { - address: 'https://script.arirang.com/api/v1/bis/listScheduleV3.do', - method: 'POST', - headers: {}, - body: { - data: { - dmParam: { - chanId: channel.site_id, - broadYmd: dayjs.tz(date, 'Asia/Seoul').format('YYYYMMDD'), - planNo: '1' - } - } - } - } - } - }, - - logo: function (context) { - return context.channel.logo - }, - - async parser(context) { - const programs = [] - const items = parseItems(context.content) - - for (let item of items) { - const programDetail = await parseProgramDetail(item) - - programs.push({ - title: parseTitle(programDetail), - start: parseStart(item), - stop: parseStop(item), - image: parseImage(programDetail), - category: parseCategory(programDetail), - description: parseDescription(programDetail) - }) - } - - return programs - } -} - -function parseItems(content) { - if (content != '') { - const data = JSON.parse(content) - return !data || !data.responseBody || !Array.isArray(data.responseBody.dsSchWeek) - ? [] - : data.responseBody.dsSchWeek - } else { - return [] - } -} - -function parseStart(item) { - return dayjs.tz(item.broadYmd + ' ' + item.broadHm, 'YYYYMMDD HHmm', 'Asia/Seoul') -} - -function parseStop(item) { - return dayjs - .tz(item.broadYmd + ' ' + item.broadHm, 'YYYYMMDD HHmm', 'Asia/Seoul') - .add(item.broadRun, 'minute') -} - -async function parseProgramDetail(item) { - return axios - .post( - 'https://www.arirang.com/v1.0/open/program/detail', - { - bis_program_code: item.pgmCd - }, - { - headers: { - Accept: 'application/json, text/plain, */*', - 'Content-Type': 'application/json', - Origin: 'https://www.arirang.com', - Referer: 'https://www.arirang.com/schedule', - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36' - }, - timeout: 5000, - cache: { ttl: 60 * 1000 } - } - ) - .then(response => { - // console.log('Retrieved program detail: bis_program_code ' + item.pgmCd) - return response.data - }) - .catch(function () { - // The provider/server may not have details on every single programs. - // console.log('Unavailable program detail: bis_program_code ' + item.pgmCd) - }) -} - -function parseTitle(programDetail) { - if (programDetail && programDetail.title && programDetail.title[0] && programDetail.title[0].text) { - return programDetail.title[0].text - } else { - return '' - } -} - -function parseImage(programDetail) { - if (programDetail && programDetail.image && programDetail.image[0].url) { - return programDetail.image[0].url - } else { - return '' - } -} - -function parseCategory(programDetail) { - if (programDetail && programDetail.category_Info && programDetail.category_Info[0].title) { - return programDetail.category_Info[0].title - } else { - return '' - } -} - -function parseDescription(programDetail) { - if ( - programDetail && - programDetail.content && - programDetail.content[0] && - programDetail.content[0].text - ) { - let description = programDetail.content[0].text - let regex = /(<([^>]+)>)/gi - return description.replace(regex, '') - } else { - return '' - } +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') +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'arirang.com', + output: 'arirang.com.guide.xml', + channels: 'arirang.com.channels.xml', + lang: 'en', + days: 7, + delay: 5000, + url: 'https://www.arirang.com/v1.0/open/external/proxy', + + request: { + method: 'POST', + timeout: 5000, + cache: { ttl: 60 * 60 * 1000 }, + headers: { + Accept: 'application/json, text/plain, */*', + 'Content-Type': 'application/json', + Origin: 'https://www.arirang.com', + Referer: 'https://www.arirang.com/schedule', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36' + }, + data: function (context) { + const { channel, date } = context + return { + address: 'https://script.arirang.com/api/v1/bis/listScheduleV3.do', + method: 'POST', + headers: {}, + body: { + data: { + dmParam: { + chanId: channel.site_id, + broadYmd: dayjs.tz(date, 'Asia/Seoul').format('YYYYMMDD'), + planNo: '1' + } + } + } + } + } + }, + + logo: function (context) { + return context.channel.logo + }, + + async parser(context) { + const programs = [] + const items = parseItems(context.content) + + for (let item of items) { + const programDetail = await parseProgramDetail(item) + + programs.push({ + title: parseTitle(programDetail), + start: parseStart(item), + stop: parseStop(item), + image: parseImage(programDetail), + category: parseCategory(programDetail), + description: parseDescription(programDetail) + }) + } + + return programs + } +} + +function parseItems(content) { + if (content != '') { + const data = JSON.parse(content) + return !data || !data.responseBody || !Array.isArray(data.responseBody.dsSchWeek) + ? [] + : data.responseBody.dsSchWeek + } else { + return [] + } +} + +function parseStart(item) { + return dayjs.tz(item.broadYmd + ' ' + item.broadHm, 'YYYYMMDD HHmm', 'Asia/Seoul') +} + +function parseStop(item) { + return dayjs + .tz(item.broadYmd + ' ' + item.broadHm, 'YYYYMMDD HHmm', 'Asia/Seoul') + .add(item.broadRun, 'minute') +} + +async function parseProgramDetail(item) { + return axios + .post( + 'https://www.arirang.com/v1.0/open/program/detail', + { + bis_program_code: item.pgmCd + }, + { + headers: { + Accept: 'application/json, text/plain, */*', + 'Content-Type': 'application/json', + Origin: 'https://www.arirang.com', + Referer: 'https://www.arirang.com/schedule', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36' + }, + timeout: 5000, + cache: { ttl: 60 * 1000 } + } + ) + .then(response => { + // console.log('Retrieved program detail: bis_program_code ' + item.pgmCd) + return response.data + }) + .catch(function () { + // The provider/server may not have details on every single programs. + // console.log('Unavailable program detail: bis_program_code ' + item.pgmCd) + }) +} + +function parseTitle(programDetail) { + if (programDetail && programDetail.title && programDetail.title[0] && programDetail.title[0].text) { + return programDetail.title[0].text + } else { + return '' + } +} + +function parseImage(programDetail) { + if (programDetail && programDetail.image && programDetail.image[0].url) { + return programDetail.image[0].url + } else { + return '' + } +} + +function parseCategory(programDetail) { + if (programDetail && programDetail.category_Info && programDetail.category_Info[0].title) { + return programDetail.category_Info[0].title + } else { + return '' + } +} + +function parseDescription(programDetail) { + if ( + programDetail && + programDetail.content && + programDetail.content[0] && + programDetail.content[0].text + ) { + let description = programDetail.content[0].text + let regex = /(<([^>]+)>)/gi + return description.replace(regex, '') + } else { + return '' + } } \ No newline at end of file diff --git a/sites/arirang.com/arirang.com.test.js b/sites/arirang.com/arirang.com.test.js index 2efbbae9..ed3bf49b 100644 --- a/sites/arirang.com/arirang.com.test.js +++ b/sites/arirang.com/arirang.com.test.js @@ -1,72 +1,72 @@ -const { url, parser } = require('./arirang.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') -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.tz('2025-04-20', 'Asia/Seoul').startOf('d') -const channel = { - xmltv_id: 'ArirangWorld.kr', - site_id: 'CH_W', - name: 'Arirang World', - lang: 'en', - logo: 'https://i.imgur.com/5Aoithj.png' -} -const content = fs.readFileSync(path.resolve(__dirname, '__data__/schedule.json'), 'utf8') -const programDetail = fs.readFileSync(path.resolve(__dirname, '__data__/detail.json'), 'utf8') -const context = { channel: channel, content: content, date: date } - -it('can generate valid url', () => { - expect(url).toBe('https://www.arirang.com/v1.0/open/external/proxy') -}) - -it('can handle empty guide', async () => { - const results = await parser({ channel: channel, content: '', date: date }) - expect(results).toMatchObject([]) -}) - -it('can parse response', async () => { - axios.post.mockImplementation((url, data) => { - if ( - url === 'https://www.arirang.com/v1.0/open/external/proxy' && - JSON.stringify(data) === - JSON.stringify({ - address: 'https://script.arirang.com/api/v1/bis/listScheduleV3.do', - method: 'POST', - headers: {}, - body: { data: { dmParam: { chanId: 'CH_W', broadYmd: '20250420', planNo: '1' } } } - }) - ) { - return Promise.resolve({ - data: JSON.parse(content) - }) - } else if ( - url === 'https://www.arirang.com/v1.0/open/program/detail' && - JSON.stringify(data) === JSON.stringify({ bis_program_code: '2025006T' }) - ) { - return Promise.resolve({ - data: JSON.parse(programDetail) - }) - } else { - return Promise.resolve({ - data: '' - }) - } - }) - - const results = await parser(context) - - expect(results[0]).toMatchObject({ - title: 'Diplomat Archives: Hidden Stories', - start: dayjs.tz(date, 'Asia/Seoul'), - stop: dayjs.tz(date, 'Asia/Seoul').add(30, 'minute'), - image: - 'https://img.arirang.com/v1/AUTH_d52449c16d3b4bbca17d4fffd9fc44af/public/images/202504/2985531324875408146.jpg', - description: 'As of April 2025, S. Korea has established diplomatic relations with a total of 194 countries.\nAmong them are countries that have had ties and exchanges with Korea for hundreds of years.\nWith such long-standing relationships with so many nations,\nmight there be fascinating hidden stories between Korea and the rest of the world that we don’t know yet? \n\n"Diplomat’s Archives: Hidden Stories" begins with this very question.\nTogether with foreign embassies in Korea, the series uncovers and sheds light on meaningful yet lesser-known stories between Korea and other countries.\nThrough this, we aim to reaffirm the deep friendships that have been built over time, highlight how countries are interconnected—bilaterally and multilaterally—\nand emphasize the importance of cooperation on the global stage today.', - category: 'Current Affairs' - }) +const { url, parser } = require('./arirang.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') +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.tz('2025-04-20', 'Asia/Seoul').startOf('d') +const channel = { + xmltv_id: 'ArirangWorld.kr', + site_id: 'CH_W', + name: 'Arirang World', + lang: 'en', + logo: 'https://i.imgur.com/5Aoithj.png' +} +const content = fs.readFileSync(path.resolve(__dirname, '__data__/schedule.json'), 'utf8') +const programDetail = fs.readFileSync(path.resolve(__dirname, '__data__/detail.json'), 'utf8') +const context = { channel: channel, content: content, date: date } + +it('can generate valid url', () => { + expect(url).toBe('https://www.arirang.com/v1.0/open/external/proxy') +}) + +it('can handle empty guide', async () => { + const results = await parser({ channel: channel, content: '', date: date }) + expect(results).toMatchObject([]) +}) + +it('can parse response', async () => { + axios.post.mockImplementation((url, data) => { + if ( + url === 'https://www.arirang.com/v1.0/open/external/proxy' && + JSON.stringify(data) === + JSON.stringify({ + address: 'https://script.arirang.com/api/v1/bis/listScheduleV3.do', + method: 'POST', + headers: {}, + body: { data: { dmParam: { chanId: 'CH_W', broadYmd: '20250420', planNo: '1' } } } + }) + ) { + return Promise.resolve({ + data: JSON.parse(content) + }) + } else if ( + url === 'https://www.arirang.com/v1.0/open/program/detail' && + JSON.stringify(data) === JSON.stringify({ bis_program_code: '2025006T' }) + ) { + return Promise.resolve({ + data: JSON.parse(programDetail) + }) + } else { + return Promise.resolve({ + data: '' + }) + } + }) + + const results = await parser(context) + + expect(results[0]).toMatchObject({ + title: 'Diplomat Archives: Hidden Stories', + start: dayjs.tz(date, 'Asia/Seoul'), + stop: dayjs.tz(date, 'Asia/Seoul').add(30, 'minute'), + image: + 'https://img.arirang.com/v1/AUTH_d52449c16d3b4bbca17d4fffd9fc44af/public/images/202504/2985531324875408146.jpg', + description: 'As of April 2025, S. Korea has established diplomatic relations with a total of 194 countries.\nAmong them are countries that have had ties and exchanges with Korea for hundreds of years.\nWith such long-standing relationships with so many nations,\nmight there be fascinating hidden stories between Korea and the rest of the world that we don’t know yet? \n\n"Diplomat’s Archives: Hidden Stories" begins with this very question.\nTogether with foreign embassies in Korea, the series uncovers and sheds light on meaningful yet lesser-known stories between Korea and other countries.\nThrough this, we aim to reaffirm the deep friendships that have been built over time, highlight how countries are interconnected—bilaterally and multilaterally—\nand emphasize the importance of cooperation on the global stage today.', + category: 'Current Affairs' + }) }) \ No newline at end of file diff --git a/sites/artonline.tv/__data__/content.json b/sites/artonline.tv/__data__/content.json new file mode 100644 index 00000000..9abba009 --- /dev/null +++ b/sites/artonline.tv/__data__/content.json @@ -0,0 +1 @@ +[{"id":158963,"eventid":null,"duration":"01:34:00","lang":"Arabic","title":"الراقصه و السياسي","description":"تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .","thumbnail":"/UploadImages/Channel/ARTAFLAM1/03/AlRaqesaWaAlSeyasi.jpg","image":"0","start_Time":"00:30","adddate":"3/4/2022 12:00:00 AM","repeat1":null,"iD_genre":0,"iD_Show_Type":0,"iD_Channel":77,"iD_country":0,"iD_rating":0,"end_time":"02:04","season_Number":0,"epoisode_Number":0,"hasCatchup":0,"cmsid":0,"containerID":0,"imagePath":"../../UploadImages/Channel/ARTAFLAM1/3/","youtube":"0","published_at":"0","directed_by":"0","composition":"0","cast":"0","timeShow":null,"short_description":"تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .","seOdescription":null,"tagseo":null,"channel_name":null,"pathimage":null,"pathThumbnail":null}] \ No newline at end of file diff --git a/sites/artonline.tv/artonline.tv.config.js b/sites/artonline.tv/artonline.tv.config.js index 2b9fe78c..1b38c8d8 100644 --- a/sites/artonline.tv/artonline.tv.config.js +++ b/sites/artonline.tv/artonline.tv.config.js @@ -1,70 +1,70 @@ -process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0 - -const customParseFormat = require('dayjs/plugin/customParseFormat') -const timezone = require('dayjs/plugin/timezone') -const utc = require('dayjs/plugin/utc') -const dayjs = require('dayjs') - -dayjs.extend(customParseFormat) -dayjs.extend(timezone) -dayjs.extend(utc) - -module.exports = { - site: 'artonline.tv', - days: 2, - url: function ({ channel }) { - const [, site_id] = channel.site_id.split('#') - - return `https://www.artonline.tv/Home/Tvlist${site_id}` - }, - request: { - method: 'POST', - headers: { - 'content-type': 'application/x-www-form-urlencoded' - }, - data: function ({ date }) { - const diff = date.diff(dayjs.utc().startOf('d'), 'd') - const params = new URLSearchParams() - params.append('objId', diff) - - return params - } - }, - parser: function ({ content }) { - const programs = [] - if (!content) return programs - const items = JSON.parse(content) - items.forEach(item => { - const image = parseImage(item) - const start = parseStart(item) - const duration = parseDuration(item) - const stop = start.add(duration, 's') - programs.push({ - title: item.title, - description: item.description, - image, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item) { - const [, M, D, YYYY] = item.adddate.match(/(\d+)\/(\d+)\/(\d+) /) - const [HH, mm] = item.start_Time.split(':') - - return dayjs.tz(`${YYYY}-${M}-${D}T${HH}:${mm}:00`, 'YYYY-M-DTHH:mm:ss', 'Asia/Riyadh') -} - -function parseDuration(item) { - const [, HH, mm, ss] = item.duration.match(/(\d+):(\d+):(\d+)/) - - return parseInt(HH) * 3600 + parseInt(mm) * 60 + parseInt(ss) -} - -function parseImage(item) { - return item.thumbnail ? `https://www.artonline.tv${item.thumbnail}` : null -} +process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0 + +const customParseFormat = require('dayjs/plugin/customParseFormat') +const timezone = require('dayjs/plugin/timezone') +const utc = require('dayjs/plugin/utc') +const dayjs = require('dayjs') + +dayjs.extend(customParseFormat) +dayjs.extend(timezone) +dayjs.extend(utc) + +module.exports = { + site: 'artonline.tv', + days: 2, + url: function ({ channel }) { + const [, site_id] = channel.site_id.split('#') + + return `https://www.artonline.tv/Home/Tvlist${site_id}` + }, + request: { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded' + }, + data: function ({ date }) { + const diff = date.diff(dayjs.utc().startOf('d'), 'd') + const params = new URLSearchParams() + params.append('objId', diff) + + return params + } + }, + parser: function ({ content }) { + const programs = [] + if (!content) return programs + const items = JSON.parse(content) + items.forEach(item => { + const image = parseImage(item) + const start = parseStart(item) + const duration = parseDuration(item) + const stop = start.add(duration, 's') + programs.push({ + title: item.title, + description: item.description, + image, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item) { + const [, M, D, YYYY] = item.adddate.match(/(\d+)\/(\d+)\/(\d+) /) + const [HH, mm] = item.start_Time.split(':') + + return dayjs.tz(`${YYYY}-${M}-${D}T${HH}:${mm}:00`, 'YYYY-M-DTHH:mm:ss', 'Asia/Riyadh') +} + +function parseDuration(item) { + const [, HH, mm, ss] = item.duration.match(/(\d+):(\d+):(\d+)/) + + return parseInt(HH) * 3600 + parseInt(mm) * 60 + parseInt(ss) +} + +function parseImage(item) { + return item.thumbnail ? `https://www.artonline.tv${item.thumbnail}` : null +} diff --git a/sites/artonline.tv/artonline.tv.test.js b/sites/artonline.tv/artonline.tv.test.js index 121f4ad5..cf6e020d 100644 --- a/sites/artonline.tv/artonline.tv.test.js +++ b/sites/artonline.tv/artonline.tv.test.js @@ -1,65 +1,66 @@ -const { parser, url, request } = require('./artonline.tv.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const channel = { - site_id: '#Aflam2', - xmltv_id: 'ARTAflam2.sa' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://www.artonline.tv/Home/TvlistAflam2') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'content-type': 'application/x-www-form-urlencoded' - }) -}) - -it('can generate valid request data for today', () => { - const date = dayjs.utc().startOf('d') - const data = request.data({ date }) - expect(data.get('objId')).toBe('0') -}) - -it('can generate valid request data for tomorrow', () => { - const date = dayjs.utc().startOf('d').add(1, 'd') - const data = request.data({ date }) - expect(data.get('objId')).toBe('1') -}) - -it('can parse response', () => { - const content = - '[{"id":158963,"eventid":null,"duration":"01:34:00","lang":"Arabic","title":"الراقصه و السياسي","description":"تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .","thumbnail":"/UploadImages/Channel/ARTAFLAM1/03/AlRaqesaWaAlSeyasi.jpg","image":"0","start_Time":"00:30","adddate":"3/4/2022 12:00:00 AM","repeat1":null,"iD_genre":0,"iD_Show_Type":0,"iD_Channel":77,"iD_country":0,"iD_rating":0,"end_time":"02:04","season_Number":0,"epoisode_Number":0,"hasCatchup":0,"cmsid":0,"containerID":0,"imagePath":"../../UploadImages/Channel/ARTAFLAM1/3/","youtube":"0","published_at":"0","directed_by":"0","composition":"0","cast":"0","timeShow":null,"short_description":"تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .","seOdescription":null,"tagseo":null,"channel_name":null,"pathimage":null,"pathThumbnail":null}]' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-03T21:30:00.000Z', - stop: '2022-03-03T23:04:00.000Z', - title: 'الراقصه و السياسي', - description: - 'تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .', - image: 'https://www.artonline.tv/UploadImages/Channel/ARTAFLAM1/03/AlRaqesaWaAlSeyasi.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./artonline.tv.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 channel = { + site_id: '#Aflam2', + xmltv_id: 'ARTAflam2.sa' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://www.artonline.tv/Home/TvlistAflam2') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'content-type': 'application/x-www-form-urlencoded' + }) +}) + +it('can generate valid request data for today', () => { + const date = dayjs.utc().startOf('d') + const data = request.data({ date }) + expect(data.get('objId')).toBe('0') +}) + +it('can generate valid request data for tomorrow', () => { + const date = dayjs.utc().startOf('d').add(1, 'd') + const data = request.data({ date }) + expect(data.get('objId')).toBe('1') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-03T21:30:00.000Z', + stop: '2022-03-03T23:04:00.000Z', + title: 'الراقصه و السياسي', + description: + 'تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .', + image: 'https://www.artonline.tv/UploadImages/Channel/ARTAFLAM1/03/AlRaqesaWaAlSeyasi.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/awilime.com/awilime.com.config.js b/sites/awilime.com/awilime.com.config.js index 1617df02..05ad06ad 100644 --- a/sites/awilime.com/awilime.com.config.js +++ b/sites/awilime.com/awilime.com.config.js @@ -1,86 +1,86 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const { DateTime } = require('luxon') - -module.exports = { - site: 'awilime.com', - days: 2, - url({ channel, date }) { - return `https://www.awilime.com/tv/napi_musor/${channel.site_id}/${date.format('YYYY_MM_DD')}` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (!start) return - if (prev) { - prev.stop = start - } - const stop = start.plus({ minute: 30 }) - - programs.push({ - title: parseTitle($item), - sub_title: parseSubTitle($item), - description: parseDescription($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const html = await axios - .get('https://www.awilime.com/tv/napi_musor') - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(html) - const items = $('#body > div.tk > div > div').toArray() - - const channels = [] - items.forEach(item => { - const name = $(item).find('a').text().trim() - const url = $(item).find('a').attr('href') - const [, site_id] = url.match(/\/tv\/napi_musor\/(.*)/) || [null, null] - if (!site_id) return - if (channels.find(channel => channel.site_id === site_id)) return - - channels.push({ - lang: 'hu', - site_id, - name - }) - }) - - return channels - } -} - -function parseTitle($item) { - return $item('b > a').text().trim() -} - -function parseSubTitle($item) { - return $item('i').clone().children().remove('s').end().text().trim() -} - -function parseDescription($item) { - return $item('p').text().trim() -} - -function parseStart($item, date) { - let time = $item('b').clone().children().remove().end().text().trim() - if (!time || !/^\d/.test(time)) return null - time = `${date.format('YYYY-MM-DD')} ${time}` - - return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Budapest' }).toUTC() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#body > div.tdc > div.td2 > div').toArray() -} +const cheerio = require('cheerio') +const axios = require('axios') +const { DateTime } = require('luxon') + +module.exports = { + site: 'awilime.com', + days: 2, + url({ channel, date }) { + return `https://www.awilime.com/tv/napi_musor/${channel.site_id}/${date.format('YYYY_MM_DD')}` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (!start) return + if (prev) { + prev.stop = start + } + const stop = start.plus({ minute: 30 }) + + programs.push({ + title: parseTitle($item), + sub_title: parseSubTitle($item), + description: parseDescription($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const html = await axios + .get('https://www.awilime.com/tv/napi_musor') + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(html) + const items = $('#body > div.tk > div > div').toArray() + + const channels = [] + items.forEach(item => { + const name = $(item).find('a').text().trim() + const url = $(item).find('a').attr('href') + const [, site_id] = url.match(/\/tv\/napi_musor\/(.*)/) || [null, null] + if (!site_id) return + if (channels.find(channel => channel.site_id === site_id)) return + + channels.push({ + lang: 'hu', + site_id, + name + }) + }) + + return channels + } +} + +function parseTitle($item) { + return $item('b > a').text().trim() +} + +function parseSubTitle($item) { + return $item('i').clone().children().remove('s').end().text().trim() +} + +function parseDescription($item) { + return $item('p').text().trim() +} + +function parseStart($item, date) { + let time = $item('b').clone().children().remove().end().text().trim() + if (!time || !/^\d/.test(time)) return null + time = `${date.format('YYYY-MM-DD')} ${time}` + + return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Budapest' }).toUTC() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('#body > div.tdc > div.td2 > div').toArray() +} diff --git a/sites/awilime.com/awilime.com.test.js b/sites/awilime.com/awilime.com.test.js index 08da5912..9ce3f39e 100644 --- a/sites/awilime.com/awilime.com.test.js +++ b/sites/awilime.com/awilime.com.test.js @@ -1,49 +1,49 @@ -const { parser, url } = require('./awilime.com.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('2024-06-26', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'budapest_europa_tv', - xmltv_id: 'BudapestEuropaTelevizio.hu' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.awilime.com/tv/napi_musor/budapest_europa_tv/2024_06_26' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(15) - - expect(results[3]).toMatchObject({ - start: '2024-06-26T07:00:00.000Z', - stop: '2024-06-26T08:00:00.000Z', - title: 'Ébredés', - sub_title: 'Amerikai dokumentumfilm (2018)', - description: 'Balla Tibor misszionárius' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - 'Object moved

    Object moved to here.

    ' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./awilime.com.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('2024-06-26', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'budapest_europa_tv', + xmltv_id: 'BudapestEuropaTelevizio.hu' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.awilime.com/tv/napi_musor/budapest_europa_tv/2024_06_26' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(15) + + expect(results[3]).toMatchObject({ + start: '2024-06-26T07:00:00.000Z', + stop: '2024-06-26T08:00:00.000Z', + title: 'Ébredés', + sub_title: 'Amerikai dokumentumfilm (2018)', + description: 'Balla Tibor misszionárius' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: + 'Object moved

    Object moved to here.

    ' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/bein.com/bein.com.config.js b/sites/bein.com/bein.com.config.js index 463e1026..16100b1f 100644 --- a/sites/bein.com/bein.com.config.js +++ b/sites/bein.com/bein.com.config.js @@ -1,110 +1,110 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const cheerio = require('cheerio') -const { DateTime } = require('luxon') - -module.exports = { - site: 'bein.com', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: function ({ date, channel }) { - const [category] = channel.site_id.split('#') - const postid = channel.lang === 'ar' ? '25344' : '25356' - - return `https://www.bein.com/${ - channel.lang - }/epg-ajax-template/?action=epg_fetch&category=${category}&cdate=${date.format( - 'YYYY-MM-DD' - )}&language=${channel.lang.toUpperCase()}&loadindex=0&mins=00&offset=0&postid=${postid}&serviceidentity=bein.net` - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel) - date = DateTime.fromMillis(date.valueOf()).minus({ days: 1 }) - items.forEach(item => { - const $item = cheerio.load(item) - const title = parseTitle($item) - if (!title) return - const category = parseCategory($item) - const prev = programs[programs.length - 1] - let start = parseTime($item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.plus({ days: 1 }) - } - prev.stop = start - } - let stop = parseTime($item, start) - if (stop < start) { - stop = stop.plus({ days: 1 }) - } - programs.push({ - title, - category, - start, - stop - }) - }) - - return programs - }, - async channels({ lang }) { - const categories = ['entertainment', 'sports'] - - let channels = [] - for (let category of categories) { - const url = `https://www.bein.com/en/epg-ajax-template/?action=epg_fetch&offset=0&category=${category}&serviceidentity=bein.net&mins=00&cdate=${dayjs().format( - 'YYYY-MM-DD' - )}&language=${lang.toUpperCase()}&postid=25356&loadindex=0` - const data = await axios - .get(url) - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(data) - $('.container-tvguide > div').each((i, el) => { - const id = $(el).attr('id') - if (!id || !/^channels_\d+/.test(id)) return - const [, channelId] = id.split('_') - - channels.push({ - lang, - site_id: `${category}#${channelId}`, - name: channelId - }) - }) - } - - return channels - } -} - -function parseTitle($item) { - return $item('.title').text() -} - -function parseCategory($item) { - return $item('.format').text() -} - -function parseTime($item, date) { - let [, time] = $item('.time') - .text() - .match(/^(\d{2}:\d{2})/) || [null, null] - if (!time) return null - time = `${date.toFormat('yyyy-MM-dd')} ${time}` - - return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Qatar' }).toUTC() -} - -function parseItems(content, channel) { - const [, channelId] = channel.site_id.split('#') - const $ = cheerio.load(content) - - return $(`#channels_${channelId} .slider > ul:first-child > li`).toArray() -} +const axios = require('axios') +const dayjs = require('dayjs') +const cheerio = require('cheerio') +const { DateTime } = require('luxon') + +module.exports = { + site: 'bein.com', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: function ({ date, channel }) { + const [category] = channel.site_id.split('#') + const postid = channel.lang === 'ar' ? '25344' : '25356' + + return `https://www.bein.com/${ + channel.lang + }/epg-ajax-template/?action=epg_fetch&category=${category}&cdate=${date.format( + 'YYYY-MM-DD' + )}&language=${channel.lang.toUpperCase()}&loadindex=0&mins=00&offset=0&postid=${postid}&serviceidentity=bein.net` + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel) + date = DateTime.fromMillis(date.valueOf()).minus({ days: 1 }) + items.forEach(item => { + const $item = cheerio.load(item) + const title = parseTitle($item) + if (!title) return + const category = parseCategory($item) + const prev = programs[programs.length - 1] + let start = parseTime($item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.plus({ days: 1 }) + } + prev.stop = start + } + let stop = parseTime($item, start) + if (stop < start) { + stop = stop.plus({ days: 1 }) + } + programs.push({ + title, + category, + start, + stop + }) + }) + + return programs + }, + async channels({ lang }) { + const categories = ['entertainment', 'sports'] + + let channels = [] + for (let category of categories) { + const url = `https://www.bein.com/en/epg-ajax-template/?action=epg_fetch&offset=0&category=${category}&serviceidentity=bein.net&mins=00&cdate=${dayjs().format( + 'YYYY-MM-DD' + )}&language=${lang.toUpperCase()}&postid=25356&loadindex=0` + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(data) + $('.container-tvguide > div').each((i, el) => { + const id = $(el).attr('id') + if (!id || !/^channels_\d+/.test(id)) return + const [, channelId] = id.split('_') + + channels.push({ + lang, + site_id: `${category}#${channelId}`, + name: channelId + }) + }) + } + + return channels + } +} + +function parseTitle($item) { + return $item('.title').text() +} + +function parseCategory($item) { + return $item('.format').text() +} + +function parseTime($item, date) { + let [, time] = $item('.time') + .text() + .match(/^(\d{2}:\d{2})/) || [null, null] + if (!time) return null + time = `${date.toFormat('yyyy-MM-dd')} ${time}` + + return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Qatar' }).toUTC() +} + +function parseItems(content, channel) { + const [, channelId] = channel.site_id.split('#') + const $ = cheerio.load(content) + + return $(`#channels_${channelId} .slider > ul:first-child > li`).toArray() +} diff --git a/sites/bein.com/bein.com.test.js b/sites/bein.com/bein.com.test.js index 9dad1339..ad56ad64 100644 --- a/sites/bein.com/bein.com.test.js +++ b/sites/bein.com/bein.com.test.js @@ -1,58 +1,58 @@ -const fs = require('fs') -const path = require('path') -const { parser, url } = require('./bein.com.config.js') -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('2023-01-19', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'entertainment#1', xmltv_id: 'beINMovies1Premiere.qa', lang: 'en' } - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe( - 'https://www.bein.com/en/epg-ajax-template/?action=epg_fetch&category=entertainment&cdate=2023-01-19&language=EN&loadindex=0&mins=00&offset=0&postid=25356&serviceidentity=bein.net' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve('sites/bein.com/__data__/content.html')) - const results = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-18T20:15:00.000Z', - stop: '2023-01-18T22:15:00.000Z', - title: 'The Walk', - category: 'Movies' - }) - - expect(results[1]).toMatchObject({ - start: '2023-01-18T22:15:00.000Z', - stop: '2023-01-19T00:00:00.000Z', - title: 'Resident Evil: Welcome To Raccoon City', - category: 'Movies' - }) - - expect(results[10]).toMatchObject({ - start: '2023-01-19T15:30:00.000Z', - stop: '2023-01-19T18:00:00.000Z', - title: 'Spider-Man: No Way Home', - category: 'Movies' - }) -}) - -it('can handle empty guide', () => { - const noContent = fs.readFileSync(path.resolve('sites/bein.com/__data__/no-content.html')) - const result = parser({ - date, - channel, - content: noContent - }) - expect(result).toMatchObject([]) -}) +const fs = require('fs') +const path = require('path') +const { parser, url } = require('./bein.com.config.js') +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('2023-01-19', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'entertainment#1', xmltv_id: 'beINMovies1Premiere.qa', lang: 'en' } + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe( + 'https://www.bein.com/en/epg-ajax-template/?action=epg_fetch&category=entertainment&cdate=2023-01-19&language=EN&loadindex=0&mins=00&offset=0&postid=25356&serviceidentity=bein.net' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve('sites/bein.com/__data__/content.html')) + const results = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-18T20:15:00.000Z', + stop: '2023-01-18T22:15:00.000Z', + title: 'The Walk', + category: 'Movies' + }) + + expect(results[1]).toMatchObject({ + start: '2023-01-18T22:15:00.000Z', + stop: '2023-01-19T00:00:00.000Z', + title: 'Resident Evil: Welcome To Raccoon City', + category: 'Movies' + }) + + expect(results[10]).toMatchObject({ + start: '2023-01-19T15:30:00.000Z', + stop: '2023-01-19T18:00:00.000Z', + title: 'Spider-Man: No Way Home', + category: 'Movies' + }) +}) + +it('can handle empty guide', () => { + const noContent = fs.readFileSync(path.resolve('sites/bein.com/__data__/no-content.html')) + const result = parser({ + date, + channel, + content: noContent + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/beinsports.com/beinsports.com.config.js b/sites/beinsports.com/beinsports.com.config.js index 41b6c899..cc6c0e48 100644 --- a/sites/beinsports.com/beinsports.com.config.js +++ b/sites/beinsports.com/beinsports.com.config.js @@ -1,73 +1,73 @@ -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') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'beinsports.com', - days: 2, - request: { - headers: { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36' - } - }, - url: function ({ date, channel }) { - return `https://www.beinsports.com/api/opta/tv-event?&startBefore=${date - .add(1, 'd') - .format('YYYY-MM-DDTHH:mm:ss.SSS')}Z&endAfter=${date.format( - 'YYYY-MM-DDTHH:mm:ss.SSS' - )}Z&channelIds=${channel.site_id}` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - if (!items.length == 0) { - items.forEach(item => { - const start = dayjs.utc(item.startDate) - const stop = dayjs.utc(item.endDate) - programs.push({ - title: item.title, - description: item.description, - start, - stop - }) - }) - } - return programs - }, - async channels({ region, lang }) { - const data = await axios - .get(`https://www.beinsports.com/api/opta/tv-channel?region=${lang}-${region}`, this.request) - .then(r => r.data) - .catch(console.log) - - return data.rows.map(item => { - return { - lang, - site_id: item.id, - name: item.name - } - }) - } -} - -function parseItems(content) { - let data - try { - data = JSON.parse(content) - } catch { - return [] - } - - if (!data || !data['rows']) { - return [] - } - - return data.rows -} +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') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'beinsports.com', + days: 2, + request: { + headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36' + } + }, + url: function ({ date, channel }) { + return `https://www.beinsports.com/api/opta/tv-event?&startBefore=${date + .add(1, 'd') + .format('YYYY-MM-DDTHH:mm:ss.SSS')}Z&endAfter=${date.format( + 'YYYY-MM-DDTHH:mm:ss.SSS' + )}Z&channelIds=${channel.site_id}` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + if (!items.length == 0) { + items.forEach(item => { + const start = dayjs.utc(item.startDate) + const stop = dayjs.utc(item.endDate) + programs.push({ + title: item.title, + description: item.description, + start, + stop + }) + }) + } + return programs + }, + async channels({ region, lang }) { + const data = await axios + .get(`https://www.beinsports.com/api/opta/tv-channel?region=${lang}-${region}`, this.request) + .then(r => r.data) + .catch(console.log) + + return data.rows.map(item => { + return { + lang, + site_id: item.id, + name: item.name + } + }) + } +} + +function parseItems(content) { + let data + try { + data = JSON.parse(content) + } catch { + return [] + } + + if (!data || !data['rows']) { + return [] + } + + return data.rows +} diff --git a/sites/beinsports.com/beinsports.com.test.js b/sites/beinsports.com/beinsports.com.test.js index 563c18b1..2d2a8b2c 100644 --- a/sites/beinsports.com/beinsports.com.test.js +++ b/sites/beinsports.com/beinsports.com.test.js @@ -1,43 +1,43 @@ -const { parser, url } = require('./beinsports.com.config.js') -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('2023-10-22T00:00:00.000', '"YYYY-MM-DDTHH:mm:ss.SSS').startOf('d') -const channel = { site_id: 'C244C48D-3B54-406A-94C9-D63B16318267', xmltv_id: 'beINSportsUSA.us' } - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe( - 'https://www.beinsports.com/api/opta/tv-event?&startBefore=2023-10-23T00:00:00.000Z&endAfter=2023-10-22T00:00:00.000Z&channelIds=C244C48D-3B54-406A-94C9-D63B16318267' - ) -}) - -const content = - '{"count":1,"rows":[{"data":{"eventId":"2028126","eventDate":"2023-10-21T10:30:00","utcEventDate":"2023-10-20T23:30:00","duration":"90","programId":"106230","programTypeId":"5","title":"ATP 500"},"duration":5400000,"title":"Tokyo Day 5 QF 2","startDate":"2023-10-20T23:30:00.000Z","endDate":"2023-10-21T01:00:00.000Z","description":"Exclusive coverage of the 2023 ATP Tour on beIN SPORTS","channelId":"164C0EDA-EBCE-4AA6-9DDA-D603E0948B9F"}]}' - -it('can parse response', () => { - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2023-10-20T23:30:00.000Z', - stop: '2023-10-21T01:00:00.000Z', - title: 'Tokyo Day 5 QF 2', - description: 'Exclusive coverage of the 2023 ATP Tour on beIN SPORTS' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '[]' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./beinsports.com.config.js') +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('2023-10-22T00:00:00.000', '"YYYY-MM-DDTHH:mm:ss.SSS').startOf('d') +const channel = { site_id: 'C244C48D-3B54-406A-94C9-D63B16318267', xmltv_id: 'beINSportsUSA.us' } + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe( + 'https://www.beinsports.com/api/opta/tv-event?&startBefore=2023-10-23T00:00:00.000Z&endAfter=2023-10-22T00:00:00.000Z&channelIds=C244C48D-3B54-406A-94C9-D63B16318267' + ) +}) + +const content = + '{"count":1,"rows":[{"data":{"eventId":"2028126","eventDate":"2023-10-21T10:30:00","utcEventDate":"2023-10-20T23:30:00","duration":"90","programId":"106230","programTypeId":"5","title":"ATP 500"},"duration":5400000,"title":"Tokyo Day 5 QF 2","startDate":"2023-10-20T23:30:00.000Z","endDate":"2023-10-21T01:00:00.000Z","description":"Exclusive coverage of the 2023 ATP Tour on beIN SPORTS","channelId":"164C0EDA-EBCE-4AA6-9DDA-D603E0948B9F"}]}' + +it('can parse response', () => { + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2023-10-20T23:30:00.000Z', + stop: '2023-10-21T01:00:00.000Z', + title: 'Tokyo Day 5 QF 2', + description: 'Exclusive coverage of the 2023 ATP Tour on beIN SPORTS' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '[]' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/berrymedia.co.kr/berrymedia.co.kr.config.js b/sites/berrymedia.co.kr/berrymedia.co.kr.config.js index 63ab575a..773de12d 100644 --- a/sites/berrymedia.co.kr/berrymedia.co.kr.config.js +++ b/sites/berrymedia.co.kr/berrymedia.co.kr.config.js @@ -1,93 +1,93 @@ -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) - -dayjs.Ls.en.weekStart = 1 - -module.exports = { - site: 'berrymedia.co.kr', - days: 2, - url({ channel }) { - return `http://www.berrymedia.co.kr/schedule_proc${channel.site_id}.php` - }, - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - 'X-Requested-With': 'XMLHttpRequest' - }, - data({ date }) { - let params = new URLSearchParams() - let startOfWeek = date.startOf('week').format('YYYY-MM-DD') - let endOfWeek = date.endOf('week').format('YYYY-MM-DD') - - params.append('week', `${startOfWeek}~${endOfWeek}`) - params.append('day', date.format('YYYY-MM-DD')) - - return params - } - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - category: parseCategory($item), - rating: parseRating($item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart($item, date) { - const time = $item('span:nth-child(1)').text().trim() - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Seoul') -} - -function parseTitle($item) { - return $item('span.sdfsdf').clone().children().remove().end().text().trim() -} - -function parseCategory($item) { - return $item('span:nth-child(2) > p').text().trim() -} - -function parseRating($item) { - const rating = $item('span:nth-child(5) > p:nth-child(1)').text().trim() - - return rating - ? { - system: 'KMRB', - value: rating - } - : null -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.sc_time dd').toArray() -} +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) + +dayjs.Ls.en.weekStart = 1 + +module.exports = { + site: 'berrymedia.co.kr', + days: 2, + url({ channel }) { + return `http://www.berrymedia.co.kr/schedule_proc${channel.site_id}.php` + }, + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'X-Requested-With': 'XMLHttpRequest' + }, + data({ date }) { + let params = new URLSearchParams() + let startOfWeek = date.startOf('week').format('YYYY-MM-DD') + let endOfWeek = date.endOf('week').format('YYYY-MM-DD') + + params.append('week', `${startOfWeek}~${endOfWeek}`) + params.append('day', date.format('YYYY-MM-DD')) + + return params + } + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + category: parseCategory($item), + rating: parseRating($item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart($item, date) { + const time = $item('span:nth-child(1)').text().trim() + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Seoul') +} + +function parseTitle($item) { + return $item('span.sdfsdf').clone().children().remove().end().text().trim() +} + +function parseCategory($item) { + return $item('span:nth-child(2) > p').text().trim() +} + +function parseRating($item) { + const rating = $item('span:nth-child(5) > p:nth-child(1)').text().trim() + + return rating + ? { + system: 'KMRB', + value: rating + } + : null +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.sc_time dd').toArray() +} diff --git a/sites/berrymedia.co.kr/berrymedia.co.kr.test.js b/sites/berrymedia.co.kr/berrymedia.co.kr.test.js index b0824ca7..050694b5 100644 --- a/sites/berrymedia.co.kr/berrymedia.co.kr.test.js +++ b/sites/berrymedia.co.kr/berrymedia.co.kr.test.js @@ -1,77 +1,77 @@ -const { parser, url, request } = require('./berrymedia.co.kr.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('2023-01-26', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '', - xmltv_id: 'GTV.kr' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('http://www.berrymedia.co.kr/schedule_proc.php') -}) - -it('can generate request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - 'X-Requested-With': 'XMLHttpRequest' - }) -}) - -it('can generate valid request data', () => { - let params = request.data({ date }) - - expect(params.get('week')).toBe('2023-01-23~2023-01-29') - expect(params.get('day')).toBe('2023-01-26') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - 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: '2023-01-25T15:00:00.000Z', - stop: '2023-01-25T16:00:00.000Z', - title: '더트롯쇼', - category: '연예/오락', - rating: { - system: 'KMRB', - value: '15' - } - }) - - expect(results[17]).toMatchObject({ - start: '2023-01-26T13:50:00.000Z', - stop: '2023-01-26T14:20:00.000Z', - title: '나는 자연인이다', - category: '교양', - rating: { - system: 'KMRB', - value: 'ALL' - } - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - }) - - expect(results).toMatchObject([]) -}) +const { parser, url, request } = require('./berrymedia.co.kr.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('2023-01-26', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '', + xmltv_id: 'GTV.kr' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('http://www.berrymedia.co.kr/schedule_proc.php') +}) + +it('can generate request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'X-Requested-With': 'XMLHttpRequest' + }) +}) + +it('can generate valid request data', () => { + let params = request.data({ date }) + + expect(params.get('week')).toBe('2023-01-23~2023-01-29') + expect(params.get('day')).toBe('2023-01-26') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + 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: '2023-01-25T15:00:00.000Z', + stop: '2023-01-25T16:00:00.000Z', + title: '더트롯쇼', + category: '연예/오락', + rating: { + system: 'KMRB', + value: '15' + } + }) + + expect(results[17]).toMatchObject({ + start: '2023-01-26T13:50:00.000Z', + stop: '2023-01-26T14:20:00.000Z', + title: '나는 자연인이다', + category: '교양', + rating: { + system: 'KMRB', + value: 'ALL' + } + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/cableplus.com.uy/cableplus.com.uy.config.js b/sites/cableplus.com.uy/cableplus.com.uy.config.js index 65ad148b..5f4a2601 100644 --- a/sites/cableplus.com.uy/cableplus.com.uy.config.js +++ b/sites/cableplus.com.uy/cableplus.com.uy.config.js @@ -1,133 +1,133 @@ -const cheerio = require('cheerio') -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') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -const API_ENDPOINT = 'https://www.reportv.com.ar/finder' - -module.exports = { - site: 'cableplus.com.uy', - days: 2, - url: `${API_ENDPOINT}/channel`, - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }, - data({ date, channel }) { - const params = new URLSearchParams() - params.append('idAlineacion', '3017') - params.append('idSenial', channel.site_id) - params.append('fecha', date.format('YYYY-MM-DD')) - params.append('hora', '00:00') - - return params - } - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - categories: parseCategories($item), - image: parseImage($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const params = new URLSearchParams({ idAlineacion: '3017' }) - const data = await axios - .post(`${API_ENDPOINT}/channelGrid`, params, { - headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' } - }) - .then(r => r.data) - .catch(console.error) - const $ = cheerio.load(data) - - return $('.senial') - .map(function () { - return { - lang: 'es', - site_id: $(this).attr('id'), - name: $(this).find('img').attr('alt') - } - }) - .get() - } -} - -function parseTitle($item) { - return $item('p.evento_titulo.texto_a_continuacion.dotdotdot,.programa-titulo > span:first-child') - .text() - .trim() -} - -function parseImage($item) { - return $item('img').data('src') || $item('img').attr('src') || null -} - -function parseCategories($item) { - return $item('p.evento_genero') - .map(function () { - return $item(this).text().trim() - }) - .toArray() -} - -function parseStart($item, date) { - let time = $item('.grid_fecha_hora').text().trim() - - if (time) { - return dayjs.tz(`${date.format('YYYY')} ${time}`, 'YYYY DD-MM HH:mm[hs.]', 'America/Montevideo') - } - - time = $item('.fechaHora').text().trim() - - return time - ? dayjs.tz(`${date.format('YYYY')} ${time}`, 'YYYY DD/MM HH:mm[hs.]', 'America/Montevideo') - : null -} - -function parseItems(content, date) { - const $ = cheerio.load(content) - - let featuredItems = $('.vista-pc > .programacion-fila > .channel-programa') - .filter(function () { - return $(this).find('.grid_fecha_hora').text().indexOf(date.format('DD-MM')) > -1 - }) - .toArray() - let otherItems = $('#owl-pc > .item-program') - .filter(function () { - return ( - $(this) - .find('.evento_titulo > .horario > p.fechaHora') - .text() - .indexOf(date.format('DD/MM')) > -1 - ) - }) - .toArray() - - return featuredItems.concat(otherItems) -} +const cheerio = require('cheerio') +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') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +const API_ENDPOINT = 'https://www.reportv.com.ar/finder' + +module.exports = { + site: 'cableplus.com.uy', + days: 2, + url: `${API_ENDPOINT}/channel`, + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }, + data({ date, channel }) { + const params = new URLSearchParams() + params.append('idAlineacion', '3017') + params.append('idSenial', channel.site_id) + params.append('fecha', date.format('YYYY-MM-DD')) + params.append('hora', '00:00') + + return params + } + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + categories: parseCategories($item), + image: parseImage($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const params = new URLSearchParams({ idAlineacion: '3017' }) + const data = await axios + .post(`${API_ENDPOINT}/channelGrid`, params, { + headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' } + }) + .then(r => r.data) + .catch(console.error) + const $ = cheerio.load(data) + + return $('.senial') + .map(function () { + return { + lang: 'es', + site_id: $(this).attr('id'), + name: $(this).find('img').attr('alt') + } + }) + .get() + } +} + +function parseTitle($item) { + return $item('p.evento_titulo.texto_a_continuacion.dotdotdot,.programa-titulo > span:first-child') + .text() + .trim() +} + +function parseImage($item) { + return $item('img').data('src') || $item('img').attr('src') || null +} + +function parseCategories($item) { + return $item('p.evento_genero') + .map(function () { + return $item(this).text().trim() + }) + .toArray() +} + +function parseStart($item, date) { + let time = $item('.grid_fecha_hora').text().trim() + + if (time) { + return dayjs.tz(`${date.format('YYYY')} ${time}`, 'YYYY DD-MM HH:mm[hs.]', 'America/Montevideo') + } + + time = $item('.fechaHora').text().trim() + + return time + ? dayjs.tz(`${date.format('YYYY')} ${time}`, 'YYYY DD/MM HH:mm[hs.]', 'America/Montevideo') + : null +} + +function parseItems(content, date) { + const $ = cheerio.load(content) + + let featuredItems = $('.vista-pc > .programacion-fila > .channel-programa') + .filter(function () { + return $(this).find('.grid_fecha_hora').text().indexOf(date.format('DD-MM')) > -1 + }) + .toArray() + let otherItems = $('#owl-pc > .item-program') + .filter(function () { + return ( + $(this) + .find('.evento_titulo > .horario > p.fechaHora') + .text() + .indexOf(date.format('DD/MM')) > -1 + ) + }) + .toArray() + + return featuredItems.concat(otherItems) +} diff --git a/sites/cableplus.com.uy/cableplus.com.uy.test.js b/sites/cableplus.com.uy/cableplus.com.uy.test.js index 642a8ffa..1420dfed 100644 --- a/sites/cableplus.com.uy/cableplus.com.uy.test.js +++ b/sites/cableplus.com.uy/cableplus.com.uy.test.js @@ -1,73 +1,73 @@ -const { parser, url, request } = require('./cableplus.com.uy.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('2023-02-12', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2035', - xmltv_id: 'APlusV.uy' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.reportv.com.ar/finder/channel') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }) -}) - -it('can generate valid request data', () => { - const params = request.data({ date, channel }) - - expect(params.get('idAlineacion')).toBe('3017') - expect(params.get('idSenial')).toBe('2035') - expect(params.get('fecha')).toBe('2023-02-12') - expect(params.get('hora')).toBe('00:00') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(21) - - expect(results[0]).toMatchObject({ - start: '2023-02-12T09:30:00.000Z', - stop: '2023-02-12T10:30:00.000Z', - title: 'Revista agropecuaria', - image: 'https://www.reportv.com.ar/buscador/img/Programas/2797844.jpg', - categories: [] - }) - - expect(results[4]).toMatchObject({ - start: '2023-02-12T12:30:00.000Z', - stop: '2023-02-12T13:30:00.000Z', - title: 'De pago en pago', - image: 'https://www.reportv.com.ar/buscador/img/Programas/3772835.jpg', - categories: ['Cultural'] - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./cableplus.com.uy.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('2023-02-12', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2035', + xmltv_id: 'APlusV.uy' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.reportv.com.ar/finder/channel') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }) +}) + +it('can generate valid request data', () => { + const params = request.data({ date, channel }) + + expect(params.get('idAlineacion')).toBe('3017') + expect(params.get('idSenial')).toBe('2035') + expect(params.get('fecha')).toBe('2023-02-12') + expect(params.get('hora')).toBe('00:00') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(21) + + expect(results[0]).toMatchObject({ + start: '2023-02-12T09:30:00.000Z', + stop: '2023-02-12T10:30:00.000Z', + title: 'Revista agropecuaria', + image: 'https://www.reportv.com.ar/buscador/img/Programas/2797844.jpg', + categories: [] + }) + + expect(results[4]).toMatchObject({ + start: '2023-02-12T12:30:00.000Z', + stop: '2023-02-12T13:30:00.000Z', + title: 'De pago en pago', + image: 'https://www.reportv.com.ar/buscador/img/Programas/3772835.jpg', + categories: ['Cultural'] + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/canalplus.com/canalplus.com.config.js b/sites/canalplus.com/canalplus.com.config.js index d6d2679b..9f7fcd5e 100644 --- a/sites/canalplus.com/canalplus.com.config.js +++ b/sites/canalplus.com/canalplus.com.config.js @@ -1,197 +1,197 @@ -const dayjs = require('dayjs') -const axios = require('axios') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'canalplus.com', - days: 2, - url: async function ({ channel, date }) { - const [region, site_id] = channel.site_id.split('#') - - const baseUrl = - region === 'pl' - ? 'https://www.canalplus.com/pl/program-tv/' - : `https://www.canalplus.com/${region}/programme-tv/` - - const data = await axios - .get(baseUrl) - .then(r => r.data.toString()) - .catch(err => console.log(err)) - - const token = parseToken(data) - const path = region === 'pl' ? 'mycanalint' : 'mycanal' - const diff = date.diff(dayjs.utc().startOf('d'), 'd') - - return `https://hodor.canalplus.pro/api/v2/${path}/channels/${token}/${site_id}/broadcasts/day/${diff}` - }, - async parser({ content }) { - let programs = [] - const items = parseItems(content) - for (let item of items) { - const prev = programs[programs.length - 1] - const details = await loadProgramDetails(item) - const info = parseInfo(details) - const start = parseStart(item) - if (prev) prev.stop = start - const stop = start.add(1, 'h') - programs.push({ - title: item.title, - description: parseDescription(info), - image: parseImage(info), - actors: parseCast(info, 'Avec :'), - director: parseCast(info, 'De :'), - writer: parseCast(info, 'Scénario :'), - composer: parseCast(info, 'Musique :'), - presenter: parseCast(info, 'Présenté par :'), - date: parseDate(info), - rating: parseRating(info), - start, - stop - }) - } - - return programs - }, - async channels({ country }) { - const paths = { - ad: 'cpafr/ad', - bf: 'cpafr/bf', - bi: 'cpafr/bi', - bj: 'cpafr/bj', - bl: 'cpant/bl', - cd: 'cpafr/cd', - cf: 'cpafr/cf', - cg: 'cpafr/cg', - ch: 'cpche', - ci: 'cpafr/ci', - cm: 'cpafr/cm', - cv: 'cpafr/cv', - dj: 'cpafr/dj', - fr: 'cpfra', - ga: 'cpafr/ga', - gf: 'cpant/gf', - gh: 'cpafr/gh', - gm: 'cpafr/gm', - gn: 'cpafr/gn', - gp: 'cpafr/gp', - gw: 'cpafr/gw', - ht: 'cpant/ht', - mf: 'cpant/mf', - mg: 'cpafr/mg', - ml: 'cpafr/ml', - mq: 'cpant/mq', - mr: 'cpafr/mr', - mu: 'cpmus/mu', - nc: 'cpncl/nc', - ne: 'cpafr/ne', - pf: 'cppyf/pf', - pl: 'cppol', - re: 'cpreu/re', - rw: 'cpafr/rw', - sl: 'cpafr/sl', - sn: 'cpafr/sn', - td: 'cpafr/td', - tg: 'cpafr/tg', - wf: 'cpncl/wf', - yt: 'cpreu/yt' - } - - let channels = [] - const path = paths[country] - const url = `https://secure-webtv-static.canal-plus.com/metadata/${path}/all/v2.2/globalchannels.json` - const data = await axios - .get(url) - .then(r => r.data) - .catch(console.log) - - data.channels.forEach(channel => { - const site_id = country === 'fr' ? `#${channel.id}` : `${country}#${channel.id}` - - if (channel.name === '.') return - - channels.push({ - lang: 'fr', - site_id, - name: channel.name - }) - }) - - return channels - } -} - -function parseToken(data) { - const [, token] = data.match(/"token":"([^"]+)/) || [null, null] - - return token -} - -function parseStart(item) { - return item && item.startTime ? dayjs(item.startTime) : null -} - -function parseImage(info) { - return info ? info.URLImage : null -} - -function parseDescription(info) { - return info ? info.summary : null -} - -function parseInfo(data) { - if (!data || !data.detail || !data.detail.informations) return null - - return data.detail.informations -} - -async function loadProgramDetails(item) { - if (!item.onClick || !item.onClick.URLPage) return {} - - return await axios - .get(item.onClick.URLPage) - .then(r => r.data) - .catch(console.error) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.timeSlices)) return [] - - return data.timeSlices.reduce((acc, curr) => { - acc = acc.concat(curr.contents) - return acc - }, []) -} - -function parseCast(info, type) { - let people = [] - if (info && info.personnalities) { - const personnalities = info.personnalities.find(i => i.prefix == type) - if (!personnalities) return people - for (let person of personnalities.personnalitiesList) { - people.push(person.title) - } - } - return people -} - -function parseDate(info) { - return info && info.productionYear ? info.productionYear : null -} - -function parseRating(info) { - if (!info || !info.parentalRatings) return null - let rating = info.parentalRatings.find(i => i.authority === 'CSA') - if (!rating || Array.isArray(rating)) return null - if (rating.value === '1') return null - if (rating.value === '2') rating.value = '-10' - if (rating.value === '3') rating.value = '-12' - if (rating.value === '4') rating.value = '-16' - if (rating.value === '5') rating.value = '-18' - return { - system: rating.authority, - value: rating.value - } -} +const dayjs = require('dayjs') +const axios = require('axios') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'canalplus.com', + days: 2, + url: async function ({ channel, date }) { + const [region, site_id] = channel.site_id.split('#') + + const baseUrl = + region === 'pl' + ? 'https://www.canalplus.com/pl/program-tv/' + : `https://www.canalplus.com/${region}/programme-tv/` + + const data = await axios + .get(baseUrl) + .then(r => r.data.toString()) + .catch(err => console.log(err)) + + const token = parseToken(data) + const path = region === 'pl' ? 'mycanalint' : 'mycanal' + const diff = date.diff(dayjs.utc().startOf('d'), 'd') + + return `https://hodor.canalplus.pro/api/v2/${path}/channels/${token}/${site_id}/broadcasts/day/${diff}` + }, + async parser({ content }) { + let programs = [] + const items = parseItems(content) + for (let item of items) { + const prev = programs[programs.length - 1] + const details = await loadProgramDetails(item) + const info = parseInfo(details) + const start = parseStart(item) + if (prev) prev.stop = start + const stop = start.add(1, 'h') + programs.push({ + title: item.title, + description: parseDescription(info), + image: parseImage(info), + actors: parseCast(info, 'Avec :'), + director: parseCast(info, 'De :'), + writer: parseCast(info, 'Scénario :'), + composer: parseCast(info, 'Musique :'), + presenter: parseCast(info, 'Présenté par :'), + date: parseDate(info), + rating: parseRating(info), + start, + stop + }) + } + + return programs + }, + async channels({ country }) { + const paths = { + ad: 'cpafr/ad', + bf: 'cpafr/bf', + bi: 'cpafr/bi', + bj: 'cpafr/bj', + bl: 'cpant/bl', + cd: 'cpafr/cd', + cf: 'cpafr/cf', + cg: 'cpafr/cg', + ch: 'cpche', + ci: 'cpafr/ci', + cm: 'cpafr/cm', + cv: 'cpafr/cv', + dj: 'cpafr/dj', + fr: 'cpfra', + ga: 'cpafr/ga', + gf: 'cpant/gf', + gh: 'cpafr/gh', + gm: 'cpafr/gm', + gn: 'cpafr/gn', + gp: 'cpafr/gp', + gw: 'cpafr/gw', + ht: 'cpant/ht', + mf: 'cpant/mf', + mg: 'cpafr/mg', + ml: 'cpafr/ml', + mq: 'cpant/mq', + mr: 'cpafr/mr', + mu: 'cpmus/mu', + nc: 'cpncl/nc', + ne: 'cpafr/ne', + pf: 'cppyf/pf', + pl: 'cppol', + re: 'cpreu/re', + rw: 'cpafr/rw', + sl: 'cpafr/sl', + sn: 'cpafr/sn', + td: 'cpafr/td', + tg: 'cpafr/tg', + wf: 'cpncl/wf', + yt: 'cpreu/yt' + } + + let channels = [] + const path = paths[country] + const url = `https://secure-webtv-static.canal-plus.com/metadata/${path}/all/v2.2/globalchannels.json` + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + + data.channels.forEach(channel => { + const site_id = country === 'fr' ? `#${channel.id}` : `${country}#${channel.id}` + + if (channel.name === '.') return + + channels.push({ + lang: 'fr', + site_id, + name: channel.name + }) + }) + + return channels + } +} + +function parseToken(data) { + const [, token] = data.match(/"token":"([^"]+)/) || [null, null] + + return token +} + +function parseStart(item) { + return item && item.startTime ? dayjs(item.startTime) : null +} + +function parseImage(info) { + return info ? info.URLImage : null +} + +function parseDescription(info) { + return info ? info.summary : null +} + +function parseInfo(data) { + if (!data || !data.detail || !data.detail.informations) return null + + return data.detail.informations +} + +async function loadProgramDetails(item) { + if (!item.onClick || !item.onClick.URLPage) return {} + + return await axios + .get(item.onClick.URLPage) + .then(r => r.data) + .catch(console.error) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.timeSlices)) return [] + + return data.timeSlices.reduce((acc, curr) => { + acc = acc.concat(curr.contents) + return acc + }, []) +} + +function parseCast(info, type) { + let people = [] + if (info && info.personnalities) { + const personnalities = info.personnalities.find(i => i.prefix == type) + if (!personnalities) return people + for (let person of personnalities.personnalitiesList) { + people.push(person.title) + } + } + return people +} + +function parseDate(info) { + return info && info.productionYear ? info.productionYear : null +} + +function parseRating(info) { + if (!info || !info.parentalRatings) return null + let rating = info.parentalRatings.find(i => i.authority === 'CSA') + if (!rating || Array.isArray(rating)) return null + if (rating.value === '1') return null + if (rating.value === '2') rating.value = '-10' + if (rating.value === '3') rating.value = '-12' + if (rating.value === '4') rating.value = '-16' + if (rating.value === '5') rating.value = '-18' + return { + system: rating.authority, + value: rating.value + } +} diff --git a/sites/canalplus.com/canalplus.com.test.js b/sites/canalplus.com/canalplus.com.test.js index 43c2fb0b..c6aec122 100644 --- a/sites/canalplus.com/canalplus.com.test.js +++ b/sites/canalplus.com/canalplus.com.test.js @@ -1,146 +1,146 @@ -const { parser, url } = require('./canalplus.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 channel = { - site_id: 'bi#198', - xmltv_id: 'CanalPlusCinemaFrance.fr' -} - -it('can generate valid url for today', done => { - axios.get.mockImplementation(url => { - if (url === 'https://www.canalplus.com/bi/programme-tv/') { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/programme-tv.html')) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - const today = dayjs.utc().startOf('d') - url({ channel, date: today }) - .then(result => { - expect(result).toBe( - 'https://hodor.canalplus.pro/api/v2/mycanal/channels/f000c6f4ebf44647682b3a0fa66d7d99/198/broadcasts/day/0' - ) - done() - }) - .catch(done) -}) - -it('can generate valid url for tomorrow', done => { - axios.get.mockImplementation(url => { - if (url === 'https://www.canalplus.com/bi/programme-tv/') { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/programme-tv.html')) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - const tomorrow = dayjs.utc().startOf('d').add(1, 'd') - url({ channel, date: tomorrow }) - .then(result => { - expect(result).toBe( - 'https://hodor.canalplus.pro/api/v2/mycanal/channels/f000c6f4ebf44647682b3a0fa66d7d99/198/broadcasts/day/1' - ) - done() - }) - .catch(done) -}) - -it('can parse response', done => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - - axios.get.mockImplementation(url => { - if ( - url === - 'https://hodor.canalplus.pro/api/v2/mycanal/detail/f000c6f4ebf44647682b3a0fa66d7d99/okapi/6564630_50001.json?detailType=detailSeason&objectType=season&broadcastID=PLM_1196447642&episodeId=20482220_50001&brandID=4501558_50001&fromDiff=true' - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) - }) - } else if ( - url === - 'https://hodor.canalplus.pro/api/v2/mycanal/detail/f000c6f4ebf44647682b3a0fa66d7d99/okapi/17230453_50001.json?detailType=detailPage&objectType=unit&broadcastID=PLM_1196447637&fromDiff=true' - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - parser({ content }) - .then(result => { - result.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2023-01-12T06:28:00.000Z', - stop: '2023-01-12T12:06:00.000Z', - title: 'Le cercle', - description: - "Tant qu'il y aura du cinéma, LE CERCLE sera là. C'est la seule émission télévisée de débats critiques 100% consacrée au cinéma et elle rentre dans sa 18e saison. Chaque semaine, elle offre des joutes enflammées, joyeuses et sans condescendance, sur les films à l'affiche ; et invite avec \"Le questionnaire du CERCLE\" les réalisatrices et réalisateurs à venir partager leur passion cinéphile.", - image: - 'https://thumb.canalplus.pro/http/unsafe/{resolutionXY}/filters:quality({imageQualityPercentage})/img-hapi.canalplus.pro:80/ServiceImage/ImageID/107297573', - presenter: ['Lily Bloom'], - rating: { - system: 'CSA', - value: '-10' - } - }, - { - start: '2023-01-12T12:06:00.000Z', - stop: '2023-01-12T13:06:00.000Z', - title: 'Illusions perdues', - description: - "Pendant la Restauration, Lucien de Rubempré, jeune provincial d'Angoulême, se rêve poète. Il débarque à Paris en quête de gloire. Il a le soutien de Louise de Bargeton, une aristocrate qui croit en son talent. Pour gagner sa vie, Lucien trouve un emploi dans le journal dirigé par le peu scrupuleux Etienne Lousteau...", - image: - 'https://thumb.canalplus.pro/http/unsafe/{resolutionXY}/filters:quality({imageQualityPercentage})/img-hapi.canalplus.pro:80/ServiceImage/ImageID/107356485', - director: ['Xavier Giannoli'], - actors: [ - 'Benjamin Voisin', - 'Cécile de France', - 'Vincent Lacoste', - 'Xavier Dolan', - 'Gérard Depardieu', - 'Salomé Dewaels', - 'Jeanne Balibar', - 'Louis-Do de Lencquesaing', - 'Alexis Barbosa', - 'Jean-François Stévenin', - 'André Marcon', - 'Marie Cornillon' - ], - writer: ['Xavier Giannoli'], - rating: { - system: 'CSA', - value: '-10' - } - } - ]) - done() - }) - .catch(done) -}) - -it('can handle empty guide', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const result = await parser({ content }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./canalplus.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 channel = { + site_id: 'bi#198', + xmltv_id: 'CanalPlusCinemaFrance.fr' +} + +it('can generate valid url for today', done => { + axios.get.mockImplementation(url => { + if (url === 'https://www.canalplus.com/bi/programme-tv/') { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/programme-tv.html')) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + const today = dayjs.utc().startOf('d') + url({ channel, date: today }) + .then(result => { + expect(result).toBe( + 'https://hodor.canalplus.pro/api/v2/mycanal/channels/f000c6f4ebf44647682b3a0fa66d7d99/198/broadcasts/day/0' + ) + done() + }) + .catch(done) +}) + +it('can generate valid url for tomorrow', done => { + axios.get.mockImplementation(url => { + if (url === 'https://www.canalplus.com/bi/programme-tv/') { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/programme-tv.html')) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + const tomorrow = dayjs.utc().startOf('d').add(1, 'd') + url({ channel, date: tomorrow }) + .then(result => { + expect(result).toBe( + 'https://hodor.canalplus.pro/api/v2/mycanal/channels/f000c6f4ebf44647682b3a0fa66d7d99/198/broadcasts/day/1' + ) + done() + }) + .catch(done) +}) + +it('can parse response', done => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + + axios.get.mockImplementation(url => { + if ( + url === + 'https://hodor.canalplus.pro/api/v2/mycanal/detail/f000c6f4ebf44647682b3a0fa66d7d99/okapi/6564630_50001.json?detailType=detailSeason&objectType=season&broadcastID=PLM_1196447642&episodeId=20482220_50001&brandID=4501558_50001&fromDiff=true' + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) + }) + } else if ( + url === + 'https://hodor.canalplus.pro/api/v2/mycanal/detail/f000c6f4ebf44647682b3a0fa66d7d99/okapi/17230453_50001.json?detailType=detailPage&objectType=unit&broadcastID=PLM_1196447637&fromDiff=true' + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + parser({ content }) + .then(result => { + result.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2023-01-12T06:28:00.000Z', + stop: '2023-01-12T12:06:00.000Z', + title: 'Le cercle', + description: + "Tant qu'il y aura du cinéma, LE CERCLE sera là. C'est la seule émission télévisée de débats critiques 100% consacrée au cinéma et elle rentre dans sa 18e saison. Chaque semaine, elle offre des joutes enflammées, joyeuses et sans condescendance, sur les films à l'affiche ; et invite avec \"Le questionnaire du CERCLE\" les réalisatrices et réalisateurs à venir partager leur passion cinéphile.", + image: + 'https://thumb.canalplus.pro/http/unsafe/{resolutionXY}/filters:quality({imageQualityPercentage})/img-hapi.canalplus.pro:80/ServiceImage/ImageID/107297573', + presenter: ['Lily Bloom'], + rating: { + system: 'CSA', + value: '-10' + } + }, + { + start: '2023-01-12T12:06:00.000Z', + stop: '2023-01-12T13:06:00.000Z', + title: 'Illusions perdues', + description: + "Pendant la Restauration, Lucien de Rubempré, jeune provincial d'Angoulême, se rêve poète. Il débarque à Paris en quête de gloire. Il a le soutien de Louise de Bargeton, une aristocrate qui croit en son talent. Pour gagner sa vie, Lucien trouve un emploi dans le journal dirigé par le peu scrupuleux Etienne Lousteau...", + image: + 'https://thumb.canalplus.pro/http/unsafe/{resolutionXY}/filters:quality({imageQualityPercentage})/img-hapi.canalplus.pro:80/ServiceImage/ImageID/107356485', + director: ['Xavier Giannoli'], + actors: [ + 'Benjamin Voisin', + 'Cécile de France', + 'Vincent Lacoste', + 'Xavier Dolan', + 'Gérard Depardieu', + 'Salomé Dewaels', + 'Jeanne Balibar', + 'Louis-Do de Lencquesaing', + 'Alexis Barbosa', + 'Jean-François Stévenin', + 'André Marcon', + 'Marie Cornillon' + ], + writer: ['Xavier Giannoli'], + rating: { + system: 'CSA', + value: '-10' + } + } + ]) + done() + }) + .catch(done) +}) + +it('can handle empty guide', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const result = await parser({ content }) + expect(result).toMatchObject([]) +}) diff --git a/sites/cgates.lt/cgates.lt.config.js b/sites/cgates.lt/cgates.lt.config.js index ad83ca2b..6696c4a8 100644 --- a/sites/cgates.lt/cgates.lt.config.js +++ b/sites/cgates.lt/cgates.lt.config.js @@ -1,92 +1,92 @@ -const dayjs = require('dayjs') -const axios = require('axios') -const cheerio = require('cheerio') -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: 'cgates.lt', - days: 2, - url: function ({ channel }) { - return `https://www.cgates.lt/tv-kanalai/${channel.site_id}/` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - let html = await axios - .get('https://www.cgates.lt/televizija/tv-programa-savaitei/') - .then(r => r.data) - .catch(console.log) - let $ = cheerio.load(html) - const items = $('.vc_tta-panel.vc_active .kanalas_wrap').toArray() - - return items.map(item => { - const name = $(item).find('h6').text().trim() - const link = $(item).find('a').attr('href') - const [, site_id] = link.match(/\/tv-kanalai\/(.*)\//) || [null, null] - - return { - lang: 'lt', - site_id, - name - } - }) - } -} - -function parseTitle($item) { - const title = $item('td:nth-child(2) > .vc_toggle > .vc_toggle_title').text().trim() - - return title || $item('td:nth-child(2)').text().trim() -} - -function parseDescription($item) { - return $item('.vc_toggle_content > p').text().trim() -} - -function parseStart($item, date) { - const time = $item('.laikas') - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Vilnius') -} - -function parseItems(content, date) { - const $ = cheerio.load(content) - const section = $( - 'article > div:nth-child(2) > div.vc_row.wpb_row.vc_row-fluid > div > div > div > div > div' - ) - .filter(function () { - return $(`.dt-fancy-title:contains("${date.format('YYYY-MM-DD')}")`, this).length === 1 - }) - .first() - - return $('.tv_programa tr', section).toArray() -} +const dayjs = require('dayjs') +const axios = require('axios') +const cheerio = require('cheerio') +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: 'cgates.lt', + days: 2, + url: function ({ channel }) { + return `https://www.cgates.lt/tv-kanalai/${channel.site_id}/` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + let html = await axios + .get('https://www.cgates.lt/televizija/tv-programa-savaitei/') + .then(r => r.data) + .catch(console.log) + let $ = cheerio.load(html) + const items = $('.vc_tta-panel.vc_active .kanalas_wrap').toArray() + + return items.map(item => { + const name = $(item).find('h6').text().trim() + const link = $(item).find('a').attr('href') + const [, site_id] = link.match(/\/tv-kanalai\/(.*)\//) || [null, null] + + return { + lang: 'lt', + site_id, + name + } + }) + } +} + +function parseTitle($item) { + const title = $item('td:nth-child(2) > .vc_toggle > .vc_toggle_title').text().trim() + + return title || $item('td:nth-child(2)').text().trim() +} + +function parseDescription($item) { + return $item('.vc_toggle_content > p').text().trim() +} + +function parseStart($item, date) { + const time = $item('.laikas') + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Vilnius') +} + +function parseItems(content, date) { + const $ = cheerio.load(content) + const section = $( + 'article > div:nth-child(2) > div.vc_row.wpb_row.vc_row-fluid > div > div > div > div > div' + ) + .filter(function () { + return $(`.dt-fancy-title:contains("${date.format('YYYY-MM-DD')}")`, this).length === 1 + }) + .first() + + return $('.tv_programa tr', section).toArray() +} diff --git a/sites/cgates.lt/cgates.lt.test.js b/sites/cgates.lt/cgates.lt.test.js index 9a0c144c..3ebd2118 100644 --- a/sites/cgates.lt/cgates.lt.test.js +++ b/sites/cgates.lt/cgates.lt.test.js @@ -1,49 +1,49 @@ -const { parser, url } = require('./cgates.lt.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('2022-08-30', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'lrt-televizija-hd', - xmltv_id: 'LRTTV.lt' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://www.cgates.lt/tv-kanalai/lrt-televizija-hd/') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(35) - expect(results[0]).toMatchObject({ - start: '2022-08-29T21:05:00.000Z', - stop: '2022-08-29T21:30:00.000Z', - title: '31-oji nuovada (District 31), Drama, 2016', - description: - 'Seriale pasakojama apie kasdienius policijos išbandymus ir sunkumus. Vadovybė pertvarko Monrealio miesto policijos struktūrą: išskirsto į 36 policijos nuovadas, kad šios būtų arčiau gyventojų. 31-osios nuovados darbuotojams tenka kone sunkiausias darbas: šiame miesto rajone gyvena socialiai remtinos šeimos, nuolat kovojančios su turtingųjų klase, įsipliekia ir rasinių konfliktų. Be to, čia akivaizdus kartų atotrūkis, o tapti nusikalstamo pasaulio dalimi labai lengva. Serialo siužetas – intensyvus, nauji nusikaltimai tiriami kiekvieną savaitę. Čia vaizduojamas nepagražintas nusikalstamas pasaulis, jo poveikis rajono gyventojams. Policijos nuovados darbuotojai narplios įvairiausių nusikaltimų schemas. Tai ir pagrobimai, įsilaužimai, žmogžudystės, smurtas artimoje aplinkoje, lytiniai nusikaltimai, prekyba narkotikais, teroristinių išpuolių grėsmė ir pan. Šis serialas leis žiūrovui įsigilinti į policijos pareigūnų realybę, pateiks skirtingą požiūrį į kiekvieną nusikaltimą.' - }) - - expect(results[34]).toMatchObject({ - start: '2022-08-30T20:45:00.000Z', - stop: '2022-08-30T21:15:00.000Z', - title: '31-oji nuovada (District 31), Drama, 2016!' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./cgates.lt.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('2022-08-30', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'lrt-televizija-hd', + xmltv_id: 'LRTTV.lt' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://www.cgates.lt/tv-kanalai/lrt-televizija-hd/') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(35) + expect(results[0]).toMatchObject({ + start: '2022-08-29T21:05:00.000Z', + stop: '2022-08-29T21:30:00.000Z', + title: '31-oji nuovada (District 31), Drama, 2016', + description: + 'Seriale pasakojama apie kasdienius policijos išbandymus ir sunkumus. Vadovybė pertvarko Monrealio miesto policijos struktūrą: išskirsto į 36 policijos nuovadas, kad šios būtų arčiau gyventojų. 31-osios nuovados darbuotojams tenka kone sunkiausias darbas: šiame miesto rajone gyvena socialiai remtinos šeimos, nuolat kovojančios su turtingųjų klase, įsipliekia ir rasinių konfliktų. Be to, čia akivaizdus kartų atotrūkis, o tapti nusikalstamo pasaulio dalimi labai lengva. Serialo siužetas – intensyvus, nauji nusikaltimai tiriami kiekvieną savaitę. Čia vaizduojamas nepagražintas nusikalstamas pasaulis, jo poveikis rajono gyventojams. Policijos nuovados darbuotojai narplios įvairiausių nusikaltimų schemas. Tai ir pagrobimai, įsilaužimai, žmogžudystės, smurtas artimoje aplinkoje, lytiniai nusikaltimai, prekyba narkotikais, teroristinių išpuolių grėsmė ir pan. Šis serialas leis žiūrovui įsigilinti į policijos pareigūnų realybę, pateiks skirtingą požiūrį į kiekvieną nusikaltimą.' + }) + + expect(results[34]).toMatchObject({ + start: '2022-08-30T20:45:00.000Z', + stop: '2022-08-30T21:15:00.000Z', + title: '31-oji nuovada (District 31), Drama, 2016!' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/chada.ma/__data__/content.html b/sites/chada.ma/__data__/content.html new file mode 100644 index 00000000..66ecac9b --- /dev/null +++ b/sites/chada.ma/__data__/content.html @@ -0,0 +1,14 @@ +
    +

    Programmes d'Aujourd'hui

    +
    +

    00:00 - 09:00

    +
    + + + +
    +

    Bloc Prime + Clips

    +
    +
    +
    +
    \ No newline at end of file diff --git a/sites/chada.ma/__data__/no_content.html b/sites/chada.ma/__data__/no_content.html new file mode 100644 index 00000000..ba01a46c --- /dev/null +++ b/sites/chada.ma/__data__/no_content.html @@ -0,0 +1 @@ +
    \ No newline at end of file diff --git a/sites/chada.ma/chada.ma.config.js b/sites/chada.ma/chada.ma.config.js index b6002c35..0c05bd8c 100644 --- a/sites/chada.ma/chada.ma.config.js +++ b/sites/chada.ma/chada.ma.config.js @@ -1,55 +1,55 @@ -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: 'chada.ma', - channels: 'chada.ma.channels.xml', - days: 1, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url() { - return 'https://chada.ma/fr/chada-tv/grille-tv/' - }, - parser: function ({ content }) { - const $ = cheerio.load(content) - const programs = [] - - $('#stopfix .posts-area h2').each((i, element) => { - const timeRange = $(element).text().trim() - const [start, stop] = timeRange.split(' - ').map(t => parseProgramTime(t.trim())) - - const titleElement = $(element).next('div').next('h3') - const title = titleElement.text().trim() - - const description = titleElement.next('div').text().trim() || 'No description available' - - programs.push({ - title, - description, - start, - stop - }) - }) - - return programs - } -} - -function parseProgramTime(timeStr) { - const timeZone = 'Africa/Casablanca' - const currentDate = dayjs().format('YYYY-MM-DD') - - return dayjs - .tz(`${currentDate} ${timeStr}`, 'YYYY-MM-DD HH:mm', timeZone) - .format('YYYY-MM-DDTHH:mm:ssZ') -} +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: 'chada.ma', + channels: 'chada.ma.channels.xml', + days: 1, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url() { + return 'https://chada.ma/fr/chada-tv/grille-tv/' + }, + parser: function ({ content }) { + const $ = cheerio.load(content) + const programs = [] + + $('#stopfix .posts-area h2').each((i, element) => { + const timeRange = $(element).text().trim() + const [start, stop] = timeRange.split(' - ').map(t => parseProgramTime(t.trim())) + + const titleElement = $(element).next('div').next('h3') + const title = titleElement.text().trim() + + const description = titleElement.next('div').text().trim() || 'No description available' + + programs.push({ + title, + description, + start, + stop + }) + }) + + return programs + } +} + +function parseProgramTime(timeStr) { + const timeZone = 'Africa/Casablanca' + const currentDate = dayjs().format('YYYY-MM-DD') + + return dayjs + .tz(`${currentDate} ${timeStr}`, 'YYYY-MM-DD HH:mm', timeZone) + .format('YYYY-MM-DDTHH:mm:ssZ') +} diff --git a/sites/chada.ma/chada.ma.test.js b/sites/chada.ma/chada.ma.test.js index 51f715d4..9d079113 100644 --- a/sites/chada.ma/chada.ma.test.js +++ b/sites/chada.ma/chada.ma.test.js @@ -1,58 +1,42 @@ -const { parser, url } = require('./chada.ma.config.js') -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) - -jest.mock('axios') - -const mockHtmlContent = ` -
    -

    Programmes d'Aujourd'hui

    -
    -

    00:00 - 09:00

    -
    - - - -
    -

    Bloc Prime + Clips

    -
    -
    -
    -
    -` - -it('can generate valid url', () => { - expect(url()).toBe('https://chada.ma/fr/chada-tv/grille-tv/') -}) - -it('can parse response', () => { - const content = mockHtmlContent - - const result = parser({ content }).map(p => { - p.start = dayjs(p.start).tz('Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ') - p.stop = dayjs(p.stop).tz('Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ') - return p - }) - - expect(result).toMatchObject([ - { - title: 'Bloc Prime + Clips', - description: 'No description available', - start: dayjs.tz('00:00', 'HH:mm', 'Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ'), - stop: dayjs.tz('09:00', 'HH:mm', 'Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ') - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '
    ' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./chada.ma.config.js') +const fs = require('fs') +const path = require('path') +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) + +jest.mock('axios') + +it('can generate valid url', () => { + expect(url()).toBe('https://chada.ma/fr/chada-tv/grille-tv/') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + const result = parser({ content }).map(p => { + p.start = dayjs(p.start).tz('Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ') + p.stop = dayjs(p.stop).tz('Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ') + return p + }) + + expect(result).toMatchObject([ + { + title: 'Bloc Prime + Clips', + description: 'No description available', + start: dayjs.tz('00:00', 'HH:mm', 'Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ'), + stop: dayjs.tz('09:00', 'HH:mm', 'Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ') + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/chaines-tv.orange.fr/__data__/content.json b/sites/chaines-tv.orange.fr/__data__/content.json new file mode 100644 index 00000000..bee0f79e --- /dev/null +++ b/sites/chaines-tv.orange.fr/__data__/content.json @@ -0,0 +1 @@ +{"192":[{"id":1635062528017,"programType":"EPISODE","title":"Tête de liste","channelId":"192","channelZappingNumber":11,"covers":[{"format":"RATIO_16_9","url":"https://proxymedia.woopic.com/340/p/169_EMI_9697669.jpg"},{"format":"RATIO_4_3","url":"https://proxymedia.woopic.com/340/p/43_EMI_9697669.jpg"}],"diffusionDate":1636328100,"duration":2700,"csa":2,"synopsis":"Un tueur en série prend un plaisir pervers à prévenir les autorités de Tallahassee avant chaque nouveau meurtre. Rossi apprend le décès d'un de ses vieux amis.","languageVersion":"VM","hearingImpaired":true,"audioDescription":false,"season":{"number":10,"episodesCount":23,"serie":{"title":"Esprits criminels"}},"episodeNumber":12,"definition":"SD","links":[{"rel":"SELF","href":"https://rp-live.orange.fr/live-webapp/v3/applications/STB4PC/programs/1635062528017"}],"dayPart":"OTHER","catchupId":null,"genre":"Série","genreDetailed":"Série Suspense"}]} \ No newline at end of file diff --git a/sites/chaines-tv.orange.fr/__data__/no_content.json b/sites/chaines-tv.orange.fr/__data__/no_content.json new file mode 100644 index 00000000..025f8b92 --- /dev/null +++ b/sites/chaines-tv.orange.fr/__data__/no_content.json @@ -0,0 +1 @@ +{"code":60,"message":"Resource not found","param":{},"description":"L'URI demandé ou la ressource demandée n'existe pas.","stackTrace":null} \ No newline at end of file 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/chaines-tv.orange.fr/chaines-tv.orange.fr.test.js b/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.test.js index 9a07e69f..3a832c35 100644 --- a/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.test.js +++ b/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.test.js @@ -1,49 +1,49 @@ -const { parser, url } = require('./chaines-tv.orange.fr.config.js') -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('2021-11-08', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '192', - xmltv_id: 'TF1.fr' -} -const content = - '{"192":[{"id":1635062528017,"programType":"EPISODE","title":"Tête de liste","channelId":"192","channelZappingNumber":11,"covers":[{"format":"RATIO_16_9","url":"https://proxymedia.woopic.com/340/p/169_EMI_9697669.jpg"},{"format":"RATIO_4_3","url":"https://proxymedia.woopic.com/340/p/43_EMI_9697669.jpg"}],"diffusionDate":1636328100,"duration":2700,"csa":2,"synopsis":"Un tueur en série prend un plaisir pervers à prévenir les autorités de Tallahassee avant chaque nouveau meurtre. Rossi apprend le décès d\'un de ses vieux amis.","languageVersion":"VM","hearingImpaired":true,"audioDescription":false,"season":{"number":10,"episodesCount":23,"serie":{"title":"Esprits criminels"}},"episodeNumber":12,"definition":"SD","links":[{"rel":"SELF","href":"https://rp-live.orange.fr/live-webapp/v3/applications/STB4PC/programs/1635062528017"}],"dayPart":"OTHER","catchupId":null,"genre":"Série","genreDetailed":"Série Suspense"}]}' - -it('can generate valid url', () => { - const result = url({ channel, date }) - expect(result).toBe( - 'https://rp-ott-mediation-tv.woopic.com/api-gw/live/v3/applications/STB4PC/programs?groupBy=channel&includeEmptyChannels=false&period=1636329600000,1636416000000&after=192&limit=1' - ) -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: '2021-11-07T23:35:00.000Z', - stop: '2021-11-08T00:20:00.000Z', - title: 'Tête de liste', - subTitle: 'Esprits criminels', - season: 10, - episode: 12, - description: - "Un tueur en série prend un plaisir pervers à prévenir les autorités de Tallahassee avant chaque nouveau meurtre. Rossi apprend le décès d'un de ses vieux amis.", - category: 'Série Suspense', - image: 'https://proxymedia.woopic.com/340/p/169_EMI_9697669.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '{"code":60,"message":"Resource not found","param":{},"description":"L\'URI demandé ou la ressource demandée n\'existe pas.","stackTrace":null}' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./chaines-tv.orange.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('2021-11-08', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '192', + xmltv_id: 'TF1.fr' +} + +it('can generate valid url', () => { + const result = url({ channel, date }) + expect(result).toBe( + 'https://rp-ott-mediation-tv.woopic.com/api-gw/live/v3/applications/STB4PC/programs?groupBy=channel&includeEmptyChannels=false&period=1636329600000,1636416000000&after=192&limit=1' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: '2021-11-07T23:35:00.000Z', + stop: '2021-11-08T00:20:00.000Z', + title: 'Tête de liste', + subTitle: 'Esprits criminels', + season: 10, + episode: 12, + description: + "Un tueur en série prend un plaisir pervers à prévenir les autorités de Tallahassee avant chaque nouveau meurtre. Rossi apprend le décès d'un de ses vieux amis.", + category: 'Série Suspense', + image: 'https://proxymedia.woopic.com/340/p/169_EMI_9697669.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/clickthecity.com/clickthecity.com.config.js b/sites/clickthecity.com/clickthecity.com.config.js index 9882af55..e726c872 100644 --- a/sites/clickthecity.com/clickthecity.com.config.js +++ b/sites/clickthecity.com/clickthecity.com.config.js @@ -1,100 +1,100 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const { DateTime } = require('luxon') - -module.exports = { - site: 'clickthecity.com', - days: 2, - url({ channel }) { - return `https://www.clickthecity.com/tv/channels/?netid=${channel.site_id}` - }, - request: { - method: 'POST', - headers: { - 'content-type': 'application/x-www-form-urlencoded' - }, - data({ date }) { - const params = new URLSearchParams() - params.append( - 'optDate', - DateTime.fromMillis(date.valueOf()).setZone('Asia/Manila').toFormat('yyyy-MM-dd') - ) - params.append('optTime', '00:00:00') - - return params - } - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - let start = parseStart($item, date) - let stop = parseStop($item, date) - if (!start || !stop) return - if (start > stop) { - stop = stop.plus({ days: 1 }) - } - - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const html = await axios - .get('https://www.clickthecity.com/tv/channels/') - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(html) - const items = $('#channels .col').toArray() - - return items.map(item => { - const name = $(item).find('.card-body').text().trim() - const url = $(item).find('a').attr('href') - const [, site_id] = url.match(/netid=(\d+)/) || [null, null] - - return { - lang: 'en', - site_id, - name - } - }) - } -} - -function parseTitle($item) { - return $item('td > a').text().trim() -} - -function parseStart($item, date) { - const url = $item('td.cPrg > a').attr('href') || '' - let [, time] = url.match(/starttime=(\d{1,2}%3A\d{2}\+(AM|PM))/) || [null, null] - if (!time) return null - time = `${date.format('YYYY-MM-DD')} ${time.replace('%3A', ':').replace('+', ' ')}` - - return DateTime.fromFormat(time, 'yyyy-MM-dd h:mm a', { zone: 'Asia/Manila' }).toUTC() -} - -function parseStop($item, date) { - const url = $item('td.cPrg > a').attr('href') || '' - let [, time] = url.match(/endtime=(\d{1,2}%3A\d{2}\+(AM|PM))/) || [null, null] - if (!time) return null - time = `${date.format('YYYY-MM-DD')} ${time.replace('%3A', ':').replace('+', ' ')}` - - return DateTime.fromFormat(time, 'yyyy-MM-dd h:mm a', { zone: 'Asia/Manila' }).toUTC() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#tvlistings > tbody > tr') - .filter(function () { - return $(this).find('td.cPrg').length - }) - .toArray() -} +const cheerio = require('cheerio') +const axios = require('axios') +const { DateTime } = require('luxon') + +module.exports = { + site: 'clickthecity.com', + days: 2, + url({ channel }) { + return `https://www.clickthecity.com/tv/channels/?netid=${channel.site_id}` + }, + request: { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded' + }, + data({ date }) { + const params = new URLSearchParams() + params.append( + 'optDate', + DateTime.fromMillis(date.valueOf()).setZone('Asia/Manila').toFormat('yyyy-MM-dd') + ) + params.append('optTime', '00:00:00') + + return params + } + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + let start = parseStart($item, date) + let stop = parseStop($item, date) + if (!start || !stop) return + if (start > stop) { + stop = stop.plus({ days: 1 }) + } + + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const html = await axios + .get('https://www.clickthecity.com/tv/channels/') + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(html) + const items = $('#channels .col').toArray() + + return items.map(item => { + const name = $(item).find('.card-body').text().trim() + const url = $(item).find('a').attr('href') + const [, site_id] = url.match(/netid=(\d+)/) || [null, null] + + return { + lang: 'en', + site_id, + name + } + }) + } +} + +function parseTitle($item) { + return $item('td > a').text().trim() +} + +function parseStart($item, date) { + const url = $item('td.cPrg > a').attr('href') || '' + let [, time] = url.match(/starttime=(\d{1,2}%3A\d{2}\+(AM|PM))/) || [null, null] + if (!time) return null + time = `${date.format('YYYY-MM-DD')} ${time.replace('%3A', ':').replace('+', ' ')}` + + return DateTime.fromFormat(time, 'yyyy-MM-dd h:mm a', { zone: 'Asia/Manila' }).toUTC() +} + +function parseStop($item, date) { + const url = $item('td.cPrg > a').attr('href') || '' + let [, time] = url.match(/endtime=(\d{1,2}%3A\d{2}\+(AM|PM))/) || [null, null] + if (!time) return null + time = `${date.format('YYYY-MM-DD')} ${time.replace('%3A', ':').replace('+', ' ')}` + + return DateTime.fromFormat(time, 'yyyy-MM-dd h:mm a', { zone: 'Asia/Manila' }).toUTC() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('#tvlistings > tbody > tr') + .filter(function () { + return $(this).find('td.cPrg').length + }) + .toArray() +} diff --git a/sites/clickthecity.com/clickthecity.com.test.js b/sites/clickthecity.com/clickthecity.com.test.js index 5044c7f4..bfa0b5d3 100644 --- a/sites/clickthecity.com/clickthecity.com.test.js +++ b/sites/clickthecity.com/clickthecity.com.test.js @@ -1,67 +1,67 @@ -const { parser, url, request } = require('./clickthecity.com.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('2023-06-12', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '5', - xmltv_id: 'TV5.ph' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://www.clickthecity.com/tv/channels/?netid=5') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'content-type': 'application/x-www-form-urlencoded' - }) -}) - -it('can generate valid request data', () => { - const result = request.data({ date }) - expect(result.get('optDate')).toBe('2023-06-12') - expect(result.get('optTime')).toBe('00:00:00') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(20) - - expect(results[0]).toMatchObject({ - start: '2023-06-11T21:00:00.000Z', - stop: '2023-06-11T22:00:00.000Z', - title: 'Word Of God' - }) - - expect(results[19]).toMatchObject({ - start: '2023-06-12T15:30:00.000Z', - stop: '2023-06-12T16:00:00.000Z', - title: 'La Suerte De Loli' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./clickthecity.com.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('2023-06-12', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '5', + xmltv_id: 'TV5.ph' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://www.clickthecity.com/tv/channels/?netid=5') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'content-type': 'application/x-www-form-urlencoded' + }) +}) + +it('can generate valid request data', () => { + const result = request.data({ date }) + expect(result.get('optDate')).toBe('2023-06-12') + expect(result.get('optTime')).toBe('00:00:00') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(20) + + expect(results[0]).toMatchObject({ + start: '2023-06-11T21:00:00.000Z', + stop: '2023-06-11T22:00:00.000Z', + title: 'Word Of God' + }) + + expect(results[19]).toMatchObject({ + start: '2023-06-12T15:30:00.000Z', + stop: '2023-06-12T16:00:00.000Z', + title: 'La Suerte De Loli' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: + '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/content.astro.com.my/content.astro.com.my.config.js b/sites/content.astro.com.my/content.astro.com.my.config.js index 6f261a91..c49bfc69 100644 --- a/sites/content.astro.com.my/content.astro.com.my.config.js +++ b/sites/content.astro.com.my/content.astro.com.my.config.js @@ -1,137 +1,137 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -const API_ENDPOINT = 'https://contenthub-api.eco.astro.com.my' - -module.exports = { - site: 'content.astro.com.my', - days: 2, - url: function ({ channel }) { - return `${API_ENDPOINT}/channel/${channel.site_id}.json` - }, - async parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - for (let item of items) { - const start = dayjs.utc(item.datetimeInUtc) - const duration = parseDuration(item.duration) - const stop = start.add(duration, 's') - const details = await loadProgramDetails(item) - programs.push({ - title: details.title, - sub_title: item.subtitles, - description: details.longSynopsis || details.shortSynopsis, - actors: parseList(details.cast), - directors: parseList(details.director), - image: details.imageUrl, - rating: parseRating(details), - categories: parseCategories(details), - episode: parseEpisode(item), - season: parseSeason(details), - start: start, - stop: stop - }) - } - - return programs - }, - async channels() { - const data = await axios - .get('https://contenthub-api.eco.astro.com.my/channel/all.json') - .then(r => r.data) - .catch(console.log) - - return data.response.map(item => { - return { - lang: 'ms', - site_id: item.id, - name: item.title - } - }) - } -} - -function parseEpisode(item) { - const [, number] = item.title.match(/Ep(\d+)$/) || [null, null] - - return number ? parseInt(number) : null -} - -function parseSeason(details) { - const [, season] = details.title ? details.title.match(/ S(\d+)/) || [null, null] : [null, null] - - return season ? parseInt(season) : null -} - -function parseList(list) { - return typeof list === 'string' ? list.split(',') : [] -} - -function parseRating(details) { - return details.certification - ? { - system: 'LPF', - value: details.certification - } - : null -} - -function parseItems(content, date) { - try { - const data = JSON.parse(content) - const schedules = data.response.schedule - - return schedules[date.format('YYYY-MM-DD')] || [] - } catch { - return [] - } -} - -function parseDuration(duration) { - const match = duration.match(/(\d{2}):(\d{2}):(\d{2})/) - const hours = parseInt(match[1]) - const minutes = parseInt(match[2]) - const seconds = parseInt(match[3]) - - return hours * 3600 + minutes * 60 + seconds -} - -function parseCategories(details) { - const genres = { - 'filter/2': 'Action', - 'filter/4': 'Anime', - 'filter/12': 'Cartoons', - 'filter/16': 'Comedy', - 'filter/19': 'Crime', - 'filter/24': 'Drama', - 'filter/25': 'Educational', - 'filter/36': 'Horror', - 'filter/39': 'Live Action', - 'filter/55': 'Pre-school', - 'filter/56': 'Reality', - 'filter/60': 'Romance', - 'filter/68': 'Talk Show', - 'filter/69': 'Thriller', - 'filter/72': 'Variety', - 'filter/75': 'Series', - 'filter/100': 'Others (Children)' - } - - return Array.isArray(details.subFilter) - ? details.subFilter.map(g => genres[g.toLowerCase()]).filter(Boolean) - : [] -} - -async function loadProgramDetails(item) { - const url = `${API_ENDPOINT}/api/v1/linear-detail?siTrafficKey=${item.siTrafficKey}` - const data = await axios - .get(url) - .then(r => r.data) - .catch(error => console.log(error.message)) - if (!data) return {} - - return data.response || {} -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +const API_ENDPOINT = 'https://contenthub-api.eco.astro.com.my' + +module.exports = { + site: 'content.astro.com.my', + days: 2, + url: function ({ channel }) { + return `${API_ENDPOINT}/channel/${channel.site_id}.json` + }, + async parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + for (let item of items) { + const start = dayjs.utc(item.datetimeInUtc) + const duration = parseDuration(item.duration) + const stop = start.add(duration, 's') + const details = await loadProgramDetails(item) + programs.push({ + title: details.title, + sub_title: item.subtitles, + description: details.longSynopsis || details.shortSynopsis, + actors: parseList(details.cast), + directors: parseList(details.director), + image: details.imageUrl, + rating: parseRating(details), + categories: parseCategories(details), + episode: parseEpisode(item), + season: parseSeason(details), + start: start, + stop: stop + }) + } + + return programs + }, + async channels() { + const data = await axios + .get('https://contenthub-api.eco.astro.com.my/channel/all.json') + .then(r => r.data) + .catch(console.log) + + return data.response.map(item => { + return { + lang: 'ms', + site_id: item.id, + name: item.title + } + }) + } +} + +function parseEpisode(item) { + const [, number] = item.title.match(/Ep(\d+)$/) || [null, null] + + return number ? parseInt(number) : null +} + +function parseSeason(details) { + const [, season] = details.title ? details.title.match(/ S(\d+)/) || [null, null] : [null, null] + + return season ? parseInt(season) : null +} + +function parseList(list) { + return typeof list === 'string' ? list.split(',') : [] +} + +function parseRating(details) { + return details.certification + ? { + system: 'LPF', + value: details.certification + } + : null +} + +function parseItems(content, date) { + try { + const data = JSON.parse(content) + const schedules = data.response.schedule + + return schedules[date.format('YYYY-MM-DD')] || [] + } catch { + return [] + } +} + +function parseDuration(duration) { + const match = duration.match(/(\d{2}):(\d{2}):(\d{2})/) + const hours = parseInt(match[1]) + const minutes = parseInt(match[2]) + const seconds = parseInt(match[3]) + + return hours * 3600 + minutes * 60 + seconds +} + +function parseCategories(details) { + const genres = { + 'filter/2': 'Action', + 'filter/4': 'Anime', + 'filter/12': 'Cartoons', + 'filter/16': 'Comedy', + 'filter/19': 'Crime', + 'filter/24': 'Drama', + 'filter/25': 'Educational', + 'filter/36': 'Horror', + 'filter/39': 'Live Action', + 'filter/55': 'Pre-school', + 'filter/56': 'Reality', + 'filter/60': 'Romance', + 'filter/68': 'Talk Show', + 'filter/69': 'Thriller', + 'filter/72': 'Variety', + 'filter/75': 'Series', + 'filter/100': 'Others (Children)' + } + + return Array.isArray(details.subFilter) + ? details.subFilter.map(g => genres[g.toLowerCase()]).filter(Boolean) + : [] +} + +async function loadProgramDetails(item) { + const url = `${API_ENDPOINT}/api/v1/linear-detail?siTrafficKey=${item.siTrafficKey}` + const data = await axios + .get(url) + .then(r => r.data) + .catch(error => console.log(error.message)) + if (!data) return {} + + return data.response || {} +} diff --git a/sites/content.astro.com.my/content.astro.com.my.test.js b/sites/content.astro.com.my/content.astro.com.my.test.js index 8e9b4660..9a169ada 100644 --- a/sites/content.astro.com.my/content.astro.com.my.test.js +++ b/sites/content.astro.com.my/content.astro.com.my.test.js @@ -1,71 +1,71 @@ -const { parser, url } = require('./content.astro.com.my.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('2022-10-31', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '425', - xmltv_id: 'TVBClassic.hk' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://contenthub-api.eco.astro.com.my/channel/425.json') -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - - axios.get.mockImplementation(url => { - if ( - url === - 'https://contenthub-api.eco.astro.com.my/api/v1/linear-detail?siTrafficKey=1:10000526:47979653' - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(31) - expect(results[0]).toMatchObject({ - start: '2022-10-30T16:10:00.000Z', - stop: '2022-10-30T17:02:00.000Z', - title: 'Triumph in the Skies S1 Ep06', - description: - 'This classic drama depicts the many aspects of two complicated relationships set against an airline company. Will those involved ever find true love?', - actors: ['Francis Ng Chun Yu', 'Joe Ma Tak Chung', 'Flora Chan Wai San'], - directors: ['Joe Ma Tak Chung'], - image: 'https://s3-ap-southeast-1.amazonaws.com/ams-astro/production/images/1035X328883.jpg', - rating: { - system: 'LPF', - value: 'U' - }, - episode: 6, - season: 1, - categories: ['Drama'] - }) -}) - -it('can handle empty guide', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - const results = await parser({ date, content }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./content.astro.com.my.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('2022-10-31', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '425', + xmltv_id: 'TVBClassic.hk' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://contenthub-api.eco.astro.com.my/channel/425.json') +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + + axios.get.mockImplementation(url => { + if ( + url === + 'https://contenthub-api.eco.astro.com.my/api/v1/linear-detail?siTrafficKey=1:10000526:47979653' + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(31) + expect(results[0]).toMatchObject({ + start: '2022-10-30T16:10:00.000Z', + stop: '2022-10-30T17:02:00.000Z', + title: 'Triumph in the Skies S1 Ep06', + description: + 'This classic drama depicts the many aspects of two complicated relationships set against an airline company. Will those involved ever find true love?', + actors: ['Francis Ng Chun Yu', 'Joe Ma Tak Chung', 'Flora Chan Wai San'], + directors: ['Joe Ma Tak Chung'], + image: 'https://s3-ap-southeast-1.amazonaws.com/ams-astro/production/images/1035X328883.jpg', + rating: { + system: 'LPF', + value: 'U' + }, + episode: 6, + season: 1, + categories: ['Drama'] + }) +}) + +it('can handle empty guide', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + const results = await parser({ date, content }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/cosmotetv.gr/__data__/content.json b/sites/cosmotetv.gr/__data__/content.json new file mode 100644 index 00000000..39259004 --- /dev/null +++ b/sites/cosmotetv.gr/__data__/content.json @@ -0,0 +1,20 @@ +{ + "channels": [ + { + "items": [ + { + "startTime": "2024-12-26T23:00:00+00:00", + "endTime": "2024-12-27T00:00:00+00:00", + "title": "Τι Λέει ο Νόμος", + "description": "νημερωτική εκπομπή. Συζήτηση με τους εισηγητές των κομμάτων για το νομοθετικό έργο.", + "qoe": { + "genre": "Special" + }, + "thumbnails": { + "standard": "https://gr-ermou-prod-cache05.static.cdn.cosmotetvott.gr/ote-prod/70/280/040029714812000800_1734415727199.jpg" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/sites/cosmotetv.gr/__data__/no_content.json b/sites/cosmotetv.gr/__data__/no_content.json new file mode 100644 index 00000000..029ebb50 --- /dev/null +++ b/sites/cosmotetv.gr/__data__/no_content.json @@ -0,0 +1 @@ +{"date":"2024-12-26","categories":[],"channels":[]} \ No newline at end of file diff --git a/sites/cosmotetv.gr/cosmotetv.gr.config.js b/sites/cosmotetv.gr/cosmotetv.gr.config.js index 8374f050..3ec1fde6 100644 --- a/sites/cosmotetv.gr/cosmotetv.gr.config.js +++ b/sites/cosmotetv.gr/cosmotetv.gr.config.js @@ -1,85 +1,85 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) -dayjs.extend(timezone) - -module.exports = { - site: 'cosmotetv.gr', - days: 5, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - }, - method: 'GET', - headers: { - referer: 'https://www.cosmotetv.gr/', - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', - Accept: '*/*', - 'Accept-Language': 'en-US,en;q=0.9', - 'Accept-Encoding': 'gzip, deflate, br, zstd', - Origin: 'https://www.cosmotetv.gr', - 'Sec-Ch-Ua': '"Not.A/Brand";v="24", "Chromium";v="131", "Google Chrome";v="131"', - 'Sec-Ch-Ua-Mobile': '?0', - 'Sec-Ch-Ua-Platform': '"Windows"', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'cross-site' - } - }, - url: function ({ date, channel }) { - const startOfDay = dayjs(date).startOf('day').utc().unix() - const endOfDay = dayjs(date).endOf('day').utc().unix() - return `https://mwapi-prod.cosmotetvott.gr/api/v3.4/epg/listings/el?from=${startOfDay}&to=${endOfDay}&callSigns=${channel.site_id}&endingIncludedInRange=false` - }, - parser: function ({ content }) { - let programs = [] - const data = JSON.parse(content) - data.channels.forEach(channel => { - channel.items.forEach(item => { - const start = dayjs(item.startTime).utc().toISOString() - const stop = dayjs(item.endTime).utc().toISOString() - programs.push({ - title: item.title, - description: item.description || 'No description available', - category: item.qoe.genre, - image: item.thumbnails.standard, - start, - stop - }) - }) - }) - return programs - }, - async channels() { - const axios = require('axios') - try { - const response = await axios.get( - 'https://mwapi-prod.cosmotetvott.gr/api/v3.4/epg/channels/all/el', - { - headers: this.request.headers - } - ) - const data = response.data - - if (data && data.channels) { - return data.channels.map(item => ({ - lang: 'el', - site_id: item.callSign, - name: item.title - //logo: item.logos.square - })) - } else { - console.error('Unexpected response structure:', data) - return [] - } - } catch (error) { - console.error('Error fetching channel data:', error) - return [] - } - } -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) +dayjs.extend(timezone) + +module.exports = { + site: 'cosmotetv.gr', + days: 5, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + }, + method: 'GET', + headers: { + referer: 'https://www.cosmotetv.gr/', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + Accept: '*/*', + 'Accept-Language': 'en-US,en;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br, zstd', + Origin: 'https://www.cosmotetv.gr', + 'Sec-Ch-Ua': '"Not.A/Brand";v="24", "Chromium";v="131", "Google Chrome";v="131"', + 'Sec-Ch-Ua-Mobile': '?0', + 'Sec-Ch-Ua-Platform': '"Windows"', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'cross-site' + } + }, + url: function ({ date, channel }) { + const startOfDay = dayjs(date).startOf('day').utc().unix() + const endOfDay = dayjs(date).endOf('day').utc().unix() + return `https://mwapi-prod.cosmotetvott.gr/api/v3.4/epg/listings/el?from=${startOfDay}&to=${endOfDay}&callSigns=${channel.site_id}&endingIncludedInRange=false` + }, + parser: function ({ content }) { + let programs = [] + const data = JSON.parse(content) + data.channels.forEach(channel => { + channel.items.forEach(item => { + const start = dayjs(item.startTime).utc().toISOString() + const stop = dayjs(item.endTime).utc().toISOString() + programs.push({ + title: item.title, + description: item.description || 'No description available', + category: item.qoe.genre, + image: item.thumbnails.standard, + start, + stop + }) + }) + }) + return programs + }, + async channels() { + const axios = require('axios') + try { + const response = await axios.get( + 'https://mwapi-prod.cosmotetvott.gr/api/v3.4/epg/channels/all/el', + { + headers: this.request.headers + } + ) + const data = response.data + + if (data && data.channels) { + return data.channels.map(item => ({ + lang: 'el', + site_id: item.callSign, + name: item.title + //logo: item.logos.square + })) + } else { + console.error('Unexpected response structure:', data) + return [] + } + } catch (error) { + console.error('Error fetching channel data:', error) + return [] + } + } +} diff --git a/sites/cosmotetv.gr/cosmotetv.gr.test.js b/sites/cosmotetv.gr/cosmotetv.gr.test.js index 405e3d9b..405a932d 100644 --- a/sites/cosmotetv.gr/cosmotetv.gr.test.js +++ b/sites/cosmotetv.gr/cosmotetv.gr.test.js @@ -1,76 +1,55 @@ -const { parser, url } = require('./cosmotetv.gr.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) -dayjs.extend(timezone) - -jest.mock('axios') - -const date = dayjs.utc('2024-12-26', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'vouli', xmltv_id: 'HellenicParliamentTV.gr' } - -const mockEpgData = { - channels: [ - { - items: [ - { - startTime: '2024-12-26T23:00:00+00:00', - endTime: '2024-12-27T00:00:00+00:00', - title: 'Τι Λέει ο Νόμος', - description: - 'νημερωτική εκπομπή. Συζήτηση με τους εισηγητές των κομμάτων για το νομοθετικό έργο.', - qoe: { - genre: 'Special' - }, - thumbnails: { - standard: - 'https://gr-ermou-prod-cache05.static.cdn.cosmotetvott.gr/ote-prod/70/280/040029714812000800_1734415727199.jpg' - } - } - ] - } - ] -} - -it('can generate valid url', () => { - const startOfDay = dayjs(date).startOf('day').utc().unix() - const endOfDay = dayjs(date).endOf('day').utc().unix() - expect(url({ date, channel })).toBe( - `https://mwapi-prod.cosmotetvott.gr/api/v3.4/epg/listings/el?from=${startOfDay}&to=${endOfDay}&callSigns=${channel.site_id}&endingIncludedInRange=false` - ) -}) - -it('can parse response', () => { - const content = JSON.stringify(mockEpgData) - const result = parser({ date, content }).map(p => { - p.start = dayjs(p.start).toISOString() - p.stop = dayjs(p.stop).toISOString() - return p - }) - - expect(result).toMatchObject([ - { - title: 'Τι Λέει ο Νόμος', - description: - 'νημερωτική εκπομπή. Συζήτηση με τους εισηγητές των κομμάτων για το νομοθετικό έργο.', - category: 'Special', - image: - 'https://gr-ermou-prod-cache05.static.cdn.cosmotetvott.gr/ote-prod/70/280/040029714812000800_1734415727199.jpg', - start: '2024-12-26T23:00:00.000Z', - stop: '2024-12-27T00:00:00.000Z' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"date":"2024-12-26","categories":[],"channels":[]}' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./cosmotetv.gr.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') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) +dayjs.extend(timezone) + +jest.mock('axios') + +const date = dayjs.utc('2024-12-26', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'vouli', xmltv_id: 'HellenicParliamentTV.gr' } + +it('can generate valid url', () => { + const startOfDay = dayjs(date).startOf('day').utc().unix() + const endOfDay = dayjs(date).endOf('day').utc().unix() + expect(url({ date, channel })).toBe( + `https://mwapi-prod.cosmotetvott.gr/api/v3.4/epg/listings/el?from=${startOfDay}&to=${endOfDay}&callSigns=${channel.site_id}&endingIncludedInRange=false` + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') + const result = parser({ date, content }).map(p => { + p.start = dayjs(p.start).toISOString() + p.stop = dayjs(p.stop).toISOString() + return p + }) + + expect(result).toMatchObject([ + { + title: 'Τι Λέει ο Νόμος', + description: + 'νημερωτική εκπομπή. Συζήτηση με τους εισηγητές των κομμάτων για το νομοθετικό έργο.', + category: 'Special', + image: + 'https://gr-ermou-prod-cache05.static.cdn.cosmotetvott.gr/ote-prod/70/280/040029714812000800_1734415727199.jpg', + start: '2024-12-26T23:00:00.000Z', + stop: '2024-12-27T00:00:00.000Z' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8') + }) + expect(result).toMatchObject([]) +}) 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/cubmu.com/__data__/content.json b/sites/cubmu.com/__data__/content.json new file mode 100644 index 00000000..d27a2baa --- /dev/null +++ b/sites/cubmu.com/__data__/content.json @@ -0,0 +1 @@ +{"result":[{"channel_id":"4028c68574537fcd0174be43042758d8","channel_name":"Trans TV","scehedule_title":"CNN Tech News","schedule_date":"2023-11-05 01:30:00","schedule_end_time":"02:00:00","schedule_json":{"availability":0,"channelId":"4028c68574537fcd0174be43042758d8","channelName":"Trans TV","duration":1800,"editable":true,"episodeName":"","imageUrl":"https://cdnjkt2.transvision.co.id:1001/catchup/schedule/thumbnail/4028c68574537fcd0174be43042758d8/4028c6858b8b3621018b9330e3701a7e/458x640","imageUrlWide":"https://cdnjkt2.transvision.co.id:1001/catchup/schedule/thumbnail/4028c68574537fcd0174be43042758d8/4028c6858b8b3621018b9330e3701a7e/320x180","name":"CNN Tech News","ottImageUrl":"","primarySynopsis":"CNN Indonesia Tech News adalah berita teknologi yang membawa pemirsa ke dunia teknologi yang penuh dengan informasi, pendidikan, hiburan sampai informasi kesehatan terkini.","scheduleId":"4028c6858b8b3621018b9330e3701a7e","scheduleTime":"18:30:00","secondarySynopsis":"CNN Indonesia Tech News is tech news brings viewers into the world of technology that provides information, education, entertainment to the latest health information.","startDt":"20231104183000","url":""},"schedule_start_time":"01:30:00"}]} \ No newline at end of file diff --git a/sites/cubmu.com/cubmu.com.config.js b/sites/cubmu.com/cubmu.com.config.js index ec288a36..56a2ae6f 100644 --- a/sites/cubmu.com/cubmu.com.config.js +++ b/sites/cubmu.com/cubmu.com.config.js @@ -1,114 +1,114 @@ -const dayjs = require('dayjs') -const timezone = require('dayjs/plugin/timezone') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(timezone) -dayjs.extend(utc) - -module.exports = { - site: 'cubmu.com', - days: 2, - url({ channel, date }) { - return `https://servicebuss.transvision.co.id/v2/cms/getEPGData?app_id=cubmu&tvs_platform_id=standalone&schedule_date=${date.format( - 'YYYY-MM-DD' - )}&channel_id=${channel.site_id}` - }, - parser({ content, channel }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: parseTitle(item), - description: parseDescription(item, channel.lang), - episode: parseEpisode(item), - start: parseStart(item).toISOString(), - stop: parseStop(item).toISOString() - }) - }) - - return programs - }, - async channels({ lang = 'id' }) { - const axios = require('axios') - const cheerio = require('cheerio') - const result = await axios - .get('https://cubmu.com/live-tv') - .then(response => response.data) - .catch(console.error) - - const $ = cheerio.load(result) - - // retrieve service api data - const config = JSON.parse($('#__NEXT_DATA__').text()).runtimeConfig || {} - - const options = { - headers: { - Origin: 'https://cubmu.com', - Referer: 'https://cubmu.com/live-tv' - } - } - // login to service bus - await axios - .post( - `https://servicebuss.transvision.co.id/tvs/login/external?email=${config.email}&password=${config.password}&deviceId=${config.deviceId}&deviceType=${config.deviceType}&deviceModel=${config.deviceModel}&deviceToken=&serial=&platformId=${config.platformId}`, - options - ) - .then(response => response.data) - .catch(console.error) - // list channels - const subscribedChannels = await axios - .post( - `https://servicebuss.transvision.co.id/tvs/subscribe_product/list?platformId=${config.platformId}`, - options - ) - .then(response => response.data) - .catch(console.error) - - const channels = [] - const included = [] - if (Array.isArray(subscribedChannels.channelPackageList)) { - subscribedChannels.channelPackageList.forEach(pkg => { - pkg.channelList.forEach(channel => { - if (included.indexOf(channel.id) < 0) { - included.push(channel.id) - channels.push({ - lang, - site_id: channel.id, - name: channel.name - }) - } - }) - }) - } - - return channels - } -} - -function parseItems(content) { - return content ? JSON.parse(content.trim()).result || [] : [] -} - -function parseTitle(item) { - return item.scehedule_title -} - -function parseDescription(item, lang = 'id') { - return lang === 'id' ? item.schedule_json.primarySynopsis : item.schedule_json.secondarySynopsis -} - -function parseEpisode(item) { - return item.schedule_json.episodeName -} - -function parseStart(item) { - return dayjs.tz(item.schedule_date, 'YYYY-MM-DD HH:mm:ss', 'Asia/Jakarta') -} - -function parseStop(item) { - return dayjs.tz( - [item.schedule_date.split(' ')[0], item.schedule_end_time].join(' '), - 'YYYY-MM-DD HH:mm:ss', - 'Asia/Jakarta' - ) -} +const dayjs = require('dayjs') +const timezone = require('dayjs/plugin/timezone') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(timezone) +dayjs.extend(utc) + +module.exports = { + site: 'cubmu.com', + days: 2, + url({ channel, date }) { + return `https://servicebuss.transvision.co.id/v2/cms/getEPGData?app_id=cubmu&tvs_platform_id=standalone&schedule_date=${date.format( + 'YYYY-MM-DD' + )}&channel_id=${channel.site_id}` + }, + parser({ content, channel }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: parseTitle(item), + description: parseDescription(item, channel.lang), + episode: parseEpisode(item), + start: parseStart(item).toISOString(), + stop: parseStop(item).toISOString() + }) + }) + + return programs + }, + async channels({ lang = 'id' }) { + const axios = require('axios') + const cheerio = require('cheerio') + const result = await axios + .get('https://cubmu.com/live-tv') + .then(response => response.data) + .catch(console.error) + + const $ = cheerio.load(result) + + // retrieve service api data + const config = JSON.parse($('#__NEXT_DATA__').text()).runtimeConfig || {} + + const options = { + headers: { + Origin: 'https://cubmu.com', + Referer: 'https://cubmu.com/live-tv' + } + } + // login to service bus + await axios + .post( + `https://servicebuss.transvision.co.id/tvs/login/external?email=${config.email}&password=${config.password}&deviceId=${config.deviceId}&deviceType=${config.deviceType}&deviceModel=${config.deviceModel}&deviceToken=&serial=&platformId=${config.platformId}`, + options + ) + .then(response => response.data) + .catch(console.error) + // list channels + const subscribedChannels = await axios + .post( + `https://servicebuss.transvision.co.id/tvs/subscribe_product/list?platformId=${config.platformId}`, + options + ) + .then(response => response.data) + .catch(console.error) + + const channels = [] + const included = [] + if (Array.isArray(subscribedChannels.channelPackageList)) { + subscribedChannels.channelPackageList.forEach(pkg => { + pkg.channelList.forEach(channel => { + if (included.indexOf(channel.id) < 0) { + included.push(channel.id) + channels.push({ + lang, + site_id: channel.id, + name: channel.name + }) + } + }) + }) + } + + return channels + } +} + +function parseItems(content) { + return content ? JSON.parse(content.trim()).result || [] : [] +} + +function parseTitle(item) { + return item.scehedule_title +} + +function parseDescription(item, lang = 'id') { + return lang === 'id' ? item.schedule_json.primarySynopsis : item.schedule_json.secondarySynopsis +} + +function parseEpisode(item) { + return item.schedule_json.episodeName +} + +function parseStart(item) { + return dayjs.tz(item.schedule_date, 'YYYY-MM-DD HH:mm:ss', 'Asia/Jakarta') +} + +function parseStop(item) { + return dayjs.tz( + [item.schedule_date.split(' ')[0], item.schedule_end_time].join(' '), + 'YYYY-MM-DD HH:mm:ss', + 'Asia/Jakarta' + ) +} diff --git a/sites/cubmu.com/cubmu.com.test.js b/sites/cubmu.com/cubmu.com.test.js index cdf5cde3..810454df 100644 --- a/sites/cubmu.com/cubmu.com.test.js +++ b/sites/cubmu.com/cubmu.com.test.js @@ -1,47 +1,47 @@ -const { url, parser } = require('./cubmu.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2023-11-05', 'DD/MM/YYYY').startOf('d') -const channel = { site_id: '4028c68574537fcd0174be43042758d8', xmltv_id: 'TransTV.id', lang: 'id' } -const channelEn = Object.assign({}, channel, { lang: 'en' }) - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://servicebuss.transvision.co.id/v2/cms/getEPGData?app_id=cubmu&tvs_platform_id=standalone&schedule_date=2023-11-05&channel_id=4028c68574537fcd0174be43042758d8' - ) -}) - -it('can parse response', () => { - const content = - '{"result":[{"channel_id":"4028c68574537fcd0174be43042758d8","channel_name":"Trans TV","scehedule_title":"CNN Tech News","schedule_date":"2023-11-05 01:30:00","schedule_end_time":"02:00:00","schedule_json":{"availability":0,"channelId":"4028c68574537fcd0174be43042758d8","channelName":"Trans TV","duration":1800,"editable":true,"episodeName":"","imageUrl":"https://cdnjkt2.transvision.co.id:1001/catchup/schedule/thumbnail/4028c68574537fcd0174be43042758d8/4028c6858b8b3621018b9330e3701a7e/458x640","imageUrlWide":"https://cdnjkt2.transvision.co.id:1001/catchup/schedule/thumbnail/4028c68574537fcd0174be43042758d8/4028c6858b8b3621018b9330e3701a7e/320x180","name":"CNN Tech News","ottImageUrl":"","primarySynopsis":"CNN Indonesia Tech News adalah berita teknologi yang membawa pemirsa ke dunia teknologi yang penuh dengan informasi, pendidikan, hiburan sampai informasi kesehatan terkini.","scheduleId":"4028c6858b8b3621018b9330e3701a7e","scheduleTime":"18:30:00","secondarySynopsis":"CNN Indonesia Tech News is tech news brings viewers into the world of technology that provides information, education, entertainment to the latest health information.","startDt":"20231104183000","url":""},"schedule_start_time":"01:30:00"}]}' - - const idResults = parser({ content, channel }) - expect(idResults).toMatchObject([ - { - start: '2023-11-04T18:30:00.000Z', - stop: '2023-11-04T19:00:00.000Z', - title: 'CNN Tech News', - description: - 'CNN Indonesia Tech News adalah berita teknologi yang membawa pemirsa ke dunia teknologi yang penuh dengan informasi, pendidikan, hiburan sampai informasi kesehatan terkini.' - } - ]) - - const enResults = parser({ content, channel: channelEn }) - expect(enResults).toMatchObject([ - { - start: '2023-11-04T18:30:00.000Z', - stop: '2023-11-04T19:00:00.000Z', - title: 'CNN Tech News', - description: - 'CNN Indonesia Tech News is tech news brings viewers into the world of technology that provides information, education, entertainment to the latest health information.' - } - ]) -}) - -it('can handle empty guide', () => { - const results = parser({ content: '' }) - - expect(results).toMatchObject([]) -}) +const { url, parser } = require('./cubmu.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2023-11-05', 'DD/MM/YYYY').startOf('d') +const channel = { site_id: '4028c68574537fcd0174be43042758d8', xmltv_id: 'TransTV.id', lang: 'id' } +const channelEn = Object.assign({}, channel, { lang: 'en' }) + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://servicebuss.transvision.co.id/v2/cms/getEPGData?app_id=cubmu&tvs_platform_id=standalone&schedule_date=2023-11-05&channel_id=4028c68574537fcd0174be43042758d8' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') + const idResults = parser({ content, channel }) + expect(idResults).toMatchObject([ + { + start: '2023-11-04T18:30:00.000Z', + stop: '2023-11-04T19:00:00.000Z', + title: 'CNN Tech News', + description: + 'CNN Indonesia Tech News adalah berita teknologi yang membawa pemirsa ke dunia teknologi yang penuh dengan informasi, pendidikan, hiburan sampai informasi kesehatan terkini.' + } + ]) + + const enResults = parser({ content, channel: channelEn }) + expect(enResults).toMatchObject([ + { + start: '2023-11-04T18:30:00.000Z', + stop: '2023-11-04T19:00:00.000Z', + title: 'CNN Tech News', + description: + 'CNN Indonesia Tech News is tech news brings viewers into the world of technology that provides information, education, entertainment to the latest health information.' + } + ]) +}) + +it('can handle empty guide', () => { + const results = parser({ content: '' }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/cyta.com.cy/cyta.com.cy.config.js b/sites/cyta.com.cy/cyta.com.cy.config.js index f698dfa4..56c2cb80 100644 --- a/sites/cyta.com.cy/cyta.com.cy.config.js +++ b/sites/cyta.com.cy/cyta.com.cy.config.js @@ -1,59 +1,59 @@ -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: 'cyta.com.cy', - days: 7, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: function ({ date, channel }) { - // Get the epoch timestamp - const todayEpoch = date.startOf('day').utc().valueOf() - // Get the epoch timestamp for the next day - const nextDayEpoch = date.add(1, 'day').startOf('day').utc().valueOf() - return `https://epg.cyta.com.cy/api/mediacatalog/fetchEpg?startTimeEpoch=${todayEpoch}&endTimeEpoch=${nextDayEpoch}&language=1&channelIds=${channel.site_id}` - }, - parser: function ({ content }) { - const data = JSON.parse(content) - const programs = [] - - data.channelEpgs.forEach(channel => { - channel.epgPlayables.forEach(epg => { - const start = new Date(epg.startTime).toISOString() - const stop = new Date(epg.endTime).toISOString() - - programs.push({ - title: epg.name, - start, - stop - }) - }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - const data = await axios - .get('https://epg.cyta.com.cy/api/mediacatalog/fetchChannels?language=1') - .then(r => r.data) - .catch(console.log) - - return data.channels.map(item => { - return { - lang: 'el', - site_id: item.id, - name: item.name - } - }) - } -} +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: 'cyta.com.cy', + days: 7, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: function ({ date, channel }) { + // Get the epoch timestamp + const todayEpoch = date.startOf('day').utc().valueOf() + // Get the epoch timestamp for the next day + const nextDayEpoch = date.add(1, 'day').startOf('day').utc().valueOf() + return `https://epg.cyta.com.cy/api/mediacatalog/fetchEpg?startTimeEpoch=${todayEpoch}&endTimeEpoch=${nextDayEpoch}&language=1&channelIds=${channel.site_id}` + }, + parser: function ({ content }) { + const data = JSON.parse(content) + const programs = [] + + data.channelEpgs.forEach(channel => { + channel.epgPlayables.forEach(epg => { + const start = new Date(epg.startTime).toISOString() + const stop = new Date(epg.endTime).toISOString() + + programs.push({ + title: epg.name, + start, + stop + }) + }) + }) + + return programs + }, + async channels() { + const axios = require('axios') + const data = await axios + .get('https://epg.cyta.com.cy/api/mediacatalog/fetchChannels?language=1') + .then(r => r.data) + .catch(console.log) + + return data.channels.map(item => { + return { + lang: 'el', + site_id: item.id, + name: item.name + } + }) + } +} diff --git a/sites/cyta.com.cy/cyta.com.cy.test.js b/sites/cyta.com.cy/cyta.com.cy.test.js index 95797a92..ebc09b24 100644 --- a/sites/cyta.com.cy/cyta.com.cy.test.js +++ b/sites/cyta.com.cy/cyta.com.cy.test.js @@ -1,49 +1,49 @@ -const { url, parser } = require('./cyta.com.cy.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -const date = dayjs.utc('2025-01-03', 'YYYY-MM-DD').startOf('day') -const channel = { - site_id: '561066', - xmltv_id: 'RIK1.cy' -} - -it('can generate valid url', () => { - const generatedUrl = url({ date, channel }) - expect(generatedUrl).toBe( - 'https://epg.cyta.com.cy/api/mediacatalog/fetchEpg?startTimeEpoch=1735862400000&endTimeEpoch=1735948800000&language=1&channelIds=561066' - ) -}) - -it('can parse response', () => { - const content = ` - { - "channelEpgs": [ - { - "epgPlayables": [ - { "name": "Πρώτη Ενημέρωση", "startTime": 1735879500000, "endTime": 1735889400000 } - ] - } - ] - }` - - const result = parser({ content }) - - expect(result).toMatchObject([ - { - title: 'Πρώτη Ενημέρωση', - start: '2025-01-03T04:45:00.000Z', - stop: '2025-01-03T07:30:00.000Z' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '{"channelEpgs":[]}' - }) - expect(result).toMatchObject([]) -}) +const { url, parser } = require('./cyta.com.cy.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +const date = dayjs.utc('2025-01-03', 'YYYY-MM-DD').startOf('day') +const channel = { + site_id: '561066', + xmltv_id: 'RIK1.cy' +} + +it('can generate valid url', () => { + const generatedUrl = url({ date, channel }) + expect(generatedUrl).toBe( + 'https://epg.cyta.com.cy/api/mediacatalog/fetchEpg?startTimeEpoch=1735862400000&endTimeEpoch=1735948800000&language=1&channelIds=561066' + ) +}) + +it('can parse response', () => { + const content = ` + { + "channelEpgs": [ + { + "epgPlayables": [ + { "name": "Πρώτη Ενημέρωση", "startTime": 1735879500000, "endTime": 1735889400000 } + ] + } + ] + }` + + const result = parser({ content }) + + expect(result).toMatchObject([ + { + title: 'Πρώτη Ενημέρωση', + start: '2025-01-03T04:45:00.000Z', + stop: '2025-01-03T07:30:00.000Z' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '{"channelEpgs":[]}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/dens.tv/dens.tv.config.js b/sites/dens.tv/dens.tv.config.js index 25cd3036..11049d6c 100644 --- a/sites/dens.tv/dens.tv.config.js +++ b/sites/dens.tv/dens.tv.config.js @@ -1,73 +1,73 @@ -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) - -const tz = 'Asia/Jakarta' - -module.exports = { - site: 'dens.tv', - days: 2, - url({ channel, date }) { - return `https://www.dens.tv/api/dens3/tv/TvChannels/listEpgByDate?date=${date.format( - 'YYYY-MM-DD' - )}&id_channel=${channel.site_id}&app_type=10` - }, - parser({ content }) { - // parsing - const response = JSON.parse(content) - const programs = [] - - if (Array.isArray(response?.data)) { - response.data.forEach(item => { - const title = item.title - const [, , , season, , , episode] = title.match( - /( (Season |Season|S)(\d+))?( (Episode|Ep) (\d+))/ - ) || [null, null, null, null, null, null, null] - programs.push({ - title, - description: item.description, - season: season ? parseInt(season) : season, - episode: episode ? parseInt(episode) : episode, - start: dayjs.tz(item.start_time, 'YYYY-MM-DD HH:mm:ss', tz), - stop: dayjs.tz(item.end_time, 'YYYY-MM-DD HH:mm:ss', tz) - }) - }) - } - - return programs - }, - async channels() { - const axios = require('axios') - - const categories = { - local: 1, - premium: 2, - international: 3 - } - - const channels = [] - for (const id_category of Object.values(categories)) { - const data = await axios - .get('https://www.dens.tv/api/dens3/tv/TvChannels/listByCategory', { - params: { id_category } - }) - .then(r => r.data) - .catch(console.error) - - data.data.contents.forEach(item => { - channels.push({ - lang: 'id', - site_id: item.meta.id, - name: item.meta.title - }) - }) - } - - return channels - } -} +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) + +const tz = 'Asia/Jakarta' + +module.exports = { + site: 'dens.tv', + days: 2, + url({ channel, date }) { + return `https://www.dens.tv/api/dens3/tv/TvChannels/listEpgByDate?date=${date.format( + 'YYYY-MM-DD' + )}&id_channel=${channel.site_id}&app_type=10` + }, + parser({ content }) { + // parsing + const response = JSON.parse(content) + const programs = [] + + if (Array.isArray(response?.data)) { + response.data.forEach(item => { + const title = item.title + const [, , , season, , , episode] = title.match( + /( (Season |Season|S)(\d+))?( (Episode|Ep) (\d+))/ + ) || [null, null, null, null, null, null, null] + programs.push({ + title, + description: item.description, + season: season ? parseInt(season) : season, + episode: episode ? parseInt(episode) : episode, + start: dayjs.tz(item.start_time, 'YYYY-MM-DD HH:mm:ss', tz), + stop: dayjs.tz(item.end_time, 'YYYY-MM-DD HH:mm:ss', tz) + }) + }) + } + + return programs + }, + async channels() { + const axios = require('axios') + + const categories = { + local: 1, + premium: 2, + international: 3 + } + + const channels = [] + for (const id_category of Object.values(categories)) { + const data = await axios + .get('https://www.dens.tv/api/dens3/tv/TvChannels/listByCategory', { + params: { id_category } + }) + .then(r => r.data) + .catch(console.error) + + data.data.contents.forEach(item => { + channels.push({ + lang: 'id', + site_id: item.meta.id, + name: item.meta.title + }) + }) + } + + return channels + } +} diff --git a/sites/dens.tv/dens.tv.test.js b/sites/dens.tv/dens.tv.test.js index 79883d0f..5200e994 100644 --- a/sites/dens.tv/dens.tv.test.js +++ b/sites/dens.tv/dens.tv.test.js @@ -1,50 +1,50 @@ -const { url, parser } = require('./dens.tv.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -const date = dayjs.utc('2024-11-24').startOf('d') -const channel = { site_id: '38', xmltv_id: 'AniplusAsia.sg', lang: 'id' } - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.dens.tv/api/dens3/tv/TvChannels/listEpgByDate?date=2024-11-24&id_channel=38&app_type=10' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(2) - - expect(results[0]).toMatchObject({ - start: '2024-11-23T17:00:00.000Z', - stop: '2024-11-23T17:30:00.000Z', - title: 'Migi & Dali Episode 2', - episode: 2 - }) - - expect(results[1]).toMatchObject({ - start: '2024-11-23T19:30:00.000Z', - stop: '2024-11-23T20:00:00.000Z', - title: 'Attack on Titan Season 3 Episode 7', - season: 3, - episode: 7 - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const results = parser({ content }) - - expect(results).toMatchObject([]) -}) +const { url, parser } = require('./dens.tv.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +const date = dayjs.utc('2024-11-24').startOf('d') +const channel = { site_id: '38', xmltv_id: 'AniplusAsia.sg', lang: 'id' } + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.dens.tv/api/dens3/tv/TvChannels/listEpgByDate?date=2024-11-24&id_channel=38&app_type=10' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(2) + + expect(results[0]).toMatchObject({ + start: '2024-11-23T17:00:00.000Z', + stop: '2024-11-23T17:30:00.000Z', + title: 'Migi & Dali Episode 2', + episode: 2 + }) + + expect(results[1]).toMatchObject({ + start: '2024-11-23T19:30:00.000Z', + stop: '2024-11-23T20:00:00.000Z', + title: 'Attack on Titan Season 3 Episode 7', + season: 3, + episode: 7 + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const results = parser({ content }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/derana.lk/derana.lk.config.js b/sites/derana.lk/derana.lk.config.js index 4d43be08..266bb725 100644 --- a/sites/derana.lk/derana.lk.config.js +++ b/sites/derana.lk/derana.lk.config.js @@ -1,48 +1,48 @@ -const dayjs = require('dayjs') -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 _ = require('lodash') - -dayjs.extend(customParseFormat) -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'derana.lk', - url({ date }) { - return `https://derana.lk/api/schedules/${date.format('DD-MM-YYYY')}` - }, - parser({ content }) { - const programs = parseItems(content).map(item => { - const start = parseStart(item) - const duration = parseDuration(item.duration) - const stop = start.add(duration, 'ms') - - return { - title: item.dramaName, - image: item.imageUrl, - start, - stop - } - }) - - return _.sortBy(programs, p => p.start) - } -} - -function parseStart(item) { - return dayjs.tz(`${item.date} ${item.time}`, 'DD-MM-YYYY H:mm A', 'Asia/Colombo') -} - -function parseItems(content) { - try { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.all_schedules)) return [] - - return data.all_schedules - } catch { - return [] - } -} +const dayjs = require('dayjs') +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('lodash.sortby') + +dayjs.extend(customParseFormat) +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'derana.lk', + url({ date }) { + return `https://derana.lk/api/schedules/${date.format('DD-MM-YYYY')}` + }, + parser({ content }) { + const programs = parseItems(content).map(item => { + const start = parseStart(item) + const duration = parseDuration(item.duration) + const stop = start.add(duration, 'ms') + + return { + title: item.dramaName, + image: item.imageUrl, + start, + stop + } + }) + + return sortBy(programs, p => p.start.valueOf()) + } +} + +function parseStart(item) { + return dayjs.tz(`${item.date} ${item.time}`, 'DD-MM-YYYY H:mm A', 'Asia/Colombo') +} + +function parseItems(content) { + try { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.all_schedules)) return [] + + return data.all_schedules + } catch { + return [] + } +} diff --git a/sites/derana.lk/derana.lk.test.js b/sites/derana.lk/derana.lk.test.js index d8191b56..07025973 100644 --- a/sites/derana.lk/derana.lk.test.js +++ b/sites/derana.lk/derana.lk.test.js @@ -1,50 +1,50 @@ -const { parser, url } = require('./derana.lk.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-05-18', 'YYYY-MM-DD').startOf('d') - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://derana.lk/api/schedules/18-05-2025') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - - let results = parser({ content }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - - return p - }) - - expect(results.length).toBe(20) - expect(results[0]).toMatchObject({ - title: 'Dahami Derana', - image: 'https://derana.lk/storage/uploads/imgs/program/51/20240717062206.jpg', - start: '2025-05-17T23:05:00.000Z', - stop: '2025-05-18T00:55:00.000Z' - }) - expect(results[1]).toMatchObject({ - title: 'Derana Aruna', - image: 'https://derana.lk/storage/uploads/imgs/program/15/20240613075807.jpg', - start: '2025-05-18T00:55:00.000Z', - stop: '2025-05-18T02:00:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: { - error: 'An error occurred' - } - }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./derana.lk.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-05-18', 'YYYY-MM-DD').startOf('d') + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://derana.lk/api/schedules/18-05-2025') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + + let results = parser({ content }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + + return p + }) + + expect(results.length).toBe(20) + expect(results[0]).toMatchObject({ + title: 'Dahami Derana', + image: 'https://derana.lk/storage/uploads/imgs/program/51/20240717062206.jpg', + start: '2025-05-17T23:05:00.000Z', + stop: '2025-05-18T00:55:00.000Z' + }) + expect(results[1]).toMatchObject({ + title: 'Derana Aruna', + image: 'https://derana.lk/storage/uploads/imgs/program/15/20240613075807.jpg', + start: '2025-05-18T00:55:00.000Z', + stop: '2025-05-18T02:00:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: { + error: 'An error occurred' + } + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/digea.gr/digea.gr.config.js b/sites/digea.gr/digea.gr.config.js index 32c2b923..c0599b38 100644 --- a/sites/digea.gr/digea.gr.config.js +++ b/sites/digea.gr/digea.gr.config.js @@ -1,86 +1,86 @@ -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') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'digea.gr', - days: 2, - url: 'https://www.digea.gr/el/api/epg/get-events', - request: { - method: 'POST', - headers: { - 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8' - }, - data({ date }) { - const data = new URLSearchParams() - data.append('action', 'get_events') - data.append('date', date.format('YYYY-M-D')) - - return data - } - }, - parser({ content, channel }) { - let programs = [] - let items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.long_synopsis, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .post( - 'https://www.digea.gr/el/api/epg/get-channels', - new URLSearchParams({ - action: 'get_chanels', - lang: 'el' - }), - { - headers: { - 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8' - } - } - ) - .then(r => r.data) - .catch(console.error) - - return data.map(channel => { - return { - lang: 'el', - site_id: channel.id, - name: channel.name - } - }) - } -} - -function parseStart(item) { - return dayjs.tz(item.actual_time, 'YYYY-MM-DD HH:mm:ss', 'Europe/Athens') -} - -function parseStop(item) { - return dayjs.tz(item.end_time, 'YYYY-MM-DD HH:mm:ss', 'Europe/Athens') -} - -function parseItems(content, channel) { - try { - const data = JSON.parse(content) - if (!Array.isArray(data)) return [] - - return data.filter(p => p.channel_id === channel.site_id) - } catch { - return [] - } -} +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') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'digea.gr', + days: 2, + url: 'https://www.digea.gr/el/api/epg/get-events', + request: { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8' + }, + data({ date }) { + const data = new URLSearchParams() + data.append('action', 'get_events') + data.append('date', date.format('YYYY-M-D')) + + return data + } + }, + parser({ content, channel }) { + let programs = [] + let items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.long_synopsis, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .post( + 'https://www.digea.gr/el/api/epg/get-channels', + new URLSearchParams({ + action: 'get_chanels', + lang: 'el' + }), + { + headers: { + 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8' + } + } + ) + .then(r => r.data) + .catch(console.error) + + return data.map(channel => { + return { + lang: 'el', + site_id: channel.id, + name: channel.name + } + }) + } +} + +function parseStart(item) { + return dayjs.tz(item.actual_time, 'YYYY-MM-DD HH:mm:ss', 'Europe/Athens') +} + +function parseStop(item) { + return dayjs.tz(item.end_time, 'YYYY-MM-DD HH:mm:ss', 'Europe/Athens') +} + +function parseItems(content, channel) { + try { + const data = JSON.parse(content) + if (!Array.isArray(data)) return [] + + return data.filter(p => p.channel_id === channel.site_id) + } catch { + return [] + } +} diff --git a/sites/digea.gr/digea.gr.test.js b/sites/digea.gr/digea.gr.test.js index e3da1ca1..4d4bd523 100644 --- a/sites/digea.gr/digea.gr.test.js +++ b/sites/digea.gr/digea.gr.test.js @@ -1,69 +1,69 @@ -const { parser, url, request } = require('./digea.gr.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-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1100', - xmltv_id: 'AlphaTV.gr' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.digea.gr/el/api/epg/get-events') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8' - }) -}) - -it('can generate valid request data', () => { - const data = request.data({ date }) - - expect(data.get('action')).toBe('get_events') - expect(data.get('date')).toBe('2025-1-17') -}) - -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(19) - expect(results[0]).toMatchObject({ - start: '2025-01-16T23:30:00.000Z', - stop: '2025-01-17T01:30:00.000Z', - title: '[K12] Το Ξεκαθάρισμα (A Score To Settle)', - description: - "Περιπέτεια αμερικανικής παραγωγής 2019 [Το πρόγραμμα περιέχει σκηνές σεξουαλικές, βίας, χρήσης ναρκωτικών κι άλλων εξαρτησιογόνων ουσιών και απρεπή εκφορά λόγου]. Ο Φρανκ απελευθερώνεται από τη φυλακή πολλά χρόνια μετά αφού κατηγορήθηκε για ένα έγκλημα που δεν διέπραξε. Τώρα, ελεύθερος, ξεκινά μια πορεία εκδίκησης εναντίον των ανθρώπων των οποίων οι πράξεις τον έστειλαν στη φυλακή. Ηθοποιοί: Νίκολας Κέιτζ, Μπέντζαμιν Μπρατ, Νόα Λε Γκρος, Καρολίνα Γουίντρα. Σενάριο: Σον Κου, Τζον Νιούμαν. Σκηνοθεσία: Σον Κου. Διάρκεια: 94'. " - }) - expect(results[18]).toMatchObject({ - start: '2025-01-17T21:30:00.000Z', - stop: '2025-01-17T23:30:00.000Z', - title: '[K8] Βασικά Καλησπέρα Σας', - description: - "Κωμωδία ελληνικής παραγωγής 1982. Δύο πειρατικοί ραδιοσταθμοί, εκ των οποίων ο ένας βάζει λαϊκά άσματα και ο άλλος ροκ μουσική, ανταγωνίζονται για την πρωτιά στην ακροαματικότητα. Ο ανταγωνισμός γίνεται βαθμηδόν όλο και πιο σκληρός, αλλά ξάφνου τα πράγματα αλλάζουν ρότα καθώς ο μεγαλοδύναμος έρως παρεμβαίνει και κάνει το θαύμα του. Παίζουν: Στάθης Ψάλτης, Πάνος Μιχαλόπουλος, Σταμάτης Γαρδέλης, Έφη Πίκουλα, Γιώργος Ρήγας, Γιάννης Μποσταντζόγλου, Σοφία Αλιμπέρτη, Καίτη Φίνου. Σκηνοθεσία - Σενάριο: Γιάννης Δαλιανίδης. Διάρκεια: 89'." - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: '[]' - }) - - expect(results).toMatchObject([]) -}) +const { parser, url, request } = require('./digea.gr.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-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1100', + xmltv_id: 'AlphaTV.gr' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.digea.gr/el/api/epg/get-events') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8' + }) +}) + +it('can generate valid request data', () => { + const data = request.data({ date }) + + expect(data.get('action')).toBe('get_events') + expect(data.get('date')).toBe('2025-1-17') +}) + +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(19) + expect(results[0]).toMatchObject({ + start: '2025-01-16T23:30:00.000Z', + stop: '2025-01-17T01:30:00.000Z', + title: '[K12] Το Ξεκαθάρισμα (A Score To Settle)', + description: + "Περιπέτεια αμερικανικής παραγωγής 2019 [Το πρόγραμμα περιέχει σκηνές σεξουαλικές, βίας, χρήσης ναρκωτικών κι άλλων εξαρτησιογόνων ουσιών και απρεπή εκφορά λόγου]. Ο Φρανκ απελευθερώνεται από τη φυλακή πολλά χρόνια μετά αφού κατηγορήθηκε για ένα έγκλημα που δεν διέπραξε. Τώρα, ελεύθερος, ξεκινά μια πορεία εκδίκησης εναντίον των ανθρώπων των οποίων οι πράξεις τον έστειλαν στη φυλακή. Ηθοποιοί: Νίκολας Κέιτζ, Μπέντζαμιν Μπρατ, Νόα Λε Γκρος, Καρολίνα Γουίντρα. Σενάριο: Σον Κου, Τζον Νιούμαν. Σκηνοθεσία: Σον Κου. Διάρκεια: 94'. " + }) + expect(results[18]).toMatchObject({ + start: '2025-01-17T21:30:00.000Z', + stop: '2025-01-17T23:30:00.000Z', + title: '[K8] Βασικά Καλησπέρα Σας', + description: + "Κωμωδία ελληνικής παραγωγής 1982. Δύο πειρατικοί ραδιοσταθμοί, εκ των οποίων ο ένας βάζει λαϊκά άσματα και ο άλλος ροκ μουσική, ανταγωνίζονται για την πρωτιά στην ακροαματικότητα. Ο ανταγωνισμός γίνεται βαθμηδόν όλο και πιο σκληρός, αλλά ξάφνου τα πράγματα αλλάζουν ρότα καθώς ο μεγαλοδύναμος έρως παρεμβαίνει και κάνει το θαύμα του. Παίζουν: Στάθης Ψάλτης, Πάνος Μιχαλόπουλος, Σταμάτης Γαρδέλης, Έφη Πίκουλα, Γιώργος Ρήγας, Γιάννης Μποσταντζόγλου, Σοφία Αλιμπέρτη, Καίτη Φίνου. Σκηνοθεσία - Σενάριο: Γιάννης Δαλιανίδης. Διάρκεια: 89'." + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: '[]' + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/digiturk.com.tr/digiturk.com.tr.config.js b/sites/digiturk.com.tr/digiturk.com.tr.config.js index 23d1b55e..39fdb8c1 100644 --- a/sites/digiturk.com.tr/digiturk.com.tr.config.js +++ b/sites/digiturk.com.tr/digiturk.com.tr.config.js @@ -1,86 +1,86 @@ -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) - -const tz = 'Europe/Istanbul' - -module.exports = { - site: 'digiturk.com.tr', - days: 2, - url({ date }) { - return `https://www.digiturk.com.tr/Ajax/GetTvGuideFromDigiturk?Day=${ - encodeURIComponent(date.format('MM/DD/YYYY')) - }+00%3A00%3A00` - }, - request: { - cache: { - ttl: 24 * 60 * 60 * 1000 // 1 day - } - }, - parser({ content, channel, date }) { - const programs = [] - if (content) { - const $ = cheerio.load(content) - $('.channelDetail').toArray() - .forEach(item => { - const $item = $(item) - const title = $item.find('.tvGuideResult-box-wholeDates-title') - if (title.length) { - const channelId = title.attr('onclick') - if (channelId) { - const site_id = channelId.match(/\s(\d+)\)/)[1] - if (channel.site_id === site_id) { - const startTime = $item.find('.tvGuideResult-box-wholeDates-time-hour').text().trim() - const duration = $item.find('.tvGuideResult-box-wholeDates-time-totalMinute') - .text().trim().match(/\d+/)[0] - const start = dayjs.tz(`${date.format('YYYY-MM-DD')} ${startTime}`, 'YYYY-MM-DD HH:mm', tz) - const stop = start.add(parseInt(duration), 'm') - programs.push({ - title: title.text().trim(), - start, - stop - }) - } - } - } - }) - } - - return programs - }, - async channels() { - const channels = {} - const axios = require('axios') - const data = await axios - .get(this.url({ date: dayjs() })) - .then(r => r.data) - .catch(console.error) - - const $ = cheerio.load(data) - $('.channelContent').toArray() - .forEach(el => { - const item = $(el) - const channelId = item.find('.channelDetail .tvGuideResult-box-wholeDates-title') - .first() - .attr('onclick') - if (channelId) { - const site_id = channelId.match(/\s(\d+)\)/)[1] - if (channels[site_id] === undefined) { - channels[site_id] = { - lang: 'tr', - site_id, - name: item.find('#channelID').val() - } - } - } - }) - - return Object.values(channels) - } -} +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) + +const tz = 'Europe/Istanbul' + +module.exports = { + site: 'digiturk.com.tr', + days: 2, + url({ date }) { + return `https://www.digiturk.com.tr/Ajax/GetTvGuideFromDigiturk?Day=${ + encodeURIComponent(date.format('MM/DD/YYYY')) + }+00%3A00%3A00` + }, + request: { + cache: { + ttl: 24 * 60 * 60 * 1000 // 1 day + } + }, + parser({ content, channel, date }) { + const programs = [] + if (content) { + const $ = cheerio.load(content) + $('.channelDetail').toArray() + .forEach(item => { + const $item = $(item) + const title = $item.find('.tvGuideResult-box-wholeDates-title') + if (title.length) { + const channelId = title.attr('onclick') + if (channelId) { + const site_id = channelId.match(/\s(\d+)\)/)[1] + if (channel.site_id === site_id) { + const startTime = $item.find('.tvGuideResult-box-wholeDates-time-hour').text().trim() + const duration = $item.find('.tvGuideResult-box-wholeDates-time-totalMinute') + .text().trim().match(/\d+/)[0] + const start = dayjs.tz(`${date.format('YYYY-MM-DD')} ${startTime}`, 'YYYY-MM-DD HH:mm', tz) + const stop = start.add(parseInt(duration), 'm') + programs.push({ + title: title.text().trim(), + start, + stop + }) + } + } + } + }) + } + + return programs + }, + async channels() { + const channels = {} + const axios = require('axios') + const data = await axios + .get(this.url({ date: dayjs() })) + .then(r => r.data) + .catch(console.error) + + const $ = cheerio.load(data) + $('.channelContent').toArray() + .forEach(el => { + const item = $(el) + const channelId = item.find('.channelDetail .tvGuideResult-box-wholeDates-title') + .first() + .attr('onclick') + if (channelId) { + const site_id = channelId.match(/\s(\d+)\)/)[1] + if (channels[site_id] === undefined) { + channels[site_id] = { + lang: 'tr', + site_id, + name: item.find('#channelID').val() + } + } + } + }) + + return Object.values(channels) + } +} diff --git a/sites/digiturk.com.tr/digiturk.com.tr.test.js b/sites/digiturk.com.tr/digiturk.com.tr.test.js index 9d0f0b22..1086ee52 100644 --- a/sites/digiturk.com.tr/digiturk.com.tr.test.js +++ b/sites/digiturk.com.tr/digiturk.com.tr.test.js @@ -1,48 +1,48 @@ -const { parser, url } = require('./digiturk.com.tr.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-12', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '351', - xmltv_id: 'Nickelodeon.tr' -} - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe( - 'https://www.digiturk.com.tr/Ajax/GetTvGuideFromDigiturk?Day=01%2F12%2F2025+00%3A00%3A00' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.html')) - const results = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(57) - expect(results[0]).toMatchObject({ - start: '2025-01-11T21:00:00.000Z', - stop: '2025-01-11T21:25:00.000Z', - title: 'Sünger Bob Kare Pantolon' - }) - expect(results[56]).toMatchObject({ - start: '2025-01-12T17:40:00.000Z', - stop: '2025-01-12T18:00:00.000Z', - title: 'Casagrande Ailesi' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ content: '', channel, date }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./digiturk.com.tr.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-12', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '351', + xmltv_id: 'Nickelodeon.tr' +} + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe( + 'https://www.digiturk.com.tr/Ajax/GetTvGuideFromDigiturk?Day=01%2F12%2F2025+00%3A00%3A00' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.html')) + const results = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(57) + expect(results[0]).toMatchObject({ + start: '2025-01-11T21:00:00.000Z', + stop: '2025-01-11T21:25:00.000Z', + title: 'Sünger Bob Kare Pantolon' + }) + expect(results[56]).toMatchObject({ + start: '2025-01-12T17:40:00.000Z', + stop: '2025-01-12T18:00:00.000Z', + title: 'Casagrande Ailesi' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ content: '', channel, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/directv.com.ar/__data__/content.json b/sites/directv.com.ar/__data__/content.json new file mode 100644 index 00000000..fe1cea5c --- /dev/null +++ b/sites/directv.com.ar/__data__/content.json @@ -0,0 +1 @@ +{"d":[{"ChannelSection":"","ChannelFullName":"A&E HD","IsFavorite":false,"ChannelName":"A&EHD","ChannelNumber":207,"ProgramList":[{"_channelSection":"","eventId":"120289890767","titleId":"SH0110397700000001","title":"Chicas guapas","programId":null,"description":"Un espacio destinado a la belleza y los distintos estilos de vida, que muestra el trabajo inspiracional de la moda latinoamericana.","episodeTitle":null,"channelNumber":120,"channelName":"AME2","channelFullName":"América TV (ARG)","channelSection":"","contentChannelID":120,"startTime":"/Date(-62135578800000)/","endTime":"/Date(-62135578800000)/","GMTstartTime":"/Date(-62135578800000)/","GMTendTime":"/Date(-62135578800000)/","css":16,"language":null,"tmsId":"SH0110397700000001","rating":"NR","categoryId":"Tipos de Programas","categoryName":0,"subCategoryId":0,"subCategoryName":"Series","serviceExpiration":"/Date(-62135578800000)/","crId":null,"promoUrl1":null,"promoUrl2":null,"price":0,"isPurchasable":"N","videoUrl":"","imageUrl":"https://dnqt2wx2urq99.cloudfront.net/ondirectv/LOGOS/Canales/AR/120.png","titleSecond":"Chicas guapas","isHD":"N","DetailsURL":null,"BuyURL":null,"ProgramServiceId":null,"SearchDateTime":null,"startTimeString":"6/19/2022 12:00:00 AM","endTimeString":"6/19/2022 12:15:00 AM","DurationInMinutes":null,"castDetails":null,"scheduleDetails":null,"seriesDetails":null,"processedSeasonDetails":null}]}]} \ No newline at end of file diff --git a/sites/directv.com.ar/directv.com.ar.config.js b/sites/directv.com.ar/directv.com.ar.config.js index a483cbb2..9918e291 100644 --- a/sites/directv.com.ar/directv.com.ar.config.js +++ b/sites/directv.com.ar/directv.com.ar.config.js @@ -1,100 +1,100 @@ -process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0 -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: 'directv.com.ar', - days: 2, - url: 'https://www.directv.com.ar/guia/ChannelDetail.aspx/GetProgramming', - request: { - method: 'POST', - headers: { - Cookie: 'PGCSS=16; PGLang=S; PGCulture=es-AR;', - Accept: '*/*', - 'Accept-Language': 'es-419,es;q=0.9', - Connection: 'keep-alive', - 'Content-Type': 'application/json; charset=UTF-8', - Origin: 'https://www.directv.com.ar', - Referer: 'https://www.directv.com.ar/guia/ChannelDetail.aspx?id=1740&name=TLCHD', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'same-origin', - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', - 'sec-ch-ua': '"Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"', - 'sec-ch-ua-mobile': '?0', - 'sec-ch-ua-platform': '"Windows"' - }, - data({ channel, date }) { - const [channelNum, channelName] = channel.site_id.split('#') - - return { - filterParameters: { - day: date.date(), - time: 0, - minute: 0, - month: date.month() + 1, - year: date.year(), - offSetValue: 0, - homeScreenFilter: '', - filtersScreenFilters: [''], - isHd: '', - isChannelDetails: 'Y', - channelNum, - channelName: channelName.replace('&', '&') - } - } - } - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.description, - rating: parseRating(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseRating(item) { - return item.rating - ? { - system: 'MPA', - value: item.rating - } - : null -} - -function parseStart(item) { - return dayjs.tz(item.startTimeString, 'M/D/YYYY h:mm:ss A', 'America/Argentina/Buenos_Aires') -} - -function parseStop(item) { - return dayjs.tz(item.endTimeString, 'M/D/YYYY h:mm:ss A', 'America/Argentina/Buenos_Aires') -} - -function parseItems(content, channel) { - if (!content) return [] - let [ChannelNumber, ChannelName] = channel.site_id.split('#') - ChannelName = ChannelName.replace('&', '&') - const data = JSON.parse(content) - if (!data || !Array.isArray(data.d)) return [] - const channelData = data.d.find( - c => c.ChannelNumber == ChannelNumber && c.ChannelName === ChannelName - ) - - return channelData && Array.isArray(channelData.ProgramList) ? channelData.ProgramList : [] -} +process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0 +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: 'directv.com.ar', + days: 2, + url: 'https://www.directv.com.ar/guia/ChannelDetail.aspx/GetProgramming', + request: { + method: 'POST', + headers: { + Cookie: 'PGCSS=16; PGLang=S; PGCulture=es-AR;', + Accept: '*/*', + 'Accept-Language': 'es-419,es;q=0.9', + Connection: 'keep-alive', + 'Content-Type': 'application/json; charset=UTF-8', + Origin: 'https://www.directv.com.ar', + Referer: 'https://www.directv.com.ar/guia/ChannelDetail.aspx?id=1740&name=TLCHD', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', + 'sec-ch-ua': '"Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"Windows"' + }, + data({ channel, date }) { + const [channelNum, channelName] = channel.site_id.split('#') + + return { + filterParameters: { + day: date.date(), + time: 0, + minute: 0, + month: date.month() + 1, + year: date.year(), + offSetValue: 0, + homeScreenFilter: '', + filtersScreenFilters: [''], + isHd: '', + isChannelDetails: 'Y', + channelNum, + channelName: channelName.replace('&', '&') + } + } + } + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.description, + rating: parseRating(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseRating(item) { + return item.rating + ? { + system: 'MPA', + value: item.rating + } + : null +} + +function parseStart(item) { + return dayjs.tz(item.startTimeString, 'M/D/YYYY h:mm:ss A', 'America/Argentina/Buenos_Aires') +} + +function parseStop(item) { + return dayjs.tz(item.endTimeString, 'M/D/YYYY h:mm:ss A', 'America/Argentina/Buenos_Aires') +} + +function parseItems(content, channel) { + if (!content) return [] + let [ChannelNumber, ChannelName] = channel.site_id.split('#') + ChannelName = ChannelName.replace('&', '&') + const data = JSON.parse(content) + if (!data || !Array.isArray(data.d)) return [] + const channelData = data.d.find( + c => c.ChannelNumber == ChannelNumber && c.ChannelName === ChannelName + ) + + return channelData && Array.isArray(channelData.ProgramList) ? channelData.ProgramList : [] +} diff --git a/sites/directv.com.ar/directv.com.ar.test.js b/sites/directv.com.ar/directv.com.ar.test.js index 8128380a..8786781a 100644 --- a/sites/directv.com.ar/directv.com.ar.test.js +++ b/sites/directv.com.ar/directv.com.ar.test.js @@ -1,77 +1,78 @@ -const { parser, url, request } = require('./directv.com.ar.config.js') -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('2022-06-19', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '207#A&EHD', - xmltv_id: 'AEHDSouth.us' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.directv.com.ar/guia/ChannelDetail.aspx/GetProgramming') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/json; charset=UTF-8', - Cookie: 'PGCSS=16; PGLang=S; PGCulture=es-AR;' - }) -}) - -it('can generate valid request data', () => { - expect(request.data({ channel, date })).toMatchObject({ - filterParameters: { - day: 19, - time: 0, - minute: 0, - month: 6, - year: 2022, - offSetValue: 0, - filtersScreenFilters: [''], - isHd: '', - isChannelDetails: 'Y', - channelNum: '207', - channelName: 'A&EHD' - } - }) -}) - -it('can parse response', () => { - const content = - '{"d":[{"ChannelSection":"","ChannelFullName":"A&E HD","IsFavorite":false,"ChannelName":"A&EHD","ChannelNumber":207,"ProgramList":[{"_channelSection":"","eventId":"120289890767","titleId":"SH0110397700000001","title":"Chicas guapas","programId":null,"description":"Un espacio destinado a la belleza y los distintos estilos de vida, que muestra el trabajo inspiracional de la moda latinoamericana.","episodeTitle":null,"channelNumber":120,"channelName":"AME2","channelFullName":"América TV (ARG)","channelSection":"","contentChannelID":120,"startTime":"/Date(-62135578800000)/","endTime":"/Date(-62135578800000)/","GMTstartTime":"/Date(-62135578800000)/","GMTendTime":"/Date(-62135578800000)/","css":16,"language":null,"tmsId":"SH0110397700000001","rating":"NR","categoryId":"Tipos de Programas","categoryName":0,"subCategoryId":0,"subCategoryName":"Series","serviceExpiration":"/Date(-62135578800000)/","crId":null,"promoUrl1":null,"promoUrl2":null,"price":0,"isPurchasable":"N","videoUrl":"","imageUrl":"https://dnqt2wx2urq99.cloudfront.net/ondirectv/LOGOS/Canales/AR/120.png","titleSecond":"Chicas guapas","isHD":"N","DetailsURL":null,"BuyURL":null,"ProgramServiceId":null,"SearchDateTime":null,"startTimeString":"6/19/2022 12:00:00 AM","endTimeString":"6/19/2022 12:15:00 AM","DurationInMinutes":null,"castDetails":null,"scheduleDetails":null,"seriesDetails":null,"processedSeasonDetails":null}]}]}' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-06-19T03:00:00.000Z', - stop: '2022-06-19T03:15:00.000Z', - title: 'Chicas guapas', - description: - 'Un espacio destinado a la belleza y los distintos estilos de vida, que muestra el trabajo inspiracional de la moda latinoamericana.', - rating: { - system: 'MPA', - value: 'NR' - } - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '', - channel - }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./directv.com.ar.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('2022-06-19', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '207#A&EHD', + xmltv_id: 'AEHDSouth.us' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.directv.com.ar/guia/ChannelDetail.aspx/GetProgramming') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/json; charset=UTF-8', + Cookie: 'PGCSS=16; PGLang=S; PGCulture=es-AR;' + }) +}) + +it('can generate valid request data', () => { + expect(request.data({ channel, date })).toMatchObject({ + filterParameters: { + day: 19, + time: 0, + minute: 0, + month: 6, + year: 2022, + offSetValue: 0, + filtersScreenFilters: [''], + isHd: '', + isChannelDetails: 'Y', + channelNum: '207', + channelName: 'A&EHD' + } + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-06-19T03:00:00.000Z', + stop: '2022-06-19T03:15:00.000Z', + title: 'Chicas guapas', + description: + 'Un espacio destinado a la belleza y los distintos estilos de vida, que muestra el trabajo inspiracional de la moda latinoamericana.', + rating: { + system: 'MPA', + value: 'NR' + } + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '', + channel + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/directv.com.uy/directv.com.uy.config.js b/sites/directv.com.uy/directv.com.uy.config.js index c071406c..f1a828ac 100644 --- a/sites/directv.com.uy/directv.com.uy.config.js +++ b/sites/directv.com.uy/directv.com.uy.config.js @@ -1,85 +1,85 @@ -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: 'directv.com.uy', - days: 2, - url: 'https://www.directv.com.uy/guia/ChannelDetail.aspx/GetProgramming', - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - Cookie: 'PGCSS=16384; PGLang=S; PGCulture=es-UY;' - }, - data({ channel, date }) { - const [channelNum, channelName] = channel.site_id.split('#') - - return { - filterParameters: { - day: date.date(), - time: 0, - minute: 0, - month: date.month() + 1, - year: date.year(), - offSetValue: 0, - filtersScreenFilters: [''], - isHd: '', - isChannelDetails: 'Y', - channelNum, - channelName: channelName.replace('&', '&') - } - } - } - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.description, - rating: parseRating(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseRating(item) { - return item.rating - ? { - system: 'MPA', - value: item.rating - } - : null -} - -function parseStart(item) { - return dayjs.tz(item.startTimeString, 'M/D/YYYY h:mm:ss A', 'America/Montevideo') -} - -function parseStop(item) { - return dayjs.tz(item.endTimeString, 'M/D/YYYY h:mm:ss A', 'America/Montevideo') -} - -function parseItems(content, channel) { - if (!content) return [] - let [ChannelNumber, ChannelName] = channel.site_id.split('#') - ChannelName = ChannelName.replace('&', '&') - const data = JSON.parse(content) - if (!data || !Array.isArray(data.d)) return [] - const channelData = data.d.find( - c => c.ChannelNumber == ChannelNumber && c.ChannelName === ChannelName - ) - - return channelData && Array.isArray(channelData.ProgramList) ? channelData.ProgramList : [] -} +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: 'directv.com.uy', + days: 2, + url: 'https://www.directv.com.uy/guia/ChannelDetail.aspx/GetProgramming', + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + Cookie: 'PGCSS=16384; PGLang=S; PGCulture=es-UY;' + }, + data({ channel, date }) { + const [channelNum, channelName] = channel.site_id.split('#') + + return { + filterParameters: { + day: date.date(), + time: 0, + minute: 0, + month: date.month() + 1, + year: date.year(), + offSetValue: 0, + filtersScreenFilters: [''], + isHd: '', + isChannelDetails: 'Y', + channelNum, + channelName: channelName.replace('&', '&') + } + } + } + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.description, + rating: parseRating(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseRating(item) { + return item.rating + ? { + system: 'MPA', + value: item.rating + } + : null +} + +function parseStart(item) { + return dayjs.tz(item.startTimeString, 'M/D/YYYY h:mm:ss A', 'America/Montevideo') +} + +function parseStop(item) { + return dayjs.tz(item.endTimeString, 'M/D/YYYY h:mm:ss A', 'America/Montevideo') +} + +function parseItems(content, channel) { + if (!content) return [] + let [ChannelNumber, ChannelName] = channel.site_id.split('#') + ChannelName = ChannelName.replace('&', '&') + const data = JSON.parse(content) + if (!data || !Array.isArray(data.d)) return [] + const channelData = data.d.find( + c => c.ChannelNumber == ChannelNumber && c.ChannelName === ChannelName + ) + + return channelData && Array.isArray(channelData.ProgramList) ? channelData.ProgramList : [] +} diff --git a/sites/directv.com.uy/directv.com.uy.test.js b/sites/directv.com.uy/directv.com.uy.test.js index 8ee6a26d..3d1634d4 100644 --- a/sites/directv.com.uy/directv.com.uy.test.js +++ b/sites/directv.com.uy/directv.com.uy.test.js @@ -1,76 +1,76 @@ -const { parser, url, request } = require('./directv.com.uy.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('2022-08-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '184#VTV', - xmltv_id: 'VTV.uy' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.directv.com.uy/guia/ChannelDetail.aspx/GetProgramming') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/json; charset=UTF-8', - Cookie: 'PGCSS=16384; PGLang=S; PGCulture=es-UY;' - }) -}) - -it('can generate valid request data', () => { - expect(request.data({ channel, date })).toMatchObject({ - filterParameters: { - day: 29, - time: 0, - minute: 0, - month: 8, - year: 2022, - offSetValue: 0, - filtersScreenFilters: [''], - isHd: '', - isChannelDetails: 'Y', - channelNum: '184', - channelName: 'VTV' - } - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const results = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-08-29T03:00:00.000Z', - stop: '2022-08-29T05:00:00.000Z', - title: 'Peñarol vs. Danubio : Fútbol Uruguayo Primera División - Peñarol vs. Danubio', - description: - 'Jornada 5 del Torneo Clausura 2022. Peñarol recibe a Danubio en el estadio Campeón del Siglo. Los carboneros llevan 3 partidos sin caer (2PG 1PE), mientras que los franjeados acumulan 6 juegos sin derrotas (4PG 2PE).', - rating: { - system: 'MPA', - value: 'NR' - } - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '', - channel - }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./directv.com.uy.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('2022-08-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '184#VTV', + xmltv_id: 'VTV.uy' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.directv.com.uy/guia/ChannelDetail.aspx/GetProgramming') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/json; charset=UTF-8', + Cookie: 'PGCSS=16384; PGLang=S; PGCulture=es-UY;' + }) +}) + +it('can generate valid request data', () => { + expect(request.data({ channel, date })).toMatchObject({ + filterParameters: { + day: 29, + time: 0, + minute: 0, + month: 8, + year: 2022, + offSetValue: 0, + filtersScreenFilters: [''], + isHd: '', + isChannelDetails: 'Y', + channelNum: '184', + channelName: 'VTV' + } + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-08-29T03:00:00.000Z', + stop: '2022-08-29T05:00:00.000Z', + title: 'Peñarol vs. Danubio : Fútbol Uruguayo Primera División - Peñarol vs. Danubio', + description: + 'Jornada 5 del Torneo Clausura 2022. Peñarol recibe a Danubio en el estadio Campeón del Siglo. Los carboneros llevan 3 partidos sin caer (2PG 1PE), mientras que los franjeados acumulan 6 juegos sin derrotas (4PG 2PE).', + rating: { + system: 'MPA', + value: 'NR' + } + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '', + channel + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/directv.com/directv.com.config.js b/sites/directv.com/directv.com.config.js index 5d8c924e..9eab9750 100644 --- a/sites/directv.com/directv.com.config.js +++ b/sites/directv.com/directv.com.config.js @@ -1,118 +1,118 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'directv.com', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - }, - headers: { - 'Accept-Language': 'en-US,en;q=0.5', - Connection: 'keep-alive' - } - }, - url({ date, channel }) { - const [channelId, childId] = channel.site_id.split('#') - return `https://www.directv.com/json/channelschedule?channels=${channelId}&startTime=${date.format()}&hours=24&chId=${childId}` - }, - async parser({ content, channel }) { - const programs = [] - const items = parseItems(content, channel) - for (let item of items) { - if (item.programID === '-1') continue - const detail = await loadProgramDetail(item.programID) - const start = parseStart(item) - const stop = start.add(item.duration, 'm') - programs.push({ - title: item.title, - sub_title: item.episodeTitle, - description: parseDescription(detail), - rating: parseRating(item), - date: parseYear(detail), - category: item.subcategoryList, - season: item.seasonNumber, - episode: item.episodeNumber, - image: parseImage(item), - start, - stop - }) - } - - return programs - }, - async channels() { - const codes = [10001] - - let channels = [] - for (let code of codes) { - const html = await axios - .get('https://www.directv.com/guide', { - headers: { - cookie: `dtve-prospect-zip=${code}` - } - }) - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(html) - const script = $('#dtvClientData').html() - const [, json] = script.match(/var dtvClientData = (.*);/) || [null, null] - const data = JSON.parse(json) - - data.guideData.channels.forEach(item => { - channels.push({ - lang: 'en', - site_id: item.chNum, - name: item.chName - }) - }) - } - - return channels - } -} - -function parseDescription(detail) { - return detail ? detail.description : null -} -function parseYear(detail) { - return detail ? detail.releaseYear : null -} -function parseRating(item) { - return item.rating - ? { - system: 'MPA', - value: item.rating - } - : null -} -function parseImage(item) { - return item.primaryImageUrl ? `https://www.directv.com${item.primaryImageUrl}` : null -} -function loadProgramDetail(programID) { - return axios - .get(`https://www.directv.com/json/program/flip/${programID}`) - .then(r => r.data) - .then(d => d.programDetail) - .catch(console.err) -} - -function parseStart(item) { - return dayjs.utc(item.airTime) -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data) return [] - if (!Array.isArray(data.schedule)) return [] - - const [, childId] = channel.site_id.split('#') - const channelData = data.schedule.find(i => i.chId == childId) - return channelData.schedules && Array.isArray(channelData.schedules) ? channelData.schedules : [] -} +const cheerio = require('cheerio') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'directv.com', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + }, + headers: { + 'Accept-Language': 'en-US,en;q=0.5', + Connection: 'keep-alive' + } + }, + url({ date, channel }) { + const [channelId, childId] = channel.site_id.split('#') + return `https://www.directv.com/json/channelschedule?channels=${channelId}&startTime=${date.format()}&hours=24&chId=${childId}` + }, + async parser({ content, channel }) { + const programs = [] + const items = parseItems(content, channel) + for (let item of items) { + if (item.programID === '-1') continue + const detail = await loadProgramDetail(item.programID) + const start = parseStart(item) + const stop = start.add(item.duration, 'm') + programs.push({ + title: item.title, + sub_title: item.episodeTitle, + description: parseDescription(detail), + rating: parseRating(item), + date: parseYear(detail), + category: item.subcategoryList, + season: item.seasonNumber, + episode: item.episodeNumber, + image: parseImage(item), + start, + stop + }) + } + + return programs + }, + async channels() { + const codes = [10001] + + let channels = [] + for (let code of codes) { + const html = await axios + .get('https://www.directv.com/guide', { + headers: { + cookie: `dtve-prospect-zip=${code}` + } + }) + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(html) + const script = $('#dtvClientData').html() + const [, json] = script.match(/var dtvClientData = (.*);/) || [null, null] + const data = JSON.parse(json) + + data.guideData.channels.forEach(item => { + channels.push({ + lang: 'en', + site_id: item.chNum, + name: item.chName + }) + }) + } + + return channels + } +} + +function parseDescription(detail) { + return detail ? detail.description : null +} +function parseYear(detail) { + return detail ? detail.releaseYear : null +} +function parseRating(item) { + return item.rating + ? { + system: 'MPA', + value: item.rating + } + : null +} +function parseImage(item) { + return item.primaryImageUrl ? `https://www.directv.com${item.primaryImageUrl}` : null +} +function loadProgramDetail(programID) { + return axios + .get(`https://www.directv.com/json/program/flip/${programID}`) + .then(r => r.data) + .then(d => d.programDetail) + .catch(console.err) +} + +function parseStart(item) { + return dayjs.utc(item.airTime) +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data) return [] + if (!Array.isArray(data.schedule)) return [] + + const [, childId] = channel.site_id.split('#') + const channelData = data.schedule.find(i => i.chId == childId) + return channelData.schedules && Array.isArray(channelData.schedules) ? channelData.schedules : [] +} diff --git a/sites/directv.com/directv.com.test.js b/sites/directv.com/directv.com.test.js index a4abfa0c..6684a18d 100644 --- a/sites/directv.com/directv.com.test.js +++ b/sites/directv.com/directv.com.test.js @@ -1,96 +1,96 @@ -const { parser, url } = require('./directv.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('2023-01-15', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '249#249', - xmltv_id: 'ComedyCentralEast.us' -} - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe( - 'https://www.directv.com/json/channelschedule?channels=249&startTime=2023-01-15T00:00:00Z&hours=24&chId=249' - ) -}) - -it('can parse response', done => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - - axios.get.mockImplementation(url => { - if (url === 'https://www.directv.com/json/program/flip/MV001173520000') { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) - }) - } else if (url === 'https://www.directv.com/json/program/flip/EP002298270445') { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - parser({ content, channel }) - .then(result => { - result = result.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2023-01-14T23:00:00.000Z', - stop: '2023-01-15T01:00:00.000Z', - title: 'Men in Black II', - description: - 'Kay (Tommy Lee Jones) and Jay (Will Smith) reunite to provide our best line of defense against a seductress who levels the toughest challenge yet to the MIBs mission statement: protecting the earth from the scum of the universe. While investigating a routine crime, Jay uncovers a plot masterminded by Serleena (Boyle), a Kylothian monster who disguises herself as a lingerie model. When Serleena takes the MIB building hostage, there is only one person Jay can turn to -- his former MIB partner.', - date: '2002', - image: 'https://www.directv.com/db_photos/movies/AllPhotosAPGI/29160/29160_aa.jpg', - category: ['Comedy', 'Movies Anywhere', 'Action/Adventure', 'Science Fiction'], - rating: { - system: 'MPA', - value: 'TV14' - } - }, - { - start: '2023-01-15T06:00:00.000Z', - stop: '2023-01-15T06:30:00.000Z', - title: 'South Park', - sub_title: 'Goth Kids 3: Dawn of the Posers', - description: 'The goth kids are sent to a camp for troubled children.', - image: - 'https://www.directv.com/db_photos/showcards/v5/AllPhotos/184338/p184338_b_v5_aa.jpg', - category: ['Series', 'Animation', 'Comedy'], - season: 17, - episode: 4, - rating: { - system: 'MPA', - value: 'TVMA' - } - } - ]) - done() - }) - .catch(done) -}) - -it('can handle empty guide', done => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json')) - parser({ content, channel }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(done) -}) +const { parser, url } = require('./directv.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('2023-01-15', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '249#249', + xmltv_id: 'ComedyCentralEast.us' +} + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe( + 'https://www.directv.com/json/channelschedule?channels=249&startTime=2023-01-15T00:00:00Z&hours=24&chId=249' + ) +}) + +it('can parse response', done => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + + axios.get.mockImplementation(url => { + if (url === 'https://www.directv.com/json/program/flip/MV001173520000') { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) + }) + } else if (url === 'https://www.directv.com/json/program/flip/EP002298270445') { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + parser({ content, channel }) + .then(result => { + result = result.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2023-01-14T23:00:00.000Z', + stop: '2023-01-15T01:00:00.000Z', + title: 'Men in Black II', + description: + 'Kay (Tommy Lee Jones) and Jay (Will Smith) reunite to provide our best line of defense against a seductress who levels the toughest challenge yet to the MIBs mission statement: protecting the earth from the scum of the universe. While investigating a routine crime, Jay uncovers a plot masterminded by Serleena (Boyle), a Kylothian monster who disguises herself as a lingerie model. When Serleena takes the MIB building hostage, there is only one person Jay can turn to -- his former MIB partner.', + date: '2002', + image: 'https://www.directv.com/db_photos/movies/AllPhotosAPGI/29160/29160_aa.jpg', + category: ['Comedy', 'Movies Anywhere', 'Action/Adventure', 'Science Fiction'], + rating: { + system: 'MPA', + value: 'TV14' + } + }, + { + start: '2023-01-15T06:00:00.000Z', + stop: '2023-01-15T06:30:00.000Z', + title: 'South Park', + sub_title: 'Goth Kids 3: Dawn of the Posers', + description: 'The goth kids are sent to a camp for troubled children.', + image: + 'https://www.directv.com/db_photos/showcards/v5/AllPhotos/184338/p184338_b_v5_aa.jpg', + category: ['Series', 'Animation', 'Comedy'], + season: 17, + episode: 4, + rating: { + system: 'MPA', + value: 'TVMA' + } + } + ]) + done() + }) + .catch(done) +}) + +it('can handle empty guide', done => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json')) + parser({ content, channel }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(done) +}) diff --git a/sites/dishtv.in/dishtv.in.config.js b/sites/dishtv.in/dishtv.in.config.js index 250aa3b8..b803e87c 100644 --- a/sites/dishtv.in/dishtv.in.config.js +++ b/sites/dishtv.in/dishtv.in.config.js @@ -1,167 +1,167 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -let authToken - -module.exports = { - site: 'dishtv.in', - days: 2, - url: 'https://epg.mysmartstick.com/dishtv/api/v1/epg/entities/programs', - request: { - method: 'POST', - async headers() { - await fetchToken() - - return { - Authorization: authToken - } - }, - data({ channel, date }) { - return { - allowPastEvents: true, - channelid: channel.site_id, - date: date.format('DD/MM/YYYY') - } - } - }, - parser: ({ content }) => { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: parseTitle(item), - description: parseDescription(item), - category: parseCategory(item), - actors: item.credits.actors, - directors: item.credits.directors, - producers: item.credits.producers, - date: item.productionyear, - icon: parseIcon(item), - image: parseImage(item), - episode: parseEpisode(item), - start: dayjs(item.start), - stop: dayjs(item.stop) - }) - }) - - return programs - }, - async channels() { - await fetchToken() - - const totalPages = await fetchPages() - - const queue = Array.from(Array(totalPages).keys()).map(i => { - const data = new FormData() - data.append('pageNum', i + 1) - - return { - method: 'post', - url: 'https://www.dishtv.in/services/epg/channels', - data, - headers: { - 'authorization-token': authToken - } - } - }) - - const channels = [] - for (let item of queue) { - const data = await axios(item) - .then(r => r.data) - .catch(console.error) - - data.programDetailsByChannel.forEach(channel => { - channels.push({ - lang: 'en', - site_id: channel.channelid, - name: channel.channelname - }) - }) - } - - return channels - } -} - -function parseTitle(item) { - return Object.values(item.regional) - .map(region => ({ - lang: region.languagecode, - value: region.title - })) - .filter(i => Boolean(i.value)) -} - -function parseDescription(item) { - return Object.values(item.regional) - .map(region => ({ - lang: region.languagecode, - value: region.desc - })) - .filter(i => Boolean(i.value)) -} - -function parseCategory(item) { - return Object.values(item.regional) - .map(region => ({ - lang: region.languagecode, - value: region.genre - })) - .filter(i => Boolean(i.value)) -} - -function parseEpisode(item) { - return item['episode-num'] ? parseInt(item['episode-num']) : null -} - -function parseIcon(item) { - return item.programmeurl || null -} - -function parseImage(item) { - return item?.images?.landscape?.['1280x720'] ? item.images.landscape['1280x720'] : null -} - -function parseItems(content) { - try { - const data = JSON.parse(content) - - return Array.isArray(data) ? data : [] - } catch { - return [] - } -} - -async function fetchToken() { - if (authToken) return - - const data = await axios - .post('https://www.dishtv.in/services/epg/signin', null, { - headers: { - 'sec-fetch-dest': 'empty', - 'sec-fetch-mode': 'cors', - 'sec-fetch-site': 'same-origin', - 'x-requested-with': 'XMLHttpRequest', - Referer: 'https://www.dishtv.in/channel-guide.html' - } - }) - .then(r => r.data) - .catch(console.error) - - authToken = data.token -} - -async function fetchPages() { - const formData = new FormData() - formData.append('pageNum', 1) - - const data = await axios - .post('https://www.dishtv.in/services/epg/channels', formData, { - headers: { 'authorization-token': authToken } - }) - .then(r => r.data) - .catch(console.error) - - return data.totalPages ? parseInt(data.totalPages) : 0 -} +const axios = require('axios') +const dayjs = require('dayjs') + +let authToken + +module.exports = { + site: 'dishtv.in', + days: 2, + url: 'https://epg.mysmartstick.com/dishtv/api/v1/epg/entities/programs', + request: { + method: 'POST', + async headers() { + await fetchToken() + + return { + Authorization: authToken + } + }, + data({ channel, date }) { + return { + allowPastEvents: true, + channelid: channel.site_id, + date: date.format('DD/MM/YYYY') + } + } + }, + parser: ({ content }) => { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: parseTitle(item), + description: parseDescription(item), + category: parseCategory(item), + actors: item.credits.actors, + directors: item.credits.directors, + producers: item.credits.producers, + date: item.productionyear, + icon: parseIcon(item), + image: parseImage(item), + episode: parseEpisode(item), + start: dayjs(item.start), + stop: dayjs(item.stop) + }) + }) + + return programs + }, + async channels() { + await fetchToken() + + const totalPages = await fetchPages() + + const queue = Array.from(Array(totalPages).keys()).map(i => { + const data = new FormData() + data.append('pageNum', i + 1) + + return { + method: 'post', + url: 'https://www.dishtv.in/services/epg/channels', + data, + headers: { + 'authorization-token': authToken + } + } + }) + + const channels = [] + for (let item of queue) { + const data = await axios(item) + .then(r => r.data) + .catch(console.error) + + data.programDetailsByChannel.forEach(channel => { + channels.push({ + lang: 'en', + site_id: channel.channelid, + name: channel.channelname + }) + }) + } + + return channels + } +} + +function parseTitle(item) { + return Object.values(item.regional) + .map(region => ({ + lang: region.languagecode, + value: region.title + })) + .filter(i => Boolean(i.value)) +} + +function parseDescription(item) { + return Object.values(item.regional) + .map(region => ({ + lang: region.languagecode, + value: region.desc + })) + .filter(i => Boolean(i.value)) +} + +function parseCategory(item) { + return Object.values(item.regional) + .map(region => ({ + lang: region.languagecode, + value: region.genre + })) + .filter(i => Boolean(i.value)) +} + +function parseEpisode(item) { + return item['episode-num'] ? parseInt(item['episode-num']) : null +} + +function parseIcon(item) { + return item.programmeurl || null +} + +function parseImage(item) { + return item?.images?.landscape?.['1280x720'] ? item.images.landscape['1280x720'] : null +} + +function parseItems(content) { + try { + const data = JSON.parse(content) + + return Array.isArray(data) ? data : [] + } catch { + return [] + } +} + +async function fetchToken() { + if (authToken) return + + const data = await axios + .post('https://www.dishtv.in/services/epg/signin', null, { + headers: { + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-origin', + 'x-requested-with': 'XMLHttpRequest', + Referer: 'https://www.dishtv.in/channel-guide.html' + } + }) + .then(r => r.data) + .catch(console.error) + + authToken = data.token +} + +async function fetchPages() { + const formData = new FormData() + formData.append('pageNum', 1) + + const data = await axios + .post('https://www.dishtv.in/services/epg/channels', formData, { + headers: { 'authorization-token': authToken } + }) + .then(r => r.data) + .catch(console.error) + + return data.totalPages ? parseInt(data.totalPages) : 0 +} diff --git a/sites/dishtv.in/dishtv.in.test.js b/sites/dishtv.in/dishtv.in.test.js index 2137d72e..fd8eddac 100644 --- a/sites/dishtv.in/dishtv.in.test.js +++ b/sites/dishtv.in/dishtv.in.test.js @@ -1,140 +1,140 @@ -const { parser, url, request } = require('./dishtv.in.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') - -axios.post.mockImplementation((url, data, params) => { - if ( - url === 'https://www.dishtv.in/services/epg/signin' && - data === null && - JSON.stringify(params) === - JSON.stringify({ - headers: { - 'sec-fetch-dest': 'empty', - 'sec-fetch-mode': 'cors', - 'sec-fetch-site': 'same-origin', - 'x-requested-with': 'XMLHttpRequest', - Referer: 'https://www.dishtv.in/channel-guide.html' - } - }) - ) { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/session.json')) - - return Promise.resolve({ - data: JSON.parse(content) - }) - } else { - return Promise.resolve({ - data: '' - }) - } -}) - -const date = dayjs.utc('2025-01-26', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: '142639', xmltv_id: 'AndpriveHD.in' } - -it('can generate valid url', () => { - expect(url).toBe('https://epg.mysmartstick.com/dishtv/api/v1/epg/entities/programs') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', async () => { - expect(await request.headers()).toMatchObject({ - Authorization: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRpZCI6ImRpc2h0di13ZWJzaXRlIiwicGxhdGZvcm0iOiJkaXNodHYiLCJpYXQiOjE3Mzc2ODIxNjEsImV4cCI6MTczNzc2ODU2MX0.sPrYfodVTbf1kJ-wGICDlnH-Yt3J0-mB-M2YROU8v2Q' - }) -}) - -it('can generate valid request data', () => { - expect(request.data({ channel, date })).toMatchObject({ - allowPastEvents: true, - channelid: '142639', - date: '26/01/2025' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(16) - expect(results[0]).toMatchObject({ - start: '2025-01-26T00:30:00.000Z', - stop: '2025-01-26T02:05:00.000Z', - title: [ - { lang: 'en', value: 'Train to Busan 2: Peninsula' }, - { lang: 'hi', value: 'ट्रेन टू बुसान 2: पेनीनसुला' }, - { lang: 'ta', value: 'ட்ரெயின் டு பூசன் ப்ரெசென்ட்ஸ்: பெனின்சுலா' }, - { lang: 'te', value: 'ట్రేన్ టు బూసాన్ ప్రజెంట్స్: పెనిన్సులా' } - ], - description: [ - { - lang: 'en', - value: - 'Jung Seok, a former soldier, along with his teammates, sets out on a mission to battle hordes of post-apocalyptic zombies in the Korean peninsula wastelands.' - }, - { - lang: 'hi', - value: - 'एक भूतपूर्व सैनिक जंग सोक अपने साथियों के साथ कोरियाई प्रायद्वीप के बंजर इलाकों में सर्वनाश के बाद की जोंबी से लड़ने के मिशन पर निकलता है।' - }, - { - lang: 'ta', - value: - 'கொரிய தீபகற்பத்தின் தரிசு நிலங்களில் அபோகாலிப்டிக் ஜாம்பிக்களின் கூட்டத்தை எதிர்த்து தன் குழுவுடன் போரிடும் ஜங் சியோக்.' - }, - { - lang: 'te', - value: - 'మాజీ సైనికుడు జంగ్ సియోక్ తన సహచరులతో కలిసి కొరియా ద్వీపకల్పంలో పోస్ట్-అపోకలిప్టిక్ జాంబీలతో యుద్దానికి సిద్దమవుతాడు.' - } - ], - category: [ - { lang: 'en', value: 'Film' }, - { lang: 'hi', value: 'फ़िल्म' }, - { lang: 'ta', value: '??????????' }, - { lang: 'te', value: 'సినిమా' }, - { lang: 'mr', value: 'चित्रपट' } - ], - actors: [ - 'Gang Dong-won', - 'Lee Jung-hyun', - 'Lee Re', - 'Kwon Hae-hyo', - 'John D. Michaels', - 'Kim Min-jae', - 'Kim Doyun', - 'Lee Ye-won', - 'Daniel Joey Albright', - 'Pierce Conran', - 'Geoffrey Giuliano', - 'Milan-Devi LaBrey' - ], - producers: [], - directors: ['Yeon Sang-ho'], - icon: 'https://dtil.tmsimg.com/assets/p17850257_v_h9_al.jpg?lock=880x660', - image: 'https://dtil.tmsimg.com/assets/p17850257_v_h8_am.jpg?lock=1280x720', - date: '2020' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ content: '[]' }) - - expect(results).toMatchObject([]) -}) +const { parser, url, request } = require('./dishtv.in.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') + +axios.post.mockImplementation((url, data, params) => { + if ( + url === 'https://www.dishtv.in/services/epg/signin' && + data === null && + JSON.stringify(params) === + JSON.stringify({ + headers: { + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-origin', + 'x-requested-with': 'XMLHttpRequest', + Referer: 'https://www.dishtv.in/channel-guide.html' + } + }) + ) { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/session.json')) + + return Promise.resolve({ + data: JSON.parse(content) + }) + } else { + return Promise.resolve({ + data: '' + }) + } +}) + +const date = dayjs.utc('2025-01-26', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: '142639', xmltv_id: 'AndpriveHD.in' } + +it('can generate valid url', () => { + expect(url).toBe('https://epg.mysmartstick.com/dishtv/api/v1/epg/entities/programs') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', async () => { + expect(await request.headers()).toMatchObject({ + Authorization: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRpZCI6ImRpc2h0di13ZWJzaXRlIiwicGxhdGZvcm0iOiJkaXNodHYiLCJpYXQiOjE3Mzc2ODIxNjEsImV4cCI6MTczNzc2ODU2MX0.sPrYfodVTbf1kJ-wGICDlnH-Yt3J0-mB-M2YROU8v2Q' + }) +}) + +it('can generate valid request data', () => { + expect(request.data({ channel, date })).toMatchObject({ + allowPastEvents: true, + channelid: '142639', + date: '26/01/2025' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(16) + expect(results[0]).toMatchObject({ + start: '2025-01-26T00:30:00.000Z', + stop: '2025-01-26T02:05:00.000Z', + title: [ + { lang: 'en', value: 'Train to Busan 2: Peninsula' }, + { lang: 'hi', value: 'ट्रेन टू बुसान 2: पेनीनसुला' }, + { lang: 'ta', value: 'ட்ரெயின் டு பூசன் ப்ரெசென்ட்ஸ்: பெனின்சுலா' }, + { lang: 'te', value: 'ట్రేన్ టు బూసాన్ ప్రజెంట్స్: పెనిన్సులా' } + ], + description: [ + { + lang: 'en', + value: + 'Jung Seok, a former soldier, along with his teammates, sets out on a mission to battle hordes of post-apocalyptic zombies in the Korean peninsula wastelands.' + }, + { + lang: 'hi', + value: + 'एक भूतपूर्व सैनिक जंग सोक अपने साथियों के साथ कोरियाई प्रायद्वीप के बंजर इलाकों में सर्वनाश के बाद की जोंबी से लड़ने के मिशन पर निकलता है।' + }, + { + lang: 'ta', + value: + 'கொரிய தீபகற்பத்தின் தரிசு நிலங்களில் அபோகாலிப்டிக் ஜாம்பிக்களின் கூட்டத்தை எதிர்த்து தன் குழுவுடன் போரிடும் ஜங் சியோக்.' + }, + { + lang: 'te', + value: + 'మాజీ సైనికుడు జంగ్ సియోక్ తన సహచరులతో కలిసి కొరియా ద్వీపకల్పంలో పోస్ట్-అపోకలిప్టిక్ జాంబీలతో యుద్దానికి సిద్దమవుతాడు.' + } + ], + category: [ + { lang: 'en', value: 'Film' }, + { lang: 'hi', value: 'फ़िल्म' }, + { lang: 'ta', value: '??????????' }, + { lang: 'te', value: 'సినిమా' }, + { lang: 'mr', value: 'चित्रपट' } + ], + actors: [ + 'Gang Dong-won', + 'Lee Jung-hyun', + 'Lee Re', + 'Kwon Hae-hyo', + 'John D. Michaels', + 'Kim Min-jae', + 'Kim Doyun', + 'Lee Ye-won', + 'Daniel Joey Albright', + 'Pierce Conran', + 'Geoffrey Giuliano', + 'Milan-Devi LaBrey' + ], + producers: [], + directors: ['Yeon Sang-ho'], + icon: 'https://dtil.tmsimg.com/assets/p17850257_v_h9_al.jpg?lock=880x660', + image: 'https://dtil.tmsimg.com/assets/p17850257_v_h8_am.jpg?lock=1280x720', + date: '2020' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ content: '[]' }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/dna.fi/dna.fi.config.js b/sites/dna.fi/dna.fi.config.js index 228254a2..dae07a22 100644 --- a/sites/dna.fi/dna.fi.config.js +++ b/sites/dna.fi/dna.fi.config.js @@ -1,99 +1,99 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'dna.fi', - days: 2, - url({ date, channel }) { - const beginTimestamp = date.add(2, 'h').valueOf() - const endTimestamp = date.add(1, 'd').add(2, 'h').subtract(1, 's').valueOf() - - return `https://mts-pro-envoy-vip.dna.fi/hbx/api/pub/xrtv/g/media?q=channel:${channel.site_id}&q=profile:pr&q=start-interval:${beginTimestamp}/${endTimestamp}` - }, - parser({ content, date }) { - let programs = [] - let items = parseItems(content, date) - items.forEach(item => { - const data = item?._embedded?.['xrtv:meta']?.data - programs.push({ - title: data?.title, - subtitle: data?.episode_title, - description: data?.description, - season: data?.season_number, - episode: data?.episode_number, - date: data?.year, - categories: parseCategories(item), - rating: parseRating(data), - images: parseImages(item), - directors: parseCast(data, 'director'), - actors: parseCast(data, 'actors'), - start: dayjs(data?.start), - stop: dayjs(data?.end) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://mts-pro-envoy-vip.dna.fi/hbx/api/pub/xrtv/g/media?q=profile:ch&limit=1000') - .then(r => r.data) - .catch(console.error) - - return data._embedded['xrtv:media-item'].map(c => { - return { - lang: 'fi', - site_id: c.datalistTerm, - name: c.name - } - }) - } -} - -function parseCast(data, role) { - if (!data[role] || !data[role].value) return [] - - return data[role].value.split(', ').map(name => ({ - lang: data[role].lang, - value: name - })) -} - -function parseCategories(item) { - const categories = item?._embedded?.['xrtv:media-category'] - - return Array.isArray(categories) ? categories.map(category => category.name) : [] -} - -function parseRating(data) { - if (!data.age_rating) return null - - return { - system: 'VET', - value: data.age_rating - } -} - -function parseImages(item) { - const images = item?._embedded?.['xrtv:image'] - - return Array.isArray(images) ? images.map(image => image.src) : [] -} - -function parseItems(content, date) { - try { - const data = JSON.parse(content) - let items = data?._embedded?.['xrtv:media-item'] - items = Array.isArray(items) ? items : [] - items = items.filter(item => { - const start = item?._embedded?.['xrtv:meta']?.data?.start - if (!start) return false - - return date.isSame(dayjs(start), 'day') - }) - - return items - } catch { - return [] - } -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'dna.fi', + days: 2, + url({ date, channel }) { + const beginTimestamp = date.add(2, 'h').valueOf() + const endTimestamp = date.add(1, 'd').add(2, 'h').subtract(1, 's').valueOf() + + return `https://mts-pro-envoy-vip.dna.fi/hbx/api/pub/xrtv/g/media?q=channel:${channel.site_id}&q=profile:pr&q=start-interval:${beginTimestamp}/${endTimestamp}` + }, + parser({ content, date }) { + let programs = [] + let items = parseItems(content, date) + items.forEach(item => { + const data = item?._embedded?.['xrtv:meta']?.data + programs.push({ + title: data?.title, + subtitle: data?.episode_title, + description: data?.description, + season: data?.season_number, + episode: data?.episode_number, + date: data?.year, + categories: parseCategories(item), + rating: parseRating(data), + images: parseImages(item), + directors: parseCast(data, 'director'), + actors: parseCast(data, 'actors'), + start: dayjs(data?.start), + stop: dayjs(data?.end) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://mts-pro-envoy-vip.dna.fi/hbx/api/pub/xrtv/g/media?q=profile:ch&limit=1000') + .then(r => r.data) + .catch(console.error) + + return data._embedded['xrtv:media-item'].map(c => { + return { + lang: 'fi', + site_id: c.datalistTerm, + name: c.name + } + }) + } +} + +function parseCast(data, role) { + if (!data[role] || !data[role].value) return [] + + return data[role].value.split(', ').map(name => ({ + lang: data[role].lang, + value: name + })) +} + +function parseCategories(item) { + const categories = item?._embedded?.['xrtv:media-category'] + + return Array.isArray(categories) ? categories.map(category => category.name) : [] +} + +function parseRating(data) { + if (!data.age_rating) return null + + return { + system: 'VET', + value: data.age_rating + } +} + +function parseImages(item) { + const images = item?._embedded?.['xrtv:image'] + + return Array.isArray(images) ? images.map(image => image.src) : [] +} + +function parseItems(content, date) { + try { + const data = JSON.parse(content) + let items = data?._embedded?.['xrtv:media-item'] + items = Array.isArray(items) ? items : [] + items = items.filter(item => { + const start = item?._embedded?.['xrtv:meta']?.data?.start + if (!start) return false + + return date.isSame(dayjs(start), 'day') + }) + + return items + } catch { + return [] + } +} diff --git a/sites/dna.fi/dna.fi.test.js b/sites/dna.fi/dna.fi.test.js index 9373e594..d4420f03 100644 --- a/sites/dna.fi/dna.fi.test.js +++ b/sites/dna.fi/dna.fi.test.js @@ -1,138 +1,138 @@ -const { parser, url } = require('./dna.fi.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-15', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'ch-216356', - xmltv_id: 'MTV3.fi' -} - -it('can generate valid url', async () => { - expect(url({ date, channel })).toBe( - 'https://mts-pro-envoy-vip.dna.fi/hbx/api/pub/xrtv/g/media?q=channel:ch-216356&q=profile:pr&q=start-interval:1736906400000/1736992799000' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') - let results = parser({ date, content }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(20) - expect(results[0]).toMatchObject({ - start: '2025-01-15T02:30:00.000Z', - stop: '2025-01-15T03:22:00.000Z', - title: { - lang: 'fi', - value: 'Next Level Chef' - }, - subtitle: { - lang: 'fi', - value: 'Brunssi' - }, - season: 1, - episode: 6, - rating: { - system: 'VET', - value: 'S' - }, - date: '2022', - images: [ - 'https://mts-pro-cache-vip.dna.fi/meme/v2/37f/3851073346622580374_aspect_ratio_16_9_1.jpg' - ], - description: { - lang: 'fi', - value: - 'Kausi 1, 6/11. Brunssi. Päivän haasteessa valmistetaan rentoa brunssiruokaa. Yksi kilpailija tekee valtaisan virheen myöhästyessään annosten luovutuksesta. Amerikkalainen tosi-tv-sarja.' - }, - categories: ['Reality TV', 'Entertainment', 'TV Show', 'Next Level Chef', 'Series 1'] - }) - expect(results[5]).toMatchObject({ - title: { - lang: 'fi', - value: 'Kauniit ja rohkeat (S)' - }, - subtitle: { - lang: 'fi', - value: 'Parantava syleily' - }, - start: '2025-01-15T08:30:00.000Z', - stop: '2025-01-15T09:00:00.000Z', - season: 37, - episode: 9380, - rating: { - system: 'VET', - value: 'S' - }, - date: '2023', - images: [ - 'https://mts-pro-cache-vip.dna.fi/meme/v2/79e/6509488401145439178_aspect_ratio_16_9_1.jpg' - ], - description: { - lang: 'fi', - value: - 'Steffy on vähällä yllättää Hopen ja Carterin kesken herkän hetken. Ridgen kannustamana Taylor suostuu kokeilemaan Shandran parannusmenetelmää, ja pitkään padotut tunteet saavat viimein vapautua.' - }, - categories: [ - 'Soap', - 'Drama', - 'Romance', - 'Series', - 'TV Show', - 'The Bold and the Beautiful', - 'Series 37' - ], - actors: [{ lang: 'en', value: 'Katherine Kelly Lang' }] - }) - expect(results[19]).toMatchObject({ - start: '2025-01-15T16:30:00.000Z', - stop: '2025-01-15T17:00:00.000Z', - title: { - lang: 'fi', - value: 'Emmerdale (S)' - }, - subtitle: { - lang: 'fi', - value: 'Epäilyksen varjossa' - }, - season: 54, - episode: 9845, - rating: { - system: 'VET', - value: 'S' - }, - date: '2023', - images: [ - 'https://mts-pro-cache-vip.dna.fi/meme/v2/5e8/5978592001161112833_aspect_ratio_16_9_1.jpg' - ], - description: { - lang: 'fi', - value: - 'Caleb haistaa palaneen käryä Craigin kuolemaan liittyen. Mackenzien yllätysvierailu antaa vahvistuksen Chloen päätökselle. Lydia pohtii, pitäisikö hänen mennä Craigin hautajaisiin. Dawnin supistukset säikäyttävät Rhonan.' - }, - categories: ['Soap', 'Drama', 'Romance', 'Series', 'TV Show', 'Emmerdale', 'Series 54'], - directors: [ - { lang: 'en', value: 'Ian Bevitt' }, - { lang: 'en', value: 'Munir Malik' } - ] - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - content: '' - }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./dna.fi.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-15', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'ch-216356', + xmltv_id: 'MTV3.fi' +} + +it('can generate valid url', async () => { + expect(url({ date, channel })).toBe( + 'https://mts-pro-envoy-vip.dna.fi/hbx/api/pub/xrtv/g/media?q=channel:ch-216356&q=profile:pr&q=start-interval:1736906400000/1736992799000' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') + let results = parser({ date, content }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(20) + expect(results[0]).toMatchObject({ + start: '2025-01-15T02:30:00.000Z', + stop: '2025-01-15T03:22:00.000Z', + title: { + lang: 'fi', + value: 'Next Level Chef' + }, + subtitle: { + lang: 'fi', + value: 'Brunssi' + }, + season: 1, + episode: 6, + rating: { + system: 'VET', + value: 'S' + }, + date: '2022', + images: [ + 'https://mts-pro-cache-vip.dna.fi/meme/v2/37f/3851073346622580374_aspect_ratio_16_9_1.jpg' + ], + description: { + lang: 'fi', + value: + 'Kausi 1, 6/11. Brunssi. Päivän haasteessa valmistetaan rentoa brunssiruokaa. Yksi kilpailija tekee valtaisan virheen myöhästyessään annosten luovutuksesta. Amerikkalainen tosi-tv-sarja.' + }, + categories: ['Reality TV', 'Entertainment', 'TV Show', 'Next Level Chef', 'Series 1'] + }) + expect(results[5]).toMatchObject({ + title: { + lang: 'fi', + value: 'Kauniit ja rohkeat (S)' + }, + subtitle: { + lang: 'fi', + value: 'Parantava syleily' + }, + start: '2025-01-15T08:30:00.000Z', + stop: '2025-01-15T09:00:00.000Z', + season: 37, + episode: 9380, + rating: { + system: 'VET', + value: 'S' + }, + date: '2023', + images: [ + 'https://mts-pro-cache-vip.dna.fi/meme/v2/79e/6509488401145439178_aspect_ratio_16_9_1.jpg' + ], + description: { + lang: 'fi', + value: + 'Steffy on vähällä yllättää Hopen ja Carterin kesken herkän hetken. Ridgen kannustamana Taylor suostuu kokeilemaan Shandran parannusmenetelmää, ja pitkään padotut tunteet saavat viimein vapautua.' + }, + categories: [ + 'Soap', + 'Drama', + 'Romance', + 'Series', + 'TV Show', + 'The Bold and the Beautiful', + 'Series 37' + ], + actors: [{ lang: 'en', value: 'Katherine Kelly Lang' }] + }) + expect(results[19]).toMatchObject({ + start: '2025-01-15T16:30:00.000Z', + stop: '2025-01-15T17:00:00.000Z', + title: { + lang: 'fi', + value: 'Emmerdale (S)' + }, + subtitle: { + lang: 'fi', + value: 'Epäilyksen varjossa' + }, + season: 54, + episode: 9845, + rating: { + system: 'VET', + value: 'S' + }, + date: '2023', + images: [ + 'https://mts-pro-cache-vip.dna.fi/meme/v2/5e8/5978592001161112833_aspect_ratio_16_9_1.jpg' + ], + description: { + lang: 'fi', + value: + 'Caleb haistaa palaneen käryä Craigin kuolemaan liittyen. Mackenzien yllätysvierailu antaa vahvistuksen Chloen päätökselle. Lydia pohtii, pitäisikö hänen mennä Craigin hautajaisiin. Dawnin supistukset säikäyttävät Rhonan.' + }, + categories: ['Soap', 'Drama', 'Romance', 'Series', 'TV Show', 'Emmerdale', 'Series 54'], + directors: [ + { lang: 'en', value: 'Ian Bevitt' }, + { lang: 'en', value: 'Munir Malik' } + ] + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + content: '' + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/dsmart.com.tr/dsmart.com.tr.config.js b/sites/dsmart.com.tr/dsmart.com.tr.config.js index 4a188b62..c5a63a4d 100644 --- a/sites/dsmart.com.tr/dsmart.com.tr.config.js +++ b/sites/dsmart.com.tr/dsmart.com.tr.config.js @@ -1,130 +1,130 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const duration = require('dayjs/plugin/duration') -const doFetch = require('@ntlab/sfetch') -const debug = require('debug')('site:dsmart.com.tr') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) -dayjs.extend(duration) - -doFetch.setDebugger(debug) - -const channelsWithSchedule = true -const pageLimit = 10 -const caches = {} - -module.exports = { - site: 'dsmart.com.tr', - days: 2, - request: { - cache: { - ttl: 24 * 60 * 60 * 1000 // 1 day - } - }, - url({ date, page = 1 }) { - return `https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=${ - page - }&limit=${ - pageLimit - }&day=${ - date.format('YYYY-MM-DD') - }` - }, - async parser({ content, channel, date, useCache = true }) { - const programs = [] - if (content) { - if (typeof content === 'string') { - content = JSON.parse(content) - } - if (useCache) { - const cacheKey = date.format('YYYYMMDD') - // cache whole channels for the day - if (caches[cacheKey] === undefined) { - if (content?.data?.total) { - const queues = [] - const pages = Math.ceil(content.data.total / pageLimit) - for (let page = 2; page <= pages; page++) { - queues.push(module.exports.url({ date, page })) - } - await doFetch(queues, (url, res) => { - if (Array.isArray(res?.data?.channels)) { - content.data.channels.push(...res.data.channels) - } - }) - caches[cacheKey] = content - } - } else { - content = caches[cacheKey] - } - } - if (Array.isArray(content?.data?.channels)) { - content.data.channels - .filter(i => i._id === channel.site_id) - .forEach(i => { - if (i.schedule.length) { - let dayStart, ofs - programs.push(...i.schedule - .map(p => { - const baseDate = dayjs.utc(p.day) - const startDate = dayjs.utc(p.start_date) - // calculate base offset if needed - if (!dayStart) { - dayStart = startDate - ofs = dayjs.duration(dayjs.utc(`${p.day.substr(0, 11)}${p.start_date.substr(11)}`).diff(baseDate)) - .asSeconds() - } - const delta = dayjs.duration(startDate.diff(dayStart)).asSeconds() - // ignore days in duration - const [h, m, s] = (p.duration.includes(',') ? p.duration.split(',')[1].trim() : p.duration) - .split(':').map(Number) - const duration = (h * 3600) + (m * 60) + s - const start = baseDate.add(ofs + delta, 's') - const stop = start.add(duration, 's') - return { - title: p.program_name, - description: p.description, - category: p.genre && p.genre.includes('/') ? - p.genre.split('/').map(g => `${g.substr(0, 1).toUpperCase()}${g.substr(1)}`) : null, - start, - stop - } - }) - ) - } - }) - } - } - - return programs - }, - async channels() { - const channels = [] - const f = page => this.url({ date: dayjs(), page }) - let pages, page = 1 - const queues = [f(page)] - await doFetch(queues, (url, res) => { - if (!pages && res.data.total) { - pages = Math.ceil(res.data.total / pageLimit) - while (page < pages) { - queues.push(f(++page)) - } - } - if (Array.isArray(res?.data?.channels)) { - channels.push(...res.data.channels - .filter(i => (channelsWithSchedule && i.schedule.length) || !channelsWithSchedule) - .map(i => { - return { - lang: 'tr', - name: i.channel_name, - site_id: i._id - } - }) - ) - } - }) - - return channels - } -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const duration = require('dayjs/plugin/duration') +const doFetch = require('@ntlab/sfetch') +const debug = require('debug')('site:dsmart.com.tr') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) +dayjs.extend(duration) + +doFetch.setDebugger(debug) + +const channelsWithSchedule = true +const pageLimit = 10 +const caches = {} + +module.exports = { + site: 'dsmart.com.tr', + days: 2, + request: { + cache: { + ttl: 24 * 60 * 60 * 1000 // 1 day + } + }, + url({ date, page = 1 }) { + return `https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=${ + page + }&limit=${ + pageLimit + }&day=${ + date.format('YYYY-MM-DD') + }` + }, + async parser({ content, channel, date, useCache = true }) { + const programs = [] + if (content) { + if (typeof content === 'string') { + content = JSON.parse(content) + } + if (useCache) { + const cacheKey = date.format('YYYYMMDD') + // cache whole channels for the day + if (caches[cacheKey] === undefined) { + if (content?.data?.total) { + const queues = [] + const pages = Math.ceil(content.data.total / pageLimit) + for (let page = 2; page <= pages; page++) { + queues.push(module.exports.url({ date, page })) + } + await doFetch(queues, (url, res) => { + if (Array.isArray(res?.data?.channels)) { + content.data.channels.push(...res.data.channels) + } + }) + caches[cacheKey] = content + } + } else { + content = caches[cacheKey] + } + } + if (Array.isArray(content?.data?.channels)) { + content.data.channels + .filter(i => i._id === channel.site_id) + .forEach(i => { + if (i.schedule.length) { + let dayStart, ofs + programs.push(...i.schedule + .map(p => { + const baseDate = dayjs.utc(p.day) + const startDate = dayjs.utc(p.start_date) + // calculate base offset if needed + if (!dayStart) { + dayStart = startDate + ofs = dayjs.duration(dayjs.utc(`${p.day.substr(0, 11)}${p.start_date.substr(11)}`).diff(baseDate)) + .asSeconds() + } + const delta = dayjs.duration(startDate.diff(dayStart)).asSeconds() + // ignore days in duration + const [h, m, s] = (p.duration.includes(',') ? p.duration.split(',')[1].trim() : p.duration) + .split(':').map(Number) + const duration = (h * 3600) + (m * 60) + s + const start = baseDate.add(ofs + delta, 's') + const stop = start.add(duration, 's') + return { + title: p.program_name, + description: p.description, + category: p.genre && p.genre.includes('/') ? + p.genre.split('/').map(g => `${g.substr(0, 1).toUpperCase()}${g.substr(1)}`) : null, + start, + stop + } + }) + ) + } + }) + } + } + + return programs + }, + async channels() { + const channels = [] + const f = page => this.url({ date: dayjs(), page }) + let pages, page = 1 + const queues = [f(page)] + await doFetch(queues, (url, res) => { + if (!pages && res.data.total) { + pages = Math.ceil(res.data.total / pageLimit) + while (page < pages) { + queues.push(f(++page)) + } + } + if (Array.isArray(res?.data?.channels)) { + channels.push(...res.data.channels + .filter(i => (channelsWithSchedule && i.schedule.length) || !channelsWithSchedule) + .map(i => { + return { + lang: 'tr', + name: i.channel_name, + site_id: i._id + } + }) + ) + } + }) + + return channels + } +} diff --git a/sites/dsmart.com.tr/dsmart.com.tr.test.js b/sites/dsmart.com.tr/dsmart.com.tr.test.js index 19bb9732..67550a76 100644 --- a/sites/dsmart.com.tr/dsmart.com.tr.test.js +++ b/sites/dsmart.com.tr/dsmart.com.tr.test.js @@ -1,82 +1,82 @@ -const { parser, url } = require('./dsmart.com.tr.config.js') -const axios = require('axios') -const dayjs = require('dayjs') -const fs = require('fs') -const path = require('path') -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-01-13', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '5fe07f5dcfef0b1593275822', - xmltv_id: 'Sinema1001.tr' -} - -axios.get.mockImplementation(url => { - const result = {} - const urls = { - 'https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=1&limit=10&day=2025-01-13': - 'content1.json', - 'https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=2&limit=10&day=2025-01-13': - 'content2.json', - } - if (urls[url] !== undefined) { - result.data = fs.readFileSync(path.join(__dirname, '__data__', urls[url])).toString() - if (!urls[url].startsWith('content1')) { - result.data = JSON.parse(result.data) - } - } - - return Promise.resolve(result) -}) - - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=1&limit=10&day=2025-01-13' - ) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.join(__dirname, '__data__', 'content1.json')).toString() - const results = (await parser({ content, channel, date })).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(11) - - expect(results[0]).toMatchObject({ - start: '2025-01-12T21:30:00.000Z', - stop: '2025-01-12T23:30:00.000Z', - title: 'Taksi Şoförü', - description: - 'Vietnam savaşının izlerinin etkisindeki bir asker ve New York sokakları. Travis Bickle, geceleri taksi şoförlüğü yaptığı New York’ta bir yandan da gündelik yaşama ayak uydurmaya çalışır. Çürümeye yüz tutmuş bir topluma karşı tutulan bir ayna niteliğindeki film, yönetmen Martin Scorsese’nin kariyerinin en önemli filmlerinden biri olarak kabul görür.', - category: ['Sinema', 'Genel'] - }) - expect(results[10]).toMatchObject({ - start: '2025-01-13T19:00:00.000Z', - stop: '2025-01-13T21:00:00.000Z', - title: 'Senin Adın', - description: - 'Dağların sardığı bir bölgede yaşayan Mitsuha, hayatından çok da memnun olmayan liseli bir kızdır. Babası vali olarak çalışmakta ve seçim kampanyaları ile uğraşmaktadır. Evde kendisi, kardeşi ve büyükannesi dışında kimse yoktur. Kırsal kesimdeki yaşamı onu bunaltmaktadır ve esas isteği Tokyo\'nun muhteşem şehir hayatının bir parçası olmaktır. Diğer tarafta ise Taki vardır.', - category: ['Sinema', 'Genel'] - }) -}) - -it('can handle empty guide', async () => { - const results = await parser({ - channel, - date, - content: fs.readFileSync(path.join(__dirname, '__data__', 'no_content.json')).toString(), - useCache: false - }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./dsmart.com.tr.config.js') +const axios = require('axios') +const dayjs = require('dayjs') +const fs = require('fs') +const path = require('path') +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-01-13', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '5fe07f5dcfef0b1593275822', + xmltv_id: 'Sinema1001.tr' +} + +axios.get.mockImplementation(url => { + const result = {} + const urls = { + 'https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=1&limit=10&day=2025-01-13': + 'content1.json', + 'https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=2&limit=10&day=2025-01-13': + 'content2.json', + } + if (urls[url] !== undefined) { + result.data = fs.readFileSync(path.join(__dirname, '__data__', urls[url])).toString() + if (!urls[url].startsWith('content1')) { + result.data = JSON.parse(result.data) + } + } + + return Promise.resolve(result) +}) + + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=1&limit=10&day=2025-01-13' + ) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.join(__dirname, '__data__', 'content1.json')).toString() + const results = (await parser({ content, channel, date })).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(11) + + expect(results[0]).toMatchObject({ + start: '2025-01-12T21:30:00.000Z', + stop: '2025-01-12T23:30:00.000Z', + title: 'Taksi Şoförü', + description: + 'Vietnam savaşının izlerinin etkisindeki bir asker ve New York sokakları. Travis Bickle, geceleri taksi şoförlüğü yaptığı New York’ta bir yandan da gündelik yaşama ayak uydurmaya çalışır. Çürümeye yüz tutmuş bir topluma karşı tutulan bir ayna niteliğindeki film, yönetmen Martin Scorsese’nin kariyerinin en önemli filmlerinden biri olarak kabul görür.', + category: ['Sinema', 'Genel'] + }) + expect(results[10]).toMatchObject({ + start: '2025-01-13T19:00:00.000Z', + stop: '2025-01-13T21:00:00.000Z', + title: 'Senin Adın', + description: + 'Dağların sardığı bir bölgede yaşayan Mitsuha, hayatından çok da memnun olmayan liseli bir kızdır. Babası vali olarak çalışmakta ve seçim kampanyaları ile uğraşmaktadır. Evde kendisi, kardeşi ve büyükannesi dışında kimse yoktur. Kırsal kesimdeki yaşamı onu bunaltmaktadır ve esas isteği Tokyo\'nun muhteşem şehir hayatının bir parçası olmaktır. Diğer tarafta ise Taki vardır.', + category: ['Sinema', 'Genel'] + }) +}) + +it('can handle empty guide', async () => { + const results = await parser({ + channel, + date, + content: fs.readFileSync(path.join(__dirname, '__data__', 'no_content.json')).toString(), + useCache: false + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/dstv.com/dstv.com.config.js b/sites/dstv.com/dstv.com.config.js index 8e526ab5..3412a83d 100644 --- a/sites/dstv.com/dstv.com.config.js +++ b/sites/dstv.com/dstv.com.config.js @@ -1,206 +1,206 @@ -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') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -const API_ENDPOINT = 'https://www.dstv.com/umbraco/api/TvGuide' - -module.exports = { - site: 'dstv.com', - days: 2, - request: { - cache: { - ttl: 3 * 60 * 60 * 1000, // 3h - interpretHeader: false - } - }, - url: function ({ channel, date }) { - const [region] = channel.site_id.split('#') - const packageName = region === 'nga' ? '&package=DStv%20Premium' : '' - - return `${API_ENDPOINT}/GetProgrammes?d=${date.format( - 'YYYY-MM-DD' - )}${packageName}&country=${region}` - }, - async parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - for (const item of items) { - const details = await loadProgramDetails(item) - programs.push({ - title: item.Title, - description: parseDescription(details), - image: parseImage(details), - category: parseCategory(details), - start: parseTime(item.StartTime, channel), - stop: parseTime(item.EndTime, channel) - }) - } - - return programs - }, - async channels({ country }) { - const _ = require('lodash') - - const countries = { - ao: 'ago', - bj: 'ben', - bw: 'bwa', - bf: 'bfa', - bi: 'bdi', - cm: 'cmr', - cv: 'cpv', - td: 'tcd', - cf: 'caf', - km: 'com', - cd: 'cod', - dj: 'dji', - gq: 'gnq', - er: 'eri', - sz: 'swz', - et: 'eth', - ga: 'gab', - gm: 'gmb', - gh: 'gha', - gn: 'gin', - gw: 'gnb', - ci: 'civ', - ke: 'ken', - lr: 'lbr', - mg: 'mdg', - mw: 'mwi', - ml: 'mli', - mr: 'mrt', - mu: 'mus', - mz: 'moz', - na: 'nam', - ne: 'ner', - ng: 'nga', - cg: 'cog', - rw: 'rwa', - st: 'stp', - sn: 'sen', - sc: 'syc', - sl: 'sle', - so: 'som', - za: 'zaf', - ss: 'ssd', - sd: 'sdn', - tz: 'tza', - tg: 'tgo', - ug: 'uga', - zm: 'zmb', - zw: 'zwe' - } - - const code = countries[country] - - const data = await axios - .get(`${API_ENDPOINT}/GetProgrammes?d=${dayjs().format('YYYY-MM-DD')}&country=${code}`) - .then(r => r.data) - .catch(console.log) - - let channels = [] - data.Channels.forEach(item => { - channels.push({ - lang: 'en', - site_id: `${code}#${item.Number}`, - name: item.Name - }) - }) - - return _.uniqBy(channels, 'site_id') - } -} - -function parseTime(time, channel) { - const tz = { - ago: 'Africa/Luanda', - ben: 'Africa/Porto-Novo', - bwa: 'Africa/Gaborone', - bfa: 'Africa/Ouagadougou', - bdi: 'Africa/Bujumbura', - cmr: 'Africa/Douala', - cpv: 'CVT', - tcd: 'Africa/Ndjamena', - caf: 'Africa/Bangui', - com: 'Indian/Comoro', - cod: 'Africa/Kinshasa', - dji: 'Africa/Djibouti', - gnq: 'Africa/Malabo', - eri: 'Africa/Asmara', - swz: 'SAST', - eth: 'Africa/Addis_Ababa', - gap: 'Africa/Libreville', - gmb: 'Africa/Banjul', - gha: 'Africa/Accra', - gin: 'Africa/Conakry', - gnb: 'Africa/Bissau', - civ: 'Africa/Abidjan', - ken: 'Africa/Nairobi', - lbr: 'Africa/Monrovia', - mdg: 'Indian/Antananarivo', - mwi: 'Africa/Blantyre', - mli: 'Africa/Bamako', - mrt: 'Africa/Nouakchott', - mus: 'Indian/Mauritius', - moz: 'Africa/Maputo', - nam: 'Africa/Windhoek', - ner: 'Africa/Niamey', - nga: 'Africa/Lagos', - cog: 'Africa/Brazzaville', - rwa: 'Africa/Kigali', - stp: 'Africa/Sao_Tome', - sen: 'Africa/Dakar', - syc: 'Indian/Mahe', - sle: 'Africa/Freetown', - som: 'Africa/Mogadishu', - zaf: 'Africa/Johannesburg', - ssd: 'Africa/Juba', - sdn: 'Africa/Khartoum', - tza: 'Africa/Dar_es_Salaam', - tgo: 'Africa/Lome', - uga: 'Africa/Kampala', - zmb: 'Africa/Lusaka', - zwe: 'Africa/Harare' - } - const [region] = channel.site_id.split('#') - - return dayjs.tz(time, 'YYYY-MM-DDTHH:mm:ss', tz[region]) -} - -function parseDescription(details) { - return details ? details.Synopsis : null -} - -function parseImage(details) { - return details ? details.ThumbnailUri : null -} - -function parseCategory(details) { - return details ? details.SubGenres : null -} - -async function loadProgramDetails(item) { - const url = `${API_ENDPOINT}/GetProgramme?id=${item.Id}` - - return axios - .get(url) - .then(r => r.data) - .catch(console.error) -} - -function parseItems(content, channel) { - const [, channelId] = channel.site_id.split('#') - const data = JSON.parse(content) - if (!data || !Array.isArray(data.Channels)) return [] - const channelData = data.Channels.find(c => c.Number === channelId) - if (!channelData || !Array.isArray(channelData.Programmes)) return [] - - return channelData.Programmes -} +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 uniqBy = require('lodash.uniqby') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +const API_ENDPOINT = 'https://www.dstv.com/umbraco/api/TvGuide' + +module.exports = { + site: 'dstv.com', + days: 2, + request: { + cache: { + ttl: 3 * 60 * 60 * 1000, // 3h + interpretHeader: false + } + }, + url: function ({ channel, date }) { + const [region] = channel.site_id.split('#') + const packageName = region === 'nga' ? '&package=DStv%20Premium' : '' + + return `${API_ENDPOINT}/GetProgrammes?d=${date.format( + 'YYYY-MM-DD' + )}${packageName}&country=${region}` + }, + async parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + for (const item of items) { + const details = await loadProgramDetails(item) + programs.push({ + title: item.Title, + description: parseDescription(details), + image: parseImage(details), + category: parseCategory(details), + start: parseTime(item.StartTime, channel), + stop: parseTime(item.EndTime, channel) + }) + } + + return programs + }, + async channels({ country }) { + + const countries = { + ao: 'ago', + bj: 'ben', + bw: 'bwa', + bf: 'bfa', + bi: 'bdi', + cm: 'cmr', + cv: 'cpv', + td: 'tcd', + cf: 'caf', + km: 'com', + cd: 'cod', + dj: 'dji', + gq: 'gnq', + er: 'eri', + sz: 'swz', + et: 'eth', + ga: 'gab', + gm: 'gmb', + gh: 'gha', + gn: 'gin', + gw: 'gnb', + ci: 'civ', + ke: 'ken', + lr: 'lbr', + mg: 'mdg', + mw: 'mwi', + ml: 'mli', + mr: 'mrt', + mu: 'mus', + mz: 'moz', + na: 'nam', + ne: 'ner', + ng: 'nga', + cg: 'cog', + rw: 'rwa', + st: 'stp', + sn: 'sen', + sc: 'syc', + sl: 'sle', + so: 'som', + za: 'zaf', + ss: 'ssd', + sd: 'sdn', + tz: 'tza', + tg: 'tgo', + ug: 'uga', + zm: 'zmb', + zw: 'zwe' + } + + const code = countries[country] + + const data = await axios + .get(`${API_ENDPOINT}/GetProgrammes?d=${dayjs().format('YYYY-MM-DD')}&country=${code}`) + .then(r => r.data) + .catch(console.log) + + let channels = [] + data.Channels.forEach(item => { + channels.push({ + lang: 'en', + site_id: `${code}#${item.Number}`, + name: item.Name + }) + }) + + return uniqBy(channels, 'site_id') + } +} + +function parseTime(time, channel) { + const tz = { + ago: 'Africa/Luanda', + ben: 'Africa/Porto-Novo', + bwa: 'Africa/Gaborone', + bfa: 'Africa/Ouagadougou', + bdi: 'Africa/Bujumbura', + cmr: 'Africa/Douala', + cpv: 'CVT', + tcd: 'Africa/Ndjamena', + caf: 'Africa/Bangui', + com: 'Indian/Comoro', + cod: 'Africa/Kinshasa', + dji: 'Africa/Djibouti', + gnq: 'Africa/Malabo', + eri: 'Africa/Asmara', + swz: 'SAST', + eth: 'Africa/Addis_Ababa', + gap: 'Africa/Libreville', + gmb: 'Africa/Banjul', + gha: 'Africa/Accra', + gin: 'Africa/Conakry', + gnb: 'Africa/Bissau', + civ: 'Africa/Abidjan', + ken: 'Africa/Nairobi', + lbr: 'Africa/Monrovia', + mdg: 'Indian/Antananarivo', + mwi: 'Africa/Blantyre', + mli: 'Africa/Bamako', + mrt: 'Africa/Nouakchott', + mus: 'Indian/Mauritius', + moz: 'Africa/Maputo', + nam: 'Africa/Windhoek', + ner: 'Africa/Niamey', + nga: 'Africa/Lagos', + cog: 'Africa/Brazzaville', + rwa: 'Africa/Kigali', + stp: 'Africa/Sao_Tome', + sen: 'Africa/Dakar', + syc: 'Indian/Mahe', + sle: 'Africa/Freetown', + som: 'Africa/Mogadishu', + zaf: 'Africa/Johannesburg', + ssd: 'Africa/Juba', + sdn: 'Africa/Khartoum', + tza: 'Africa/Dar_es_Salaam', + tgo: 'Africa/Lome', + uga: 'Africa/Kampala', + zmb: 'Africa/Lusaka', + zwe: 'Africa/Harare' + } + const [region] = channel.site_id.split('#') + + return dayjs.tz(time, 'YYYY-MM-DDTHH:mm:ss', tz[region]) +} + +function parseDescription(details) { + return details ? details.Synopsis : null +} + +function parseImage(details) { + return details ? details.ThumbnailUri : null +} + +function parseCategory(details) { + return details ? details.SubGenres : null +} + +async function loadProgramDetails(item) { + const url = `${API_ENDPOINT}/GetProgramme?id=${item.Id}` + + return axios + .get(url) + .then(r => r.data) + .catch(console.error) +} + +function parseItems(content, channel) { + const [, channelId] = channel.site_id.split('#') + const data = JSON.parse(content) + if (!data || !Array.isArray(data.Channels)) return [] + const channelData = data.Channels.find(c => c.Number === channelId) + if (!channelData || !Array.isArray(channelData.Programmes)) return [] + + return channelData.Programmes +} diff --git a/sites/dstv.com/dstv.com.test.js b/sites/dstv.com/dstv.com.test.js index c2065a58..220afa94 100644 --- a/sites/dstv.com/dstv.com.test.js +++ b/sites/dstv.com/dstv.com.test.js @@ -1,111 +1,111 @@ -const { parser, url } = require('./dstv.com.config.js') -const axios = require('axios') -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) - -jest.mock('axios') - -const API_ENDPOINT = 'https://www.dstv.com/umbraco/api/TvGuide' - -const date = dayjs.utc('2022-11-22', 'YYYY-MM-DD').startOf('d') -const channelZA = { - site_id: 'zaf#201', - xmltv_id: 'SuperSportGrandstand.za' -} -const channelNG = { - site_id: 'nga#201', - xmltv_id: 'SuperSportGrandstand.za' -} - -it('can generate valid url for zaf', () => { - expect(url({ channel: channelZA, date })).toBe( - `${API_ENDPOINT}/GetProgrammes?d=2022-11-22&country=zaf` - ) -}) - -it('can generate valid url for nga', () => { - expect(url({ channel: channelNG, date })).toBe( - `${API_ENDPOINT}/GetProgrammes?d=2022-11-22&package=DStv%20Premium&country=nga` - ) -}) - -it('can parse response for ZA', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_zaf.json')) - - axios.get.mockImplementation(url => { - if (url === `${API_ENDPOINT}/GetProgramme?id=8b237235-aa17-4bb8-9ea6-097e7a813336`) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_zaf.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, channel: channelZA }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[1]).toMatchObject({ - start: '2022-11-21T23:00:00.000Z', - stop: '2022-11-22T00:00:00.000Z', - title: 'UFC FN HL: Nzechukwu v Cutelaba', - description: - "'UFC Fight Night Highlights - Heavyweight Bout: Kennedy Nzechukwu vs Ion Cutelaba'. From The UFC APEX Center - Las Vegas, USA.", - image: - 'https://03mcdecdnimagerepository.blob.core.windows.net/epguideimage/img/271546_UFC Fight Night.png', - category: ['All Sport', 'Mixed Martial Arts'] - }) -}) - -it('can parse response for NG', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_nga.json')) - - axios.get.mockImplementation(url => { - if (url === `${API_ENDPOINT}/GetProgramme?id=6d58931e-2192-486a-a202-14720136d204`) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_nga.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, channel: channelNG }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-21T23:00:00.000Z', - stop: '2022-11-22T00:00:00.000Z', - title: 'UFC FN HL: Nzechukwu v Cutelaba', - description: - "'UFC Fight Night Highlights - Heavyweight Bout: Kennedy Nzechukwu vs Ion Cutelaba'. From The UFC APEX Center - Las Vegas, USA.", - image: - 'https://03mcdecdnimagerepository.blob.core.windows.net/epguideimage/img/271546_UFC Fight Night.png', - category: ['All Sport', 'Mixed Martial Arts'] - }) -}) - -it('can handle empty guide', done => { - parser({ - content: '{"Total":0,"Channels":[]}', - channel: channelZA - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(done) -}) +const { parser, url } = require('./dstv.com.config.js') +const axios = require('axios') +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) + +jest.mock('axios') + +const API_ENDPOINT = 'https://www.dstv.com/umbraco/api/TvGuide' + +const date = dayjs.utc('2022-11-22', 'YYYY-MM-DD').startOf('d') +const channelZA = { + site_id: 'zaf#201', + xmltv_id: 'SuperSportGrandstand.za' +} +const channelNG = { + site_id: 'nga#201', + xmltv_id: 'SuperSportGrandstand.za' +} + +it('can generate valid url for zaf', () => { + expect(url({ channel: channelZA, date })).toBe( + `${API_ENDPOINT}/GetProgrammes?d=2022-11-22&country=zaf` + ) +}) + +it('can generate valid url for nga', () => { + expect(url({ channel: channelNG, date })).toBe( + `${API_ENDPOINT}/GetProgrammes?d=2022-11-22&package=DStv%20Premium&country=nga` + ) +}) + +it('can parse response for ZA', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_zaf.json')) + + axios.get.mockImplementation(url => { + if (url === `${API_ENDPOINT}/GetProgramme?id=8b237235-aa17-4bb8-9ea6-097e7a813336`) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_zaf.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, channel: channelZA }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[1]).toMatchObject({ + start: '2022-11-21T23:00:00.000Z', + stop: '2022-11-22T00:00:00.000Z', + title: 'UFC FN HL: Nzechukwu v Cutelaba', + description: + "'UFC Fight Night Highlights - Heavyweight Bout: Kennedy Nzechukwu vs Ion Cutelaba'. From The UFC APEX Center - Las Vegas, USA.", + image: + 'https://03mcdecdnimagerepository.blob.core.windows.net/epguideimage/img/271546_UFC Fight Night.png', + category: ['All Sport', 'Mixed Martial Arts'] + }) +}) + +it('can parse response for NG', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_nga.json')) + + axios.get.mockImplementation(url => { + if (url === `${API_ENDPOINT}/GetProgramme?id=6d58931e-2192-486a-a202-14720136d204`) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_nga.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, channel: channelNG }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-21T23:00:00.000Z', + stop: '2022-11-22T00:00:00.000Z', + title: 'UFC FN HL: Nzechukwu v Cutelaba', + description: + "'UFC Fight Night Highlights - Heavyweight Bout: Kennedy Nzechukwu vs Ion Cutelaba'. From The UFC APEX Center - Las Vegas, USA.", + image: + 'https://03mcdecdnimagerepository.blob.core.windows.net/epguideimage/img/271546_UFC Fight Night.png', + category: ['All Sport', 'Mixed Martial Arts'] + }) +}) + +it('can handle empty guide', done => { + parser({ + content: '{"Total":0,"Channels":[]}', + channel: channelZA + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(done) +}) diff --git a/sites/dtv8.net/dtv8.net.config.js b/sites/dtv8.net/dtv8.net.config.js index 4d68b20f..8f4afadc 100644 --- a/sites/dtv8.net/dtv8.net.config.js +++ b/sites/dtv8.net/dtv8.net.config.js @@ -1,90 +1,90 @@ -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: 'dtv8.net', - days: 2, - url({ date }) { - const day = date.format('dddd') - - return `https://dtv8.net/tv-listings/${day.toLowerCase()}/` - }, - parser({ content, date }) { - let programs = [] - - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - let prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start < prev.start) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - image: parseImage($item), - start, - stop - }) - }) - - return programs - }, - channels() { - return [] - } -} - -function parseTitle($item) { - return $item( - 'td:nth-child(2) > strong:nth-child(1),td:nth-child(2) > span > strong,td:nth-child(2) > span > b' - ).text() -} - -function parseDescription($item) { - return ( - $item( - 'td:nth-child(2) > strong:nth-child(3) > span,td:nth-child(2) > p:nth-child(3) > strong > span' - ).text() || null - ) -} - -function parseImage($item) { - return $item('td:nth-child(1) > img.size-full').attr('src') || null -} - -function parseStart($item, date) { - const time = $item('td:nth-child(1)').text() - - return dayjs.tz( - `${date.format('YYYY-MM-DD')} ${time}`, - 'YYYY-MM-DD HH:mm [hrs.]', - 'America/Guyana' - ) -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('table tr') - .filter((i, el) => { - const firstColumn = $(el).find('td').text() - - return Boolean(firstColumn) && !firstColumn.includes('Time') - }) - .toArray() -} +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: 'dtv8.net', + days: 2, + url({ date }) { + const day = date.format('dddd') + + return `https://dtv8.net/tv-listings/${day.toLowerCase()}/` + }, + parser({ content, date }) { + let programs = [] + + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + let prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start < prev.start) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + image: parseImage($item), + start, + stop + }) + }) + + return programs + }, + channels() { + return [] + } +} + +function parseTitle($item) { + return $item( + 'td:nth-child(2) > strong:nth-child(1),td:nth-child(2) > span > strong,td:nth-child(2) > span > b' + ).text() +} + +function parseDescription($item) { + return ( + $item( + 'td:nth-child(2) > strong:nth-child(3) > span,td:nth-child(2) > p:nth-child(3) > strong > span' + ).text() || null + ) +} + +function parseImage($item) { + return $item('td:nth-child(1) > img.size-full').attr('src') || null +} + +function parseStart($item, date) { + const time = $item('td:nth-child(1)').text() + + return dayjs.tz( + `${date.format('YYYY-MM-DD')} ${time}`, + 'YYYY-MM-DD HH:mm [hrs.]', + 'America/Guyana' + ) +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('table tr') + .filter((i, el) => { + const firstColumn = $(el).find('td').text() + + return Boolean(firstColumn) && !firstColumn.includes('Time') + }) + .toArray() +} diff --git a/sites/dtv8.net/dtv8.net.test.js b/sites/dtv8.net/dtv8.net.test.js index 027519ad..fc37443a 100644 --- a/sites/dtv8.net/dtv8.net.test.js +++ b/sites/dtv8.net/dtv8.net.test.js @@ -1,79 +1,79 @@ -const { parser, url } = require('./dtv8.net.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-02-21', 'YYYY-MM-DD').startOf('d') - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://dtv8.net/tv-listings/friday/') -}) - -it('can parse response for friday', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_fri.html')) - - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - - return p - }) - - expect(results.length).toBe(18) - expect(results[9]).toMatchObject({ - title: 'Smallville', - image: 'http://dtv8.net/wp-content/uploads/71P0aShCBXL._SL1300_.jpg', - description: - 'A young Clark Kent struggles to find his place in the world as he learns to harness his alien powers for good and deals with the typical troubles of teenage life in Smallville, Kansas.', - start: '2025-02-21T21:00:00.000Z', - stop: '2025-02-21T22:00:00.000Z' - }) - expect(results[15]).toMatchObject({ - title: 'Law & Order', - image: null, - description: - 'In God We Trust: A young lawyer with a secret past is found dead; Price and Baxter debate the pros and cons of prison as a punishment versus alternative justice options.', - start: '2025-02-22T01:45:00.000Z', - stop: '2025-02-22T02:30:00.000Z' - }) -}) - -it('can parse response for saturday', () => { - const date = dayjs.utc('2025-02-22', 'YYYY-MM-DD').startOf('d') - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_sat.html')) - - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - - return p - }) - - expect(results.length).toBe(11) - expect(results[0]).toMatchObject({ - title: 'Sign On', - image: null, - description: null, - start: '2025-02-22T13:55:00.000Z', - stop: '2025-02-22T14:00:00.000Z' - }) - expect(results[10]).toMatchObject({ - title: 'Sign Off', - image: null, - description: null, - start: '2025-02-23T04:00:00.000Z', - stop: '2025-02-23T04:30:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ content: '' }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./dtv8.net.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-02-21', 'YYYY-MM-DD').startOf('d') + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://dtv8.net/tv-listings/friday/') +}) + +it('can parse response for friday', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_fri.html')) + + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + + return p + }) + + expect(results.length).toBe(18) + expect(results[9]).toMatchObject({ + title: 'Smallville', + image: 'http://dtv8.net/wp-content/uploads/71P0aShCBXL._SL1300_.jpg', + description: + 'A young Clark Kent struggles to find his place in the world as he learns to harness his alien powers for good and deals with the typical troubles of teenage life in Smallville, Kansas.', + start: '2025-02-21T21:00:00.000Z', + stop: '2025-02-21T22:00:00.000Z' + }) + expect(results[15]).toMatchObject({ + title: 'Law & Order', + image: null, + description: + 'In God We Trust: A young lawyer with a secret past is found dead; Price and Baxter debate the pros and cons of prison as a punishment versus alternative justice options.', + start: '2025-02-22T01:45:00.000Z', + stop: '2025-02-22T02:30:00.000Z' + }) +}) + +it('can parse response for saturday', () => { + const date = dayjs.utc('2025-02-22', 'YYYY-MM-DD').startOf('d') + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_sat.html')) + + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + + return p + }) + + expect(results.length).toBe(11) + expect(results[0]).toMatchObject({ + title: 'Sign On', + image: null, + description: null, + start: '2025-02-22T13:55:00.000Z', + stop: '2025-02-22T14:00:00.000Z' + }) + expect(results[10]).toMatchObject({ + title: 'Sign Off', + image: null, + description: null, + start: '2025-02-23T04:00:00.000Z', + stop: '2025-02-23T04:30:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ content: '' }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/elcinema.com/elcinema.com.config.js b/sites/elcinema.com/elcinema.com.config.js index c6c4b85a..7b925723 100644 --- a/sites/elcinema.com/elcinema.com.config.js +++ b/sites/elcinema.com/elcinema.com.config.js @@ -1,149 +1,149 @@ -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') -require('dayjs/locale/ar') - -dayjs.extend(customParseFormat) -dayjs.extend(timezone) -dayjs.extend(utc) - -const headers = { - 'User-Agent': -'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 OPR/115.0.0.0' } - -module.exports = { - site: 'elcinema.com', - days: 2, - request: { headers }, - url({ channel }) { - const lang = channel.lang === 'en' ? 'en/' : '/' - - return `https://elcinema.com/${lang}tvguide/${channel.site_id}/` - }, - parser({ content, channel, date }) { - const programs = [] - const items = parseItems(content, channel, date) - items.forEach(item => { - const start = parseStart(item, date) - const duration = parseDuration(item) - const stop = start.add(duration, 'm') - programs.push({ - title: parseTitle(item), - description: parseDescription(item), - category: parseCategory(item), - image: parseImage(item), - start, - stop - }) - }) - - return programs - }, - async channels({ lang }) { - const axios = require('axios') - const data = await axios - .get(`https://elcinema.com/${lang}/tvguide/`, { - headers: headers - }) - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(data) - - return $('.tv-line') - .map((i, el) => { - const link = $(el).find('.channel > div > div.hide-for-small-only > a') - const name = $(link).text() - const href = $(link).attr('href') - const [, site_id] = href.match(/\/(\d+)\/$/) - - return { - lang, - site_id, - name - } - }) - .get() - } -} - -function parseImage(item) { - const $ = cheerio.load(item) - const imgSrc = - $('.row > div.columns.small-3.large-1 > a > img').data('src') || - $('.row > div.columns.small-5.large-1 > img').data('src') - - return imgSrc || null -} - -function parseCategory(item) { - const $ = cheerio.load(item) - const category = $('.row > div.columns.small-6.large-3 > ul > li:nth-child(2)').text() - - return category.replace(/\(\d+\)/, '').trim() || null -} - -function parseDuration(item) { - const $ = cheerio.load(item) - const duration = - $('.row > div.columns.small-3.large-2 > ul > li:nth-child(2) > span').text() || - $('.row > div.columns.small-7.large-11 > ul > li:nth-child(2) > span').text() - - return duration.replace(/\D/g, '') || '' -} - -function parseStart(item, initDate) { - const $ = cheerio.load(item) - let time = - $('.row > div.columns.small-3.large-2 > ul > li:nth-child(1)').text() || - $('.row > div.columns.small-7.large-11 > ul > li:nth-child(2)').text() || - '' - - time = time - .replace(/\[.*\]/, '') - .replace('مساءً', 'PM') - .replace('صباحًا', 'AM') - .trim() - - time = `${initDate.format('YYYY-MM-DD')} ${time}` - - return dayjs.tz(time, 'YYYY-MM-DD hh:mm A', dayjs.tz.guess()) -} - -function parseTitle(item) { - const $ = cheerio.load(item) - - return ( - $('.row > div.columns.small-6.large-3 > ul > li:nth-child(1) > a').text() || - $('.row > div.columns.small-7.large-11 > ul > li:nth-child(1)').text() || - null - ) -} - -function parseDescription(item) { - const $ = cheerio.load(item) - const excerpt = $('.row > div.columns.small-12.large-6 > ul > li:nth-child(3)').text() || '' - - return excerpt.replace('...اقرأ المزيد', '').replace('...Read more', '') -} - -function parseItems(content, channel, date) { - const $ = cheerio.load(content) - - const dateString = date.locale(channel.lang).format('dddd D') - - const list = $('.dates') - .filter((i, el) => { - let parsedDateString = $(el).text().trim() - parsedDateString = parsedDateString.replace(/\s\s+/g, ' ') - - return parsedDateString.includes(dateString) - }) - .first() - .parent() - .next() - - return $('.padded-half', list).toArray() -} +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') +require('dayjs/locale/ar') + +dayjs.extend(customParseFormat) +dayjs.extend(timezone) +dayjs.extend(utc) + +const headers = { + 'User-Agent': +'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 OPR/115.0.0.0' } + +module.exports = { + site: 'elcinema.com', + days: 2, + request: { headers }, + url({ channel }) { + const lang = channel.lang === 'en' ? 'en/' : '/' + + return `https://elcinema.com/${lang}tvguide/${channel.site_id}/` + }, + parser({ content, channel, date }) { + const programs = [] + const items = parseItems(content, channel, date) + items.forEach(item => { + const start = parseStart(item, date) + const duration = parseDuration(item) + const stop = start.add(duration, 'm') + programs.push({ + title: parseTitle(item), + description: parseDescription(item), + category: parseCategory(item), + image: parseImage(item), + start, + stop + }) + }) + + return programs + }, + async channels({ lang }) { + const axios = require('axios') + const data = await axios + .get(`https://elcinema.com/${lang}/tvguide/`, { + headers: headers + }) + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(data) + + return $('.tv-line') + .map((i, el) => { + const link = $(el).find('.channel > div > div.hide-for-small-only > a') + const name = $(link).text() + const href = $(link).attr('href') + const [, site_id] = href.match(/\/(\d+)\/$/) + + return { + lang, + site_id, + name + } + }) + .get() + } +} + +function parseImage(item) { + const $ = cheerio.load(item) + const imgSrc = + $('.row > div.columns.small-3.large-1 > a > img').data('src') || + $('.row > div.columns.small-5.large-1 > img').data('src') + + return imgSrc || null +} + +function parseCategory(item) { + const $ = cheerio.load(item) + const category = $('.row > div.columns.small-6.large-3 > ul > li:nth-child(2)').text() + + return category.replace(/\(\d+\)/, '').trim() || null +} + +function parseDuration(item) { + const $ = cheerio.load(item) + const duration = + $('.row > div.columns.small-3.large-2 > ul > li:nth-child(2) > span').text() || + $('.row > div.columns.small-7.large-11 > ul > li:nth-child(2) > span').text() + + return duration.replace(/\D/g, '') || '' +} + +function parseStart(item, initDate) { + const $ = cheerio.load(item) + let time = + $('.row > div.columns.small-3.large-2 > ul > li:nth-child(1)').text() || + $('.row > div.columns.small-7.large-11 > ul > li:nth-child(2)').text() || + '' + + time = time + .replace(/\[.*\]/, '') + .replace('مساءً', 'PM') + .replace('صباحًا', 'AM') + .trim() + + time = `${initDate.format('YYYY-MM-DD')} ${time}` + + return dayjs.tz(time, 'YYYY-MM-DD hh:mm A', dayjs.tz.guess()) +} + +function parseTitle(item) { + const $ = cheerio.load(item) + + return ( + $('.row > div.columns.small-6.large-3 > ul > li:nth-child(1) > a').text() || + $('.row > div.columns.small-7.large-11 > ul > li:nth-child(1)').text() || + null + ) +} + +function parseDescription(item) { + const $ = cheerio.load(item) + const excerpt = $('.row > div.columns.small-12.large-6 > ul > li:nth-child(3)').text() || '' + + return excerpt.replace('...اقرأ المزيد', '').replace('...Read more', '') +} + +function parseItems(content, channel, date) { + const $ = cheerio.load(content) + + const dateString = date.locale(channel.lang).format('dddd D') + + const list = $('.dates') + .filter((i, el) => { + let parsedDateString = $(el).text().trim() + parsedDateString = parsedDateString.replace(/\s\s+/g, ' ') + + return parsedDateString.includes(dateString) + }) + .first() + .parent() + .next() + + return $('.padded-half', list).toArray() +} diff --git a/sites/elcinema.com/elcinema.com.test.js b/sites/elcinema.com/elcinema.com.test.js index 92a794fe..d80c37ba 100644 --- a/sites/elcinema.com/elcinema.com.test.js +++ b/sites/elcinema.com/elcinema.com.test.js @@ -1,69 +1,69 @@ -const { parser, url } = require('./elcinema.com.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('2022-08-28', 'YYYY-MM-DD').startOf('d') -const channelAR = { - lang: 'ar', - site_id: '1254', - xmltv_id: 'OSNSeries.ae' -} -const channelEN = { - lang: 'en', - site_id: '1254', - xmltv_id: 'OSNSeries.ae' -} - -it('can generate valid url', () => { - expect(url({ channel: channelEN })).toBe('https://elcinema.com/en/tvguide/1254/') -}) - -it('can parse response (en)', () => { - const contentEN = fs.readFileSync(path.resolve(__dirname, '__data__/content.en.html')) - const results = parser({ date, channel: channelEN, content: contentEN }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-08-27T14:25:00.000Z', - stop: '2022-08-27T15:15:00.000Z', - title: 'Station 19 S5', - image: - 'https://media.elcinema.com/uploads/_150x200_ec30d1a2251c8edf83334be4860184c74d2534d7ba508a334ad66fa59acc4926.jpg', - category: 'Series' - }) -}) - -it('can parse response (ar)', () => { - const contentAR = fs.readFileSync(path.resolve(__dirname, '__data__/content.ar.html')) - const results = parser({ date, channel: channelAR, content: contentAR }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-08-27T14:25:00.000Z', - stop: '2022-08-27T15:15:00.000Z', - title: 'Station 19 S5', - image: - 'https://media.elcinema.com/uploads/_150x200_ec30d1a2251c8edf83334be4860184c74d2534d7ba508a334ad66fa59acc4926.jpg', - category: 'مسلسل' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel: channelEN, - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./elcinema.com.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('2022-08-28', 'YYYY-MM-DD').startOf('d') +const channelAR = { + lang: 'ar', + site_id: '1254', + xmltv_id: 'OSNSeries.ae' +} +const channelEN = { + lang: 'en', + site_id: '1254', + xmltv_id: 'OSNSeries.ae' +} + +it('can generate valid url', () => { + expect(url({ channel: channelEN })).toBe('https://elcinema.com/en/tvguide/1254/') +}) + +it('can parse response (en)', () => { + const contentEN = fs.readFileSync(path.resolve(__dirname, '__data__/content.en.html')) + const results = parser({ date, channel: channelEN, content: contentEN }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-08-27T14:25:00.000Z', + stop: '2022-08-27T15:15:00.000Z', + title: 'Station 19 S5', + image: + 'https://media.elcinema.com/uploads/_150x200_ec30d1a2251c8edf83334be4860184c74d2534d7ba508a334ad66fa59acc4926.jpg', + category: 'Series' + }) +}) + +it('can parse response (ar)', () => { + const contentAR = fs.readFileSync(path.resolve(__dirname, '__data__/content.ar.html')) + const results = parser({ date, channel: channelAR, content: contentAR }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-08-27T14:25:00.000Z', + stop: '2022-08-27T15:15:00.000Z', + title: 'Station 19 S5', + image: + 'https://media.elcinema.com/uploads/_150x200_ec30d1a2251c8edf83334be4860184c74d2534d7ba508a334ad66fa59acc4926.jpg', + category: 'مسلسل' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel: channelEN, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/ena.skylifetv.co.kr/ena.skylifetv.co.kr.config.js b/sites/ena.skylifetv.co.kr/ena.skylifetv.co.kr.config.js index e94f170a..d8401689 100644 --- a/sites/ena.skylifetv.co.kr/ena.skylifetv.co.kr.config.js +++ b/sites/ena.skylifetv.co.kr/ena.skylifetv.co.kr.config.js @@ -1,68 +1,68 @@ -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: 'ena.skylifetv.co.kr', - days: 2, - url({ channel, date }) { - return `http://ena.skylifetv.co.kr/${channel.site_id}/?day=${date.format('YYYYMMDD')}&sc_dvsn=U` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const $item = cheerio.load(item) - const start = parseStart($item, date) - const duration = parseDuration($item) - const stop = start.add(duration, 'm') - programs.push({ - title: parseTitle($item), - rating: parseRating($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('.col2 > .tit').text().trim() -} - -function parseRating($item) { - const rating = $item('.col4').text().trim() - - return rating - ? { - system: 'KMRB', - value: rating - } - : null -} - -function parseDuration($item) { - const duration = $item('.col5').text().trim() - - return duration ? parseInt(duration) : 30 -} - -function parseStart($item, date) { - const time = $item('.col1').text().trim() - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Seoul') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.tbl_schedule > tbody > tr').toArray() -} +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: 'ena.skylifetv.co.kr', + days: 2, + url({ channel, date }) { + return `http://ena.skylifetv.co.kr/${channel.site_id}/?day=${date.format('YYYYMMDD')}&sc_dvsn=U` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const $item = cheerio.load(item) + const start = parseStart($item, date) + const duration = parseDuration($item) + const stop = start.add(duration, 'm') + programs.push({ + title: parseTitle($item), + rating: parseRating($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('.col2 > .tit').text().trim() +} + +function parseRating($item) { + const rating = $item('.col4').text().trim() + + return rating + ? { + system: 'KMRB', + value: rating + } + : null +} + +function parseDuration($item) { + const duration = $item('.col5').text().trim() + + return duration ? parseInt(duration) : 30 +} + +function parseStart($item, date) { + const time = $item('.col1').text().trim() + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Seoul') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.tbl_schedule > tbody > tr').toArray() +} diff --git a/sites/ena.skylifetv.co.kr/ena.skylifetv.co.kr.test.js b/sites/ena.skylifetv.co.kr/ena.skylifetv.co.kr.test.js index 7528ce91..6233e183 100644 --- a/sites/ena.skylifetv.co.kr/ena.skylifetv.co.kr.test.js +++ b/sites/ena.skylifetv.co.kr/ena.skylifetv.co.kr.test.js @@ -1,57 +1,57 @@ -const { parser, url } = require('./ena.skylifetv.co.kr.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('2023-01-27', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'ENA', - xmltv_id: 'ENA.kr' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('http://ena.skylifetv.co.kr/ENA/?day=20230127&sc_dvsn=U') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - 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: '2023-01-26T16:05:00.000Z', - stop: '2023-01-26T17:20:00.000Z', - title: '법쩐 6화', - rating: { - system: 'KMRB', - value: '15' - } - }) - - expect(results[17]).toMatchObject({ - start: '2023-01-27T14:10:00.000Z', - stop: '2023-01-27T15:25:00.000Z', - title: '남이 될 수 있을까 4화', - rating: { - system: 'KMRB', - value: '15' - } - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./ena.skylifetv.co.kr.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('2023-01-27', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'ENA', + xmltv_id: 'ENA.kr' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('http://ena.skylifetv.co.kr/ENA/?day=20230127&sc_dvsn=U') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + 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: '2023-01-26T16:05:00.000Z', + stop: '2023-01-26T17:20:00.000Z', + title: '법쩐 6화', + rating: { + system: 'KMRB', + value: '15' + } + }) + + expect(results[17]).toMatchObject({ + start: '2023-01-27T14:10:00.000Z', + stop: '2023-01-27T15:25:00.000Z', + title: '남이 될 수 있을까 4화', + rating: { + system: 'KMRB', + value: '15' + } + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/energeek.cl/energeek.cl.config.js b/sites/energeek.cl/energeek.cl.config.js index dfbfb7d9..c4447f21 100644 --- a/sites/energeek.cl/energeek.cl.config.js +++ b/sites/energeek.cl/energeek.cl.config.js @@ -1,33 +1,33 @@ -const parser = require('epg-parser') - -module.exports = { - site: 'energeek.cl', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: 'https://raw.githubusercontent.com/luisms123/tdt/master/guiaenergeek.xml', - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel, date) - items.forEach(item => { - programs.push({ - title: item.title?.[0]?.value, - description: item.desc?.[0]?.value, - icon: item.icon?.[0]?.src, - start: item.start, - stop: item.stop - }) - }) - - return programs - } -} - -function parseItems(content, channel, date) { - const { programs } = parser.parse(content) - - return programs.filter(p => p.channel === channel.site_id && date.isSame(p.start, 'day')) -} +const parser = require('epg-parser') + +module.exports = { + site: 'energeek.cl', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: 'https://raw.githubusercontent.com/luisms123/tdt/master/guiaenergeek.xml', + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel, date) + items.forEach(item => { + programs.push({ + title: item.title?.[0]?.value, + description: item.desc?.[0]?.value, + icon: item.icon?.[0]?.src, + start: item.start, + stop: item.stop + }) + }) + + return programs + } +} + +function parseItems(content, channel, date) { + const { programs } = parser.parse(content) + + return programs.filter(p => p.channel === channel.site_id && date.isSame(p.start, 'day')) +} diff --git a/sites/energeek.cl/energeek.cl.test.js b/sites/energeek.cl/energeek.cl.test.js index 540d63ce..f3e2c19f 100644 --- a/sites/energeek.cl/energeek.cl.test.js +++ b/sites/energeek.cl/energeek.cl.test.js @@ -1,37 +1,37 @@ -const { parser, url } = require('./energeek.cl.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('2022-11-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'EnerGeek Retro', - xmltv_id: 'EnerGeekRetro.cl' -} - -it('can generate valid url', () => { - expect(url).toBe('https://raw.githubusercontent.com/luisms123/tdt/master/guiaenergeek.xml') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml')) - let results = parser({ content, channel, date }) - - expect(results[0]).toMatchObject({ - start: '2022-11-29T03:00:00.000Z', - stop: '2022-11-29T03:30:00.000Z', - title: 'Noir', - description: - 'Kirika Yuumura es una adolescente japonesa que no recuerda nada de su pasado, salvo la palabra NOIR, por lo que decidirá contactar con Mireille Bouquet, una asesina profesional para que la ayude a investigar. Ambas forman un equipo muy eficiente, que resuelve un trabajo tras otro con gran éxito, hasta que aparece un grupo conocido como "Les Soldats", relacionados con el pasado de Kirika. Estos tratarán de eliminar a las dos chicas, antes de que indaguen más hondo sobre la verdad acerca de Noir', - icon: 'https://pics.filmaffinity.com/nowaru_noir_tv_series-225888552-mmed.jpg' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ content: '', channel, date }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./energeek.cl.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('2022-11-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'EnerGeek Retro', + xmltv_id: 'EnerGeekRetro.cl' +} + +it('can generate valid url', () => { + expect(url).toBe('https://raw.githubusercontent.com/luisms123/tdt/master/guiaenergeek.xml') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml')) + let results = parser({ content, channel, date }) + + expect(results[0]).toMatchObject({ + start: '2022-11-29T03:00:00.000Z', + stop: '2022-11-29T03:30:00.000Z', + title: 'Noir', + description: + 'Kirika Yuumura es una adolescente japonesa que no recuerda nada de su pasado, salvo la palabra NOIR, por lo que decidirá contactar con Mireille Bouquet, una asesina profesional para que la ayude a investigar. Ambas forman un equipo muy eficiente, que resuelve un trabajo tras otro con gran éxito, hasta que aparece un grupo conocido como "Les Soldats", relacionados con el pasado de Kirika. Estos tratarán de eliminar a las dos chicas, antes de que indaguen más hondo sobre la verdad acerca de Noir', + icon: 'https://pics.filmaffinity.com/nowaru_noir_tv_series-225888552-mmed.jpg' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ content: '', channel, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/entertainment.ie/entertainment.ie.config.js b/sites/entertainment.ie/entertainment.ie.config.js index 04ff4ec6..3bba2f04 100644 --- a/sites/entertainment.ie/entertainment.ie.config.js +++ b/sites/entertainment.ie/entertainment.ie.config.js @@ -1,96 +1,96 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const { DateTime } = require('luxon') - -module.exports = { - site: 'entertainment.ie', - days: 2, - url: function ({ date, channel }) { - return `https://entertainment.ie/tv/${channel.site_id}/?date=${date.format( - 'DD-MM-YYYY' - )}&time=all-day` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (!start) return - if (prev && start < prev.start) { - start = start.plus({ days: 1 }) - } - const duration = parseDuration($item) - const stop = start.plus({ minutes: duration }) - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - categories: parseCategories($item), - image: parseImage($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://entertainment.ie/tv/all-channels/') - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(data) - let channels = $('.tv-filter-container > tv-filter').attr(':channels') - channels = JSON.parse(channels) - - return channels.map(c => { - return { - lang: 'en', - site_id: c.slug, - name: c.name - } - }) - } -} - -function parseImage($item) { - return $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('img') -} - -function parseTitle($item) { - return $item('.text-holder h3').text().trim() -} - -function parseDescription($item) { - return $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('description') -} - -function parseCategories($item) { - const genres = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('genres') - - return genres ? genres.split(', ') : [] -} - -function parseStart($item, date) { - let d = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('time') - let [, time] = d ? d.split(', ') : [null, null] - - return time - ? DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { - zone: 'UTC' - }).toUTC() - : null -} - -function parseDuration($item) { - const duration = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('duration') - - return parseInt(duration) -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.info-list > li').toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const { DateTime } = require('luxon') + +module.exports = { + site: 'entertainment.ie', + days: 2, + url: function ({ date, channel }) { + return `https://entertainment.ie/tv/${channel.site_id}/?date=${date.format( + 'DD-MM-YYYY' + )}&time=all-day` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (!start) return + if (prev && start < prev.start) { + start = start.plus({ days: 1 }) + } + const duration = parseDuration($item) + const stop = start.plus({ minutes: duration }) + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + categories: parseCategories($item), + image: parseImage($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://entertainment.ie/tv/all-channels/') + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(data) + let channels = $('.tv-filter-container > tv-filter').attr(':channels') + channels = JSON.parse(channels) + + return channels.map(c => { + return { + lang: 'en', + site_id: c.slug, + name: c.name + } + }) + } +} + +function parseImage($item) { + return $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('img') +} + +function parseTitle($item) { + return $item('.text-holder h3').text().trim() +} + +function parseDescription($item) { + return $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('description') +} + +function parseCategories($item) { + const genres = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('genres') + + return genres ? genres.split(', ') : [] +} + +function parseStart($item, date) { + let d = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('time') + let [, time] = d ? d.split(', ') : [null, null] + + return time + ? DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { + zone: 'UTC' + }).toUTC() + : null +} + +function parseDuration($item) { + const duration = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('duration') + + return parseInt(duration) +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.info-list > li').toArray() +} diff --git a/sites/entertainment.ie/entertainment.ie.test.js b/sites/entertainment.ie/entertainment.ie.test.js index eaf660dd..5a35e68e 100644 --- a/sites/entertainment.ie/entertainment.ie.test.js +++ b/sites/entertainment.ie/entertainment.ie.test.js @@ -1,58 +1,58 @@ -const fs = require('fs') -const path = require('path') -const { parser, url } = require('./entertainment.ie.config.js') -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('2023-06-29', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'rte2', xmltv_id: 'RTE2.ie' } - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://entertainment.ie/tv/rte2/?date=29-06-2023&time=all-day' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ date, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(51) - - expect(results[0]).toMatchObject({ - start: '2023-06-29T06:00:00.000Z', - stop: '2023-06-29T08:00:00.000Z', - title: 'EuroNews', - description: 'European and international headlines live via satellite', - image: - 'https://img.resized.co/entertainment/eyJkYXRhIjoie1widXJsXCI6XCJodHRwczpcXFwvXFxcL3R2LmFzc2V0cy5wcmVzc2Fzc29jaWF0aW9uLmlvXFxcLzcxZDdkYWY2LWQxMjItNTliYy1iMGRjLTFkMjc2ODg1MzhkNC5qcGdcIixcIndpZHRoXCI6NDgwLFwiaGVpZ2h0XCI6Mjg4LFwiZGVmYXVsdFwiOlwiaHR0cHM6XFxcL1xcXC9lbnRlcnRhaW5tZW50LmllXFxcL2ltYWdlc1xcXC9uby1pbWFnZS5wbmdcIn0iLCJoYXNoIjoiZDhjYzA0NzFhMGZhOTI1Yjc5ODI0M2E3OWZjMGI2ZGJmMDIxMjllNyJ9/71d7daf6-d122-59bc-b0dc-1d27688538d4.jpg', - categories: ['Factual'] - }) - - expect(results[50]).toMatchObject({ - start: '2023-06-30T02:25:00.000Z', - stop: '2023-06-30T06:00:00.000Z', - title: 'EuroNews', - description: 'European and international headlines live via satellite', - image: - 'https://img.resized.co/entertainment/eyJkYXRhIjoie1widXJsXCI6XCJodHRwczpcXFwvXFxcL3R2LmFzc2V0cy5wcmVzc2Fzc29jaWF0aW9uLmlvXFxcLzcxZDdkYWY2LWQxMjItNTliYy1iMGRjLTFkMjc2ODg1MzhkNC5qcGdcIixcIndpZHRoXCI6NDgwLFwiaGVpZ2h0XCI6Mjg4LFwiZGVmYXVsdFwiOlwiaHR0cHM6XFxcL1xcXC9lbnRlcnRhaW5tZW50LmllXFxcL2ltYWdlc1xcXC9uby1pbWFnZS5wbmdcIn0iLCJoYXNoIjoiZDhjYzA0NzFhMGZhOTI1Yjc5ODI0M2E3OWZjMGI2ZGJmMDIxMjllNyJ9/71d7daf6-d122-59bc-b0dc-1d27688538d4.jpg', - categories: ['Factual'] - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html')) - const result = parser({ - date, - channel, - content - }) - expect(result).toMatchObject([]) -}) +const fs = require('fs') +const path = require('path') +const { parser, url } = require('./entertainment.ie.config.js') +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('2023-06-29', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'rte2', xmltv_id: 'RTE2.ie' } + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://entertainment.ie/tv/rte2/?date=29-06-2023&time=all-day' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ date, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(51) + + expect(results[0]).toMatchObject({ + start: '2023-06-29T06:00:00.000Z', + stop: '2023-06-29T08:00:00.000Z', + title: 'EuroNews', + description: 'European and international headlines live via satellite', + image: + 'https://img.resized.co/entertainment/eyJkYXRhIjoie1widXJsXCI6XCJodHRwczpcXFwvXFxcL3R2LmFzc2V0cy5wcmVzc2Fzc29jaWF0aW9uLmlvXFxcLzcxZDdkYWY2LWQxMjItNTliYy1iMGRjLTFkMjc2ODg1MzhkNC5qcGdcIixcIndpZHRoXCI6NDgwLFwiaGVpZ2h0XCI6Mjg4LFwiZGVmYXVsdFwiOlwiaHR0cHM6XFxcL1xcXC9lbnRlcnRhaW5tZW50LmllXFxcL2ltYWdlc1xcXC9uby1pbWFnZS5wbmdcIn0iLCJoYXNoIjoiZDhjYzA0NzFhMGZhOTI1Yjc5ODI0M2E3OWZjMGI2ZGJmMDIxMjllNyJ9/71d7daf6-d122-59bc-b0dc-1d27688538d4.jpg', + categories: ['Factual'] + }) + + expect(results[50]).toMatchObject({ + start: '2023-06-30T02:25:00.000Z', + stop: '2023-06-30T06:00:00.000Z', + title: 'EuroNews', + description: 'European and international headlines live via satellite', + image: + 'https://img.resized.co/entertainment/eyJkYXRhIjoie1widXJsXCI6XCJodHRwczpcXFwvXFxcL3R2LmFzc2V0cy5wcmVzc2Fzc29jaWF0aW9uLmlvXFxcLzcxZDdkYWY2LWQxMjItNTliYy1iMGRjLTFkMjc2ODg1MzhkNC5qcGdcIixcIndpZHRoXCI6NDgwLFwiaGVpZ2h0XCI6Mjg4LFwiZGVmYXVsdFwiOlwiaHR0cHM6XFxcL1xcXC9lbnRlcnRhaW5tZW50LmllXFxcL2ltYWdlc1xcXC9uby1pbWFnZS5wbmdcIn0iLCJoYXNoIjoiZDhjYzA0NzFhMGZhOTI1Yjc5ODI0M2E3OWZjMGI2ZGJmMDIxMjllNyJ9/71d7daf6-d122-59bc-b0dc-1d27688538d4.jpg', + categories: ['Factual'] + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html')) + const result = parser({ + date, + channel, + content + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/epg.112114.xyz/epg.112114.xyz.config.js b/sites/epg.112114.xyz/epg.112114.xyz.config.js index dc010944..e512fa20 100644 --- a/sites/epg.112114.xyz/epg.112114.xyz.config.js +++ b/sites/epg.112114.xyz/epg.112114.xyz.config.js @@ -1,45 +1,45 @@ -const axios = require('axios') -const parser = require('epg-parser') - -module.exports = { - site: 'epg.112114.xyz', - days: 1, - url: 'https://epg.112114.xyz/pp.xml', - request: { - cache: { - ttl: 24 * 60 * 60 * 1000 // 1 day - } - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel, date) - items.forEach(item => { - programs.push({ - title: item.title?.[0]?.value, - start: item.start, - stop: item.stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://epg.112114.xyz/pp.xml') - .then(r => r.data) - .catch(console.log) - const { channels } = parser.parse(data) - - return channels.map(channel => ({ - lang: 'zh', - site_id: channel.id, - name: channel.displayName[0].value - })) - } -} - -function parseItems(content, channel, date) { - const { programs } = parser.parse(content) - - return programs.filter(p => p.channel === channel.site_id && date.isSame(p.start, 'day')) -} +const axios = require('axios') +const parser = require('epg-parser') + +module.exports = { + site: 'epg.112114.xyz', + days: 1, + url: 'https://epg.112114.xyz/pp.xml', + request: { + cache: { + ttl: 24 * 60 * 60 * 1000 // 1 day + } + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel, date) + items.forEach(item => { + programs.push({ + title: item.title?.[0]?.value, + start: item.start, + stop: item.stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://epg.112114.xyz/pp.xml') + .then(r => r.data) + .catch(console.log) + const { channels } = parser.parse(data) + + return channels.map(channel => ({ + lang: 'zh', + site_id: channel.id, + name: channel.displayName[0].value + })) + } +} + +function parseItems(content, channel, date) { + const { programs } = parser.parse(content) + + return programs.filter(p => p.channel === channel.site_id && date.isSame(p.start, 'day')) +} diff --git a/sites/epg.112114.xyz/epg.112114.xyz.test.js b/sites/epg.112114.xyz/epg.112114.xyz.test.js index ea6647c8..9e0b7acd 100644 --- a/sites/epg.112114.xyz/epg.112114.xyz.test.js +++ b/sites/epg.112114.xyz/epg.112114.xyz.test.js @@ -1,42 +1,42 @@ -const { parser, url } = require('./epg.112114.xyz.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const fs = require('fs') -const path = require('path') - -dayjs.extend(utc) -dayjs.extend(timezone) - -const date = dayjs.utc('2025-01-11', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'BTV文艺', xmltv_id: 'BRTVArtsChannel.cn', lang: 'zh' } - -it('can generate valid url', () => { - expect(url).toBe('https://epg.112114.xyz/pp.xml') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml')) - const results = parser({ date, content, channel }) - - expect(results.length).toBe(28) - expect(results[0]).toMatchObject({ - start: '2025-01-11T00:07:00.000Z', - stop: '2025-01-11T00:24:00.000Z', - title: '每日文艺播报' - }) - expect(results[27]).toMatchObject({ - start: '2025-01-11T15:16:00.000Z', - stop: '2025-01-11T15:59:00.000Z', - title: '笑动剧场' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./epg.112114.xyz.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const fs = require('fs') +const path = require('path') + +dayjs.extend(utc) +dayjs.extend(timezone) + +const date = dayjs.utc('2025-01-11', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'BTV文艺', xmltv_id: 'BRTVArtsChannel.cn', lang: 'zh' } + +it('can generate valid url', () => { + expect(url).toBe('https://epg.112114.xyz/pp.xml') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml')) + const results = parser({ date, content, channel }) + + expect(results.length).toBe(28) + expect(results[0]).toMatchObject({ + start: '2025-01-11T00:07:00.000Z', + stop: '2025-01-11T00:24:00.000Z', + title: '每日文艺播报' + }) + expect(results[27]).toMatchObject({ + start: '2025-01-11T15:16:00.000Z', + stop: '2025-01-11T15:59:00.000Z', + title: '笑动剧场' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/epg.iptvx.one/epg.iptvx.one.config.js b/sites/epg.iptvx.one/epg.iptvx.one.config.js index 2b4d9419..2087e8ae 100644 --- a/sites/epg.iptvx.one/epg.iptvx.one.config.js +++ b/sites/epg.iptvx.one/epg.iptvx.one.config.js @@ -1,64 +1,64 @@ -const axios = require('axios') -const iconv = require('iconv-lite') -const parser = require('epg-parser') -const { ungzip } = require('pako') - -let cachedContent - -module.exports = { - site: 'epg.iptvx.one', - days: 2, - url: 'https://iptvx.one/epg/epg_noarch.xml.gz', - request: { - maxContentLength: 500000000, // 500 MB - cache: { - ttl: 24 * 60 * 60 * 1000 // 1 day - } - }, - parser: function ({ buffer, channel, date, cached }) { - if (!cached) cachedContent = undefined - - let programs = [] - const items = parseItems(buffer, channel, date) - items.forEach(item => { - programs.push({ - title: item.title?.[0]?.value, - description: item.desc?.[0]?.value, - start: item.start, - stop: item.stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://epg.iptvx.one/api/channels.json') - .then(r => r.data) - .catch(console.log) - - return data.channels.map(channel => { - const [name] = channel.chan_names.split(' • ') - - return { - lang: 'ru', - site_id: channel.chan_id, - name - } - }) - } -} - -function parseItems(buffer, channel, date) { - if (!buffer) return [] - - if (!cachedContent) { - const content = ungzip(buffer) - const encoded = iconv.decode(content, 'utf8') - cachedContent = parser.parse(encoded) - } - - const { programs } = cachedContent - - return programs.filter(p => p.channel === channel.site_id && date.isSame(p.start, 'day')) -} +const axios = require('axios') +const iconv = require('iconv-lite') +const parser = require('epg-parser') +const { ungzip } = require('pako') + +let cachedContent + +module.exports = { + site: 'epg.iptvx.one', + days: 2, + url: 'https://iptvx.one/epg/epg_noarch.xml.gz', + request: { + maxContentLength: 500000000, // 500 MB + cache: { + ttl: 24 * 60 * 60 * 1000 // 1 day + } + }, + parser: function ({ buffer, channel, date, cached }) { + if (!cached) cachedContent = undefined + + let programs = [] + const items = parseItems(buffer, channel, date) + items.forEach(item => { + programs.push({ + title: item.title?.[0]?.value, + description: item.desc?.[0]?.value, + start: item.start, + stop: item.stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://epg.iptvx.one/api/channels.json') + .then(r => r.data) + .catch(console.log) + + return data.channels.map(channel => { + const [name] = channel.chan_names.split(' • ') + + return { + lang: 'ru', + site_id: channel.chan_id, + name + } + }) + } +} + +function parseItems(buffer, channel, date) { + if (!buffer) return [] + + if (!cachedContent) { + const content = ungzip(buffer) + const encoded = iconv.decode(content, 'utf8') + cachedContent = parser.parse(encoded) + } + + const { programs } = cachedContent + + return programs.filter(p => p.channel === channel.site_id && date.isSame(p.start, 'day')) +} diff --git a/sites/epg.iptvx.one/epg.iptvx.one.test.js b/sites/epg.iptvx.one/epg.iptvx.one.test.js index a35d14a4..1d3a3888 100644 --- a/sites/epg.iptvx.one/epg.iptvx.one.test.js +++ b/sites/epg.iptvx.one/epg.iptvx.one.test.js @@ -1,46 +1,46 @@ -const { parser, url } = require('./epg.iptvx.one.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const fs = require('fs') -const path = require('path') - -dayjs.extend(utc) -dayjs.extend(timezone) - -const date = dayjs.utc('2025-01-13', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: '12-omsk', xmltv_id: 'Channel12.ru' } - -it('can generate valid url', () => { - expect(url).toBe('https://iptvx.one/epg/epg_noarch.xml.gz') -}) - -it('can parse response', () => { - const buffer = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml.gz')) - const results = parser({ date, buffer, channel }) - - expect(results.length).toBe(29) - expect(results[0]).toMatchObject({ - start: '2025-01-13T00:00:00.000Z', - stop: '2025-01-13T00:55:00.000Z', - title: 'Акценты недели', - description: - 'Программа расскажет зрителям о том, как развивались самые яркие события недели, поможет расставить акценты над самыми обсуждаемыми новостями. Россия, ток-шоу' - }) - expect(results[28]).toMatchObject({ - start: '2025-01-13T22:15:00.000Z', - stop: '2025-01-14T00:00:00.000Z', - title: 'д/с Необыкновенные люди', - description: - 'Герои цикла – врачи, спортсмены, представители творческих профессий, волонтеры и многие-многие другие. Их деятельность связана с жизнью особенных людей. Россия, док. сериал' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - buffer: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./epg.iptvx.one.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const fs = require('fs') +const path = require('path') + +dayjs.extend(utc) +dayjs.extend(timezone) + +const date = dayjs.utc('2025-01-13', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: '12-omsk', xmltv_id: 'Channel12.ru' } + +it('can generate valid url', () => { + expect(url).toBe('https://iptvx.one/epg/epg_noarch.xml.gz') +}) + +it('can parse response', () => { + const buffer = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml.gz')) + const results = parser({ date, buffer, channel }) + + expect(results.length).toBe(29) + expect(results[0]).toMatchObject({ + start: '2025-01-13T00:00:00.000Z', + stop: '2025-01-13T00:55:00.000Z', + title: 'Акценты недели', + description: + 'Программа расскажет зрителям о том, как развивались самые яркие события недели, поможет расставить акценты над самыми обсуждаемыми новостями. Россия, ток-шоу' + }) + expect(results[28]).toMatchObject({ + start: '2025-01-13T22:15:00.000Z', + stop: '2025-01-14T00:00:00.000Z', + title: 'д/с Необыкновенные люди', + description: + 'Герои цикла – врачи, спортсмены, представители творческих профессий, волонтеры и многие-многие другие. Их деятельность связана с жизнью особенных людей. Россия, док. сериал' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + buffer: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/epg.telemach.ba/epg.telemach.ba.config.js b/sites/epg.telemach.ba/epg.telemach.ba.config.js index ccb9c978..b927fc02 100644 --- a/sites/epg.telemach.ba/epg.telemach.ba.config.js +++ b/sites/epg.telemach.ba/epg.telemach.ba.config.js @@ -1,100 +1,100 @@ -const dayjs = require('dayjs') -const axios = require('axios') - -const BASIC_TOKEN = - 'MjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1OjEyejJzMXJ3bXdhZmsxMGNkdzl0cjloOWFjYjZwdjJoZDhscXZ0aGc=' - -let session - -module.exports = { - site: 'epg.telemach.ba', - days: 3, - url({ channel, date }) { - return `https://api-web.ug-be.cdn.united.cloud/v1/public/events/epg?fromTime=${date.format( - 'YYYY-MM-DDTHH:mm:ss-00:00' - )}&toTime=${date - .add(1, 'days') - .subtract(1, 's') - .format('YYYY-MM-DDTHH:mm:ss-00:00')}&communityId=12&languageId=59&cid=${channel.site_id}` - }, - request: { - async headers() { - if (!session) { - session = await loadSessionDetails() - if (!session || !session.access_token) return null - } - - return { - Authorization: `Bearer ${session.access_token}` - } - } - }, - parser({ content }) { - try { - const programs = [] - const data = JSON.parse(content) - for (const channelId in data) { - if (Array.isArray(data[channelId])) { - data[channelId].forEach(item => { - programs.push({ - title: item.title, - description: item.shortDescription, - image: parseImage(item), - season: item.seasonNumber, - episode: item.episodeNumber, - start: dayjs(item.startTime), - stop: dayjs(item.endTime) - }) - }) - } - } - - return programs - } catch { - return [] - } - }, - async channels() { - const session = await loadSessionDetails() - if (!session || !session.access_token) return null - - const data = await axios - .get( - 'https://api-web.ug-be.cdn.united.cloud/v1/public/channels?channelType=TV&communityId=12&languageId=59&imageSize=L', - { - headers: { - Authorization: `Bearer ${session.access_token}` - } - } - ) - .then(r => r.data) - .catch(console.error) - - return data.map(item => ({ - lang: 'hr', - site_id: item.id, - name: item.name - })) - } -} - -function parseImage(item) { - const baseURL = 'https://images-web.ug-be.cdn.united.cloud' - - return Array.isArray(item?.images) && item.images[0] ? `${baseURL}${item.images[0].path}` : null -} - -function loadSessionDetails() { - return axios - .post( - 'https://api-web.ug-be.cdn.united.cloud/oauth/token?grant_type=client_credentials', - {}, - { - headers: { - Authorization: `Basic ${BASIC_TOKEN}` - } - } - ) - .then(r => r.data) - .catch(console.log) -} +const dayjs = require('dayjs') +const axios = require('axios') + +const BASIC_TOKEN = + 'MjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1OjEyejJzMXJ3bXdhZmsxMGNkdzl0cjloOWFjYjZwdjJoZDhscXZ0aGc=' + +let session + +module.exports = { + site: 'epg.telemach.ba', + days: 3, + url({ channel, date }) { + return `https://api-web.ug-be.cdn.united.cloud/v1/public/events/epg?fromTime=${date.format( + 'YYYY-MM-DDTHH:mm:ss-00:00' + )}&toTime=${date + .add(1, 'days') + .subtract(1, 's') + .format('YYYY-MM-DDTHH:mm:ss-00:00')}&communityId=12&languageId=59&cid=${channel.site_id}` + }, + request: { + async headers() { + if (!session) { + session = await loadSessionDetails() + if (!session || !session.access_token) return null + } + + return { + Authorization: `Bearer ${session.access_token}` + } + } + }, + parser({ content }) { + try { + const programs = [] + const data = JSON.parse(content) + for (const channelId in data) { + if (Array.isArray(data[channelId])) { + data[channelId].forEach(item => { + programs.push({ + title: item.title, + description: item.shortDescription, + image: parseImage(item), + season: item.seasonNumber, + episode: item.episodeNumber, + start: dayjs(item.startTime), + stop: dayjs(item.endTime) + }) + }) + } + } + + return programs + } catch { + return [] + } + }, + async channels() { + const session = await loadSessionDetails() + if (!session || !session.access_token) return null + + const data = await axios + .get( + 'https://api-web.ug-be.cdn.united.cloud/v1/public/channels?channelType=TV&communityId=12&languageId=59&imageSize=L', + { + headers: { + Authorization: `Bearer ${session.access_token}` + } + } + ) + .then(r => r.data) + .catch(console.error) + + return data.map(item => ({ + lang: 'hr', + site_id: item.id, + name: item.name + })) + } +} + +function parseImage(item) { + const baseURL = 'https://images-web.ug-be.cdn.united.cloud' + + return Array.isArray(item?.images) && item.images[0] ? `${baseURL}${item.images[0].path}` : null +} + +function loadSessionDetails() { + return axios + .post( + 'https://api-web.ug-be.cdn.united.cloud/oauth/token?grant_type=client_credentials', + {}, + { + headers: { + Authorization: `Basic ${BASIC_TOKEN}` + } + } + ) + .then(r => r.data) + .catch(console.log) +} diff --git a/sites/epg.telemach.ba/epg.telemach.ba.test.js b/sites/epg.telemach.ba/epg.telemach.ba.test.js index 56836b4d..51819344 100644 --- a/sites/epg.telemach.ba/epg.telemach.ba.test.js +++ b/sites/epg.telemach.ba/epg.telemach.ba.test.js @@ -1,94 +1,94 @@ -const { parser, url, request } = require('./epg.telemach.ba.config.js') -const fs = require('fs') -const axios = require('axios') -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) - -jest.mock('axios') - -axios.post.mockImplementation((url, data, opts) => { - if ( - url === 'https://api-web.ug-be.cdn.united.cloud/oauth/token?grant_type=client_credentials' && - JSON.stringify(opts.headers) === - JSON.stringify({ - Authorization: - 'Basic MjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1OjEyejJzMXJ3bXdhZmsxMGNkdzl0cjloOWFjYjZwdjJoZDhscXZ0aGc=' - }) - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/session.json'))) - }) - } else { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/no_session.json'))) - }) - } -}) - -const date = dayjs.utc('2025-01-20', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1607', - xmltv_id: 'N1HD.hr' -} - -it('can generate valid url', async () => { - const result = url({ date, channel }) - - expect(result).toBe( - 'https://api-web.ug-be.cdn.united.cloud/v1/public/events/epg?fromTime=2025-01-20T00:00:00-00:00&toTime=2025-01-20T23:59:59-00:00&communityId=12&languageId=59&cid=1607' - ) -}) - -it('can generate valid request headers', async () => { - const result = await request.headers() - - expect(result).toMatchObject({ - Authorization: - 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidWMtaW5mby1zZXJ2aWNlIl0sInNjb3BlIjpbInJlYWQiXSwiZXhwIjoxNzM3Mzc3NDUxLCJhdXRob3JpdGllcyI6WyJST0xFX1BVQkxJQ19FUEciXSwianRpIjoiUVBubHdRSDczS1EwSnU0WDZwRTc2Zm5mUmRnIiwiY2xpZW50X2lkIjoiMjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1In0.LqJAZUWEqIOcLrRSMpxZxnF-f1arKbHgfweLMXt-MBjCDbVJD39OQEsh_b68mtePAoa3n8LRbf3IFT40Ys5Vbe-k_Btm4a9gdEGr6cNi_4HGk4Bto6RUDvCp59VRfoRZhWe145Q2b5TS6szmC4Ws2YWIcZU5vrJcYs2GZiCk6U11MOcd1i52WmZj8cLPq0ZPDB_bzmTgYkvkVa7zOzUOPSl4M8T6fPUa__vVKUt0jOgtFoHeue2mQVgISC2puEGsBN0jJwvJ8PzM6IVxXrQno3MBv0VJy_qILiFPcxRePGRAmKLuEqagvikO7P_XQgFjZgg-j8u8wX2WwO0Yxft0Pg' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') - let results = parser({ content }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(35) - expect(results[0]).toMatchObject({ - start: '2025-01-20T00:00:00.000Z', - stop: '2025-01-20T00:30:00.000Z', - title: 'DW Euromaxx', - description: - 'Euromaxx je lifestyle Europe magazine, koji nam donosi zanimljivosti iz evropskih gradova, priče o načinu života ljudi i upoznaje nas sa njihovim kulturama.', - image: - 'https://images-web.ug-be.cdn.united.cloud/2021/02/18/06/05/21/stb_xl_cd4f72e01d308ecce782e29b69af7de6707b9e85.jpg', - season: null, - episode: null - }) - expect(results[34]).toMatchObject({ - start: '2025-01-20T23:50:00.000Z', - stop: '2025-01-21T00:00:00.000Z', - title: 'DW Shift', - description: 'Tjedni magazin koji nam donosi najnovije vijesti vezane za Internet.', - image: - 'https://images-web.ug-be.cdn.united.cloud/2023/06/09/13/07/53/stb_xl_0849d5d70c1337651b85b6335e340e15bd5d6a73_340fc454bc73019d052cf936ebee5da3.jpg', - season: null, - episode: null - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8') - }) - - expect(results).toMatchObject([]) -}) +const { parser, url, request } = require('./epg.telemach.ba.config.js') +const fs = require('fs') +const axios = require('axios') +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) + +jest.mock('axios') + +axios.post.mockImplementation((url, data, opts) => { + if ( + url === 'https://api-web.ug-be.cdn.united.cloud/oauth/token?grant_type=client_credentials' && + JSON.stringify(opts.headers) === + JSON.stringify({ + Authorization: + 'Basic MjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1OjEyejJzMXJ3bXdhZmsxMGNkdzl0cjloOWFjYjZwdjJoZDhscXZ0aGc=' + }) + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/session.json'))) + }) + } else { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/no_session.json'))) + }) + } +}) + +const date = dayjs.utc('2025-01-20', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1607', + xmltv_id: 'N1HD.hr' +} + +it('can generate valid url', async () => { + const result = url({ date, channel }) + + expect(result).toBe( + 'https://api-web.ug-be.cdn.united.cloud/v1/public/events/epg?fromTime=2025-01-20T00:00:00-00:00&toTime=2025-01-20T23:59:59-00:00&communityId=12&languageId=59&cid=1607' + ) +}) + +it('can generate valid request headers', async () => { + const result = await request.headers() + + expect(result).toMatchObject({ + Authorization: + 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidWMtaW5mby1zZXJ2aWNlIl0sInNjb3BlIjpbInJlYWQiXSwiZXhwIjoxNzM3Mzc3NDUxLCJhdXRob3JpdGllcyI6WyJST0xFX1BVQkxJQ19FUEciXSwianRpIjoiUVBubHdRSDczS1EwSnU0WDZwRTc2Zm5mUmRnIiwiY2xpZW50X2lkIjoiMjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1In0.LqJAZUWEqIOcLrRSMpxZxnF-f1arKbHgfweLMXt-MBjCDbVJD39OQEsh_b68mtePAoa3n8LRbf3IFT40Ys5Vbe-k_Btm4a9gdEGr6cNi_4HGk4Bto6RUDvCp59VRfoRZhWe145Q2b5TS6szmC4Ws2YWIcZU5vrJcYs2GZiCk6U11MOcd1i52WmZj8cLPq0ZPDB_bzmTgYkvkVa7zOzUOPSl4M8T6fPUa__vVKUt0jOgtFoHeue2mQVgISC2puEGsBN0jJwvJ8PzM6IVxXrQno3MBv0VJy_qILiFPcxRePGRAmKLuEqagvikO7P_XQgFjZgg-j8u8wX2WwO0Yxft0Pg' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') + let results = parser({ content }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(35) + expect(results[0]).toMatchObject({ + start: '2025-01-20T00:00:00.000Z', + stop: '2025-01-20T00:30:00.000Z', + title: 'DW Euromaxx', + description: + 'Euromaxx je lifestyle Europe magazine, koji nam donosi zanimljivosti iz evropskih gradova, priče o načinu života ljudi i upoznaje nas sa njihovim kulturama.', + image: + 'https://images-web.ug-be.cdn.united.cloud/2021/02/18/06/05/21/stb_xl_cd4f72e01d308ecce782e29b69af7de6707b9e85.jpg', + season: null, + episode: null + }) + expect(results[34]).toMatchObject({ + start: '2025-01-20T23:50:00.000Z', + stop: '2025-01-21T00:00:00.000Z', + title: 'DW Shift', + description: 'Tjedni magazin koji nam donosi najnovije vijesti vezane za Internet.', + image: + 'https://images-web.ug-be.cdn.united.cloud/2023/06/09/13/07/53/stb_xl_0849d5d70c1337651b85b6335e340e15bd5d6a73_340fc454bc73019d052cf936ebee5da3.jpg', + season: null, + episode: null + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8') + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/epg.telemach.me/epg.telemach.me.config.js b/sites/epg.telemach.me/epg.telemach.me.config.js index 46522150..d559882a 100644 --- a/sites/epg.telemach.me/epg.telemach.me.config.js +++ b/sites/epg.telemach.me/epg.telemach.me.config.js @@ -1,101 +1,101 @@ -const dayjs = require('dayjs') -const axios = require('axios') - -const BASIC_TOKEN = - 'MjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1OjEyejJzMXJ3bXdhZmsxMGNkdzl0cjloOWFjYjZwdjJoZDhscXZ0aGc=' - -let session - -module.exports = { - site: 'epg.telemach.me', - days: 3, - url({ channel, date }) { - return `https://api-web.ug-be.cdn.united.cloud/v1/public/events/epg?fromTime=${date.format( - 'YYYY-MM-DDTHH:mm:ss-00:00' - )}&toTime=${date - .add(1, 'days') - .subtract(1, 's') - .format('YYYY-MM-DDTHH:mm:ss-00:00')}&communityId=5&languageId=10001&cid=${channel.site_id}` - }, - request: { - async headers() { - if (!session) { - session = await loadSessionDetails() - if (!session || !session.access_token) return null - } - - return { - Authorization: `Bearer ${session.access_token}`, - Referer: 'https://epg.telemach.me/' - } - } - }, - parser({ content }) { - try { - const programs = [] - const data = JSON.parse(content) - for (const channelId in data) { - if (Array.isArray(data[channelId])) { - data[channelId].forEach(item => { - programs.push({ - title: item.title, - description: item.shortDescription, - image: parseImage(item), - season: item.seasonNumber, - episode: item.episodeNumber, - start: dayjs(item.startTime), - stop: dayjs(item.endTime) - }) - }) - } - } - - return programs - } catch { - return [] - } - }, - async channels() { - const session = await loadSessionDetails() - if (!session || !session.access_token) return null - - const data = await axios - .get( - 'https://api-web.ug-be.cdn.united.cloud/v1/public/channels?channelType=TV&communityId=5&languageId=10001&imageSize=L', - { - headers: { - Authorization: `Bearer ${session.access_token}` - } - } - ) - .then(r => r.data) - .catch(console.error) - - return data.map(item => ({ - lang: 'bs', - site_id: item.id, - name: item.name - })) - } -} - -function parseImage(item) { - const baseURL = 'https://images-web.ug-be.cdn.united.cloud' - - return Array.isArray(item?.images) && item.images[0] ? `${baseURL}${item.images[0].path}` : null -} - -function loadSessionDetails() { - return axios - .post( - 'https://api-web.ug-be.cdn.united.cloud/oauth/token?grant_type=client_credentials', - {}, - { - headers: { - Authorization: `Basic ${BASIC_TOKEN}` - } - } - ) - .then(r => r.data) - .catch(console.log) -} +const dayjs = require('dayjs') +const axios = require('axios') + +const BASIC_TOKEN = + 'MjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1OjEyejJzMXJ3bXdhZmsxMGNkdzl0cjloOWFjYjZwdjJoZDhscXZ0aGc=' + +let session + +module.exports = { + site: 'epg.telemach.me', + days: 3, + url({ channel, date }) { + return `https://api-web.ug-be.cdn.united.cloud/v1/public/events/epg?fromTime=${date.format( + 'YYYY-MM-DDTHH:mm:ss-00:00' + )}&toTime=${date + .add(1, 'days') + .subtract(1, 's') + .format('YYYY-MM-DDTHH:mm:ss-00:00')}&communityId=5&languageId=10001&cid=${channel.site_id}` + }, + request: { + async headers() { + if (!session) { + session = await loadSessionDetails() + if (!session || !session.access_token) return null + } + + return { + Authorization: `Bearer ${session.access_token}`, + Referer: 'https://epg.telemach.me/' + } + } + }, + parser({ content }) { + try { + const programs = [] + const data = JSON.parse(content) + for (const channelId in data) { + if (Array.isArray(data[channelId])) { + data[channelId].forEach(item => { + programs.push({ + title: item.title, + description: item.shortDescription, + image: parseImage(item), + season: item.seasonNumber, + episode: item.episodeNumber, + start: dayjs(item.startTime), + stop: dayjs(item.endTime) + }) + }) + } + } + + return programs + } catch { + return [] + } + }, + async channels() { + const session = await loadSessionDetails() + if (!session || !session.access_token) return null + + const data = await axios + .get( + 'https://api-web.ug-be.cdn.united.cloud/v1/public/channels?channelType=TV&communityId=5&languageId=10001&imageSize=L', + { + headers: { + Authorization: `Bearer ${session.access_token}` + } + } + ) + .then(r => r.data) + .catch(console.error) + + return data.map(item => ({ + lang: 'bs', + site_id: item.id, + name: item.name + })) + } +} + +function parseImage(item) { + const baseURL = 'https://images-web.ug-be.cdn.united.cloud' + + return Array.isArray(item?.images) && item.images[0] ? `${baseURL}${item.images[0].path}` : null +} + +function loadSessionDetails() { + return axios + .post( + 'https://api-web.ug-be.cdn.united.cloud/oauth/token?grant_type=client_credentials', + {}, + { + headers: { + Authorization: `Basic ${BASIC_TOKEN}` + } + } + ) + .then(r => r.data) + .catch(console.log) +} diff --git a/sites/epg.telemach.me/epg.telemach.me.test.js b/sites/epg.telemach.me/epg.telemach.me.test.js index ab0ac6c4..5299ab27 100644 --- a/sites/epg.telemach.me/epg.telemach.me.test.js +++ b/sites/epg.telemach.me/epg.telemach.me.test.js @@ -1,96 +1,96 @@ -const { parser, url, request } = require('./epg.telemach.me.config.js') -const fs = require('fs') -const axios = require('axios') -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) - -jest.mock('axios') - -axios.post.mockImplementation((url, data, opts) => { - if ( - url === 'https://api-web.ug-be.cdn.united.cloud/oauth/token?grant_type=client_credentials' && - JSON.stringify(opts.headers) === - JSON.stringify({ - Authorization: - 'Basic MjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1OjEyejJzMXJ3bXdhZmsxMGNkdzl0cjloOWFjYjZwdjJoZDhscXZ0aGc=' - }) - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/session.json'))) - }) - } else { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/no_session.json'))) - }) - } -}) - -const date = dayjs.utc('2025-01-20', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '92', - xmltv_id: 'PinkKids.rs' -} - -it('can generate valid url', async () => { - const result = url({ date, channel }) - - expect(result).toBe( - 'https://api-web.ug-be.cdn.united.cloud/v1/public/events/epg?fromTime=2025-01-20T00:00:00-00:00&toTime=2025-01-20T23:59:59-00:00&communityId=5&languageId=10001&cid=92' - ) -}) - -it('can generate valid request headers', async () => { - const result = await request.headers() - - expect(result).toMatchObject({ - Authorization: - 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidWMtaW5mby1zZXJ2aWNlIl0sInNjb3BlIjpbInJlYWQiXSwiZXhwIjoxNzM3Mzc3NDUxLCJhdXRob3JpdGllcyI6WyJST0xFX1BVQkxJQ19FUEciXSwianRpIjoiUVBubHdRSDczS1EwSnU0WDZwRTc2Zm5mUmRnIiwiY2xpZW50X2lkIjoiMjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1In0.LqJAZUWEqIOcLrRSMpxZxnF-f1arKbHgfweLMXt-MBjCDbVJD39OQEsh_b68mtePAoa3n8LRbf3IFT40Ys5Vbe-k_Btm4a9gdEGr6cNi_4HGk4Bto6RUDvCp59VRfoRZhWe145Q2b5TS6szmC4Ws2YWIcZU5vrJcYs2GZiCk6U11MOcd1i52WmZj8cLPq0ZPDB_bzmTgYkvkVa7zOzUOPSl4M8T6fPUa__vVKUt0jOgtFoHeue2mQVgISC2puEGsBN0jJwvJ8PzM6IVxXrQno3MBv0VJy_qILiFPcxRePGRAmKLuEqagvikO7P_XQgFjZgg-j8u8wX2WwO0Yxft0Pg', - Referer: 'https://epg.telemach.me/' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') - let results = parser({ content }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(55) - expect(results[0]).toMatchObject({ - start: '2025-01-19T23:20:00.000Z', - stop: '2025-01-20T00:10:00.000Z', - title: 'Pinkove Zvezdice', - description: - 'Četvrta sezona najgledanijeg dečijeg muzičkog takmičenja, "Pinkove zvezdice" došlo do promena, pa će tako gledaoci imati priliku da najtalentovaniju decu gledaju na novoj, spektakularnoj sceni. Nova...', - image: - 'https://images-web.ug-be.cdn.united.cloud/2023/06/22/11/19/19/stb_xl_115752ec1e05872b86ceda7726d347f533e17f43_340fc454bc73019d052cf936ebee5da3.jpg', - season: null, - episode: null - }) - expect(results[54]).toMatchObject({ - start: '2025-01-20T23:50:00.000Z', - stop: '2025-01-21T00:10:00.000Z', - title: 'Hajdi', - description: - 'Život nekada nije jednostavan. To najbolje zna Hajdi. Nakon što je ostala siroče, njena tetka je odvodi visoko u Alpe kod njenog dede. Ona uz nove prijatelje i dedu uskoro zavoli svoj novi život. Ipak...', - image: - 'https://images-web.ug-be.cdn.united.cloud/2024/05/10/14/49/09/stb_xl_7d1c73ee4df7de5c4157e9daccae098d50ee853d_99230e7f5bdc95451f37aa31f8425b4d.jpg', - season: null, - episode: null - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8') - }) - - expect(results).toMatchObject([]) -}) +const { parser, url, request } = require('./epg.telemach.me.config.js') +const fs = require('fs') +const axios = require('axios') +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) + +jest.mock('axios') + +axios.post.mockImplementation((url, data, opts) => { + if ( + url === 'https://api-web.ug-be.cdn.united.cloud/oauth/token?grant_type=client_credentials' && + JSON.stringify(opts.headers) === + JSON.stringify({ + Authorization: + 'Basic MjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1OjEyejJzMXJ3bXdhZmsxMGNkdzl0cjloOWFjYjZwdjJoZDhscXZ0aGc=' + }) + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/session.json'))) + }) + } else { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/no_session.json'))) + }) + } +}) + +const date = dayjs.utc('2025-01-20', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '92', + xmltv_id: 'PinkKids.rs' +} + +it('can generate valid url', async () => { + const result = url({ date, channel }) + + expect(result).toBe( + 'https://api-web.ug-be.cdn.united.cloud/v1/public/events/epg?fromTime=2025-01-20T00:00:00-00:00&toTime=2025-01-20T23:59:59-00:00&communityId=5&languageId=10001&cid=92' + ) +}) + +it('can generate valid request headers', async () => { + const result = await request.headers() + + expect(result).toMatchObject({ + Authorization: + 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidWMtaW5mby1zZXJ2aWNlIl0sInNjb3BlIjpbInJlYWQiXSwiZXhwIjoxNzM3Mzc3NDUxLCJhdXRob3JpdGllcyI6WyJST0xFX1BVQkxJQ19FUEciXSwianRpIjoiUVBubHdRSDczS1EwSnU0WDZwRTc2Zm5mUmRnIiwiY2xpZW50X2lkIjoiMjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1In0.LqJAZUWEqIOcLrRSMpxZxnF-f1arKbHgfweLMXt-MBjCDbVJD39OQEsh_b68mtePAoa3n8LRbf3IFT40Ys5Vbe-k_Btm4a9gdEGr6cNi_4HGk4Bto6RUDvCp59VRfoRZhWe145Q2b5TS6szmC4Ws2YWIcZU5vrJcYs2GZiCk6U11MOcd1i52WmZj8cLPq0ZPDB_bzmTgYkvkVa7zOzUOPSl4M8T6fPUa__vVKUt0jOgtFoHeue2mQVgISC2puEGsBN0jJwvJ8PzM6IVxXrQno3MBv0VJy_qILiFPcxRePGRAmKLuEqagvikO7P_XQgFjZgg-j8u8wX2WwO0Yxft0Pg', + Referer: 'https://epg.telemach.me/' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') + let results = parser({ content }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(55) + expect(results[0]).toMatchObject({ + start: '2025-01-19T23:20:00.000Z', + stop: '2025-01-20T00:10:00.000Z', + title: 'Pinkove Zvezdice', + description: + 'Četvrta sezona najgledanijeg dečijeg muzičkog takmičenja, "Pinkove zvezdice" došlo do promena, pa će tako gledaoci imati priliku da najtalentovaniju decu gledaju na novoj, spektakularnoj sceni. Nova...', + image: + 'https://images-web.ug-be.cdn.united.cloud/2023/06/22/11/19/19/stb_xl_115752ec1e05872b86ceda7726d347f533e17f43_340fc454bc73019d052cf936ebee5da3.jpg', + season: null, + episode: null + }) + expect(results[54]).toMatchObject({ + start: '2025-01-20T23:50:00.000Z', + stop: '2025-01-21T00:10:00.000Z', + title: 'Hajdi', + description: + 'Život nekada nije jednostavan. To najbolje zna Hajdi. Nakon što je ostala siroče, njena tetka je odvodi visoko u Alpe kod njenog dede. Ona uz nove prijatelje i dedu uskoro zavoli svoj novi život. Ipak...', + image: + 'https://images-web.ug-be.cdn.united.cloud/2024/05/10/14/49/09/stb_xl_7d1c73ee4df7de5c4157e9daccae098d50ee853d_99230e7f5bdc95451f37aa31f8425b4d.jpg', + season: null, + episode: null + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8') + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/epgmaster.com/epgmaster.com.config.js b/sites/epgmaster.com/epgmaster.com.config.js index deedd45a..e1ed791a 100644 --- a/sites/epgmaster.com/epgmaster.com.config.js +++ b/sites/epgmaster.com/epgmaster.com.config.js @@ -1,45 +1,45 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const TOKEN = '1610283054' - -module.exports = { - site: 'epgmaster.com', - url({ channel }) { - return `https://epgmaster.com/api/channels/${channel.site_id}/epgs?token=${TOKEN}` - }, - parser({ content, date }) { - return parseItems(content, date).map(item => { - return { - title: item.programName, - start: parseStart(item), - stop: parseStop(item) - } - }) - } -} - -function parseStart(item) { - return dayjs.utc(`${item.startDate} ${item.startTime}`, 'YYYY-MM-DD HH:mm:ss') -} - -function parseStop(item) { - return dayjs.utc(`${item.startDate} ${item.endTime}`, 'YYYY-MM-DD HH:mm:ss') -} - -function parseItems(content, date) { - try { - const data = JSON.parse(content) - if (!data || !Array.isArray(data)) return [] - const filtered = data.find(group => date.format('YYYY-MM-DD') === group.date) - if (!filtered || !Array.isArray(filtered.epgTokenList)) return [] - - return filtered.epgTokenList - } catch { - return [] - } -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const TOKEN = '1610283054' + +module.exports = { + site: 'epgmaster.com', + url({ channel }) { + return `https://epgmaster.com/api/channels/${channel.site_id}/epgs?token=${TOKEN}` + }, + parser({ content, date }) { + return parseItems(content, date).map(item => { + return { + title: item.programName, + start: parseStart(item), + stop: parseStop(item) + } + }) + } +} + +function parseStart(item) { + return dayjs.utc(`${item.startDate} ${item.startTime}`, 'YYYY-MM-DD HH:mm:ss') +} + +function parseStop(item) { + return dayjs.utc(`${item.startDate} ${item.endTime}`, 'YYYY-MM-DD HH:mm:ss') +} + +function parseItems(content, date) { + try { + const data = JSON.parse(content) + if (!data || !Array.isArray(data)) return [] + const filtered = data.find(group => date.format('YYYY-MM-DD') === group.date) + if (!filtered || !Array.isArray(filtered.epgTokenList)) return [] + + return filtered.epgTokenList + } catch { + return [] + } +} diff --git a/sites/epgmaster.com/epgmaster.com.test.js b/sites/epgmaster.com/epgmaster.com.test.js index f1f5c46b..544d34b8 100644 --- a/sites/epgmaster.com/epgmaster.com.test.js +++ b/sites/epgmaster.com/epgmaster.com.test.js @@ -1,45 +1,45 @@ -const { parser, url } = require('./epgmaster.com.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-05-18', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'ntv' } - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://epgmaster.com/api/channels/ntv/epgs?token=1610283054') -}) - -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.length).toBe(46) - expect(results[0]).toMatchObject({ - title: 'Krishi Teleflim-Bharosa Yuwama', - start: '2025-05-18T00:00:00.000Z', - stop: '2025-05-18T00:15:00.000Z' - }) - expect(results[1]).toMatchObject({ - title: 'News in Nepali [Rec.]', - start: '2025-05-18T00:15:00.000Z', - stop: '2025-05-18T00:45:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ content: '', date }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./epgmaster.com.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-05-18', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'ntv' } + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://epgmaster.com/api/channels/ntv/epgs?token=1610283054') +}) + +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.length).toBe(46) + expect(results[0]).toMatchObject({ + title: 'Krishi Teleflim-Bharosa Yuwama', + start: '2025-05-18T00:00:00.000Z', + stop: '2025-05-18T00:15:00.000Z' + }) + expect(results[1]).toMatchObject({ + title: 'News in Nepali [Rec.]', + start: '2025-05-18T00:15:00.000Z', + stop: '2025-05-18T00:45:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ content: '', date }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/epgshare01.online/epgshare01.online.config.js b/sites/epgshare01.online/epgshare01.online.config.js index 767e09b3..f3cd17b7 100644 --- a/sites/epgshare01.online/epgshare01.online.config.js +++ b/sites/epgshare01.online/epgshare01.online.config.js @@ -1,75 +1,75 @@ -const axios = require('axios') -const iconv = require('iconv-lite') -const parser = require('epg-parser') -const { ungzip } = require('pako') - -let cachedContent - -module.exports = { - site: 'epgshare01.online', - days: 2, - url({ channel }) { - const [tag] = channel.site_id.split('#') - - return `https://epgshare01.online/epgshare01/epg_ripper_${tag}.xml.gz` - }, - request: { - cache: { - ttl: 24 * 60 * 60 * 1000 // 1 day - }, - maxContentLength: 100000000 // 100 MB - }, - parser({ buffer, channel, date, cached }) { - if (!cached) cachedContent = undefined - - let programs = [] - const items = parseItems(buffer, channel, date) - items.forEach(item => { - programs.push({ - title: item.title?.[0]?.value, - description: item.desc?.[0]?.value, - start: item.start, - stop: item.stop - }) - }) - - return programs - }, - async channels({ tag }) { - const buffer = await axios - .get(`https://epgshare01.online/epgshare01/epg_ripper_${tag}.xml.gz`, { - responseType: 'arraybuffer' - }) - .then(r => r.data) - .catch(console.error) - - const content = ungzip(buffer) - const encoded = iconv.decode(content, 'utf8') - const { channels } = parser.parse(encoded) - - return channels.map(channel => { - const displayName = channel.displayName[0] - - return { - lang: displayName.lang || 'en', - site_id: `${tag}#${channel.id}`, - name: displayName.value - } - }) - } -} - -function parseItems(buffer, channel, date) { - if (!buffer) return [] - - if (!cachedContent) { - const content = ungzip(buffer) - const encoded = iconv.decode(content, 'utf8') - cachedContent = parser.parse(encoded) - } - - const { programs } = cachedContent - const [, channelId] = channel.site_id.split('#') - - return programs.filter(p => p.channel === channelId && date.isSame(p.start, 'day')) -} +const axios = require('axios') +const iconv = require('iconv-lite') +const parser = require('epg-parser') +const { ungzip } = require('pako') + +let cachedContent + +module.exports = { + site: 'epgshare01.online', + days: 2, + url({ channel }) { + const [tag] = channel.site_id.split('#') + + return `https://epgshare01.online/epgshare01/epg_ripper_${tag}.xml.gz` + }, + request: { + cache: { + ttl: 24 * 60 * 60 * 1000 // 1 day + }, + maxContentLength: 100000000 // 100 MB + }, + parser({ buffer, channel, date, cached }) { + if (!cached) cachedContent = undefined + + let programs = [] + const items = parseItems(buffer, channel, date) + items.forEach(item => { + programs.push({ + title: item.title?.[0]?.value, + description: item.desc?.[0]?.value, + start: item.start, + stop: item.stop + }) + }) + + return programs + }, + async channels({ tag }) { + const buffer = await axios + .get(`https://epgshare01.online/epgshare01/epg_ripper_${tag}.xml.gz`, { + responseType: 'arraybuffer' + }) + .then(r => r.data) + .catch(console.error) + + const content = ungzip(buffer) + const encoded = iconv.decode(content, 'utf8') + const { channels } = parser.parse(encoded) + + return channels.map(channel => { + const displayName = channel.displayName[0] + + return { + lang: displayName.lang || 'en', + site_id: `${tag}#${channel.id}`, + name: displayName.value + } + }) + } +} + +function parseItems(buffer, channel, date) { + if (!buffer) return [] + + if (!cachedContent) { + const content = ungzip(buffer) + const encoded = iconv.decode(content, 'utf8') + cachedContent = parser.parse(encoded) + } + + const { programs } = cachedContent + const [, channelId] = channel.site_id.split('#') + + return programs.filter(p => p.channel === channelId && date.isSame(p.start, 'day')) +} diff --git a/sites/epgshare01.online/epgshare01.online.test.js b/sites/epgshare01.online/epgshare01.online.test.js index 88015351..f7646378 100644 --- a/sites/epgshare01.online/epgshare01.online.test.js +++ b/sites/epgshare01.online/epgshare01.online.test.js @@ -1,43 +1,43 @@ -const { parser, url } = require('./epgshare01.online.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-02-09', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'ALJAZEERA1#AlJazeera.English.net' } - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://epgshare01.online/epgshare01/epg_ripper_ALJAZEERA1.xml.gz') -}) - -it('can parse response', () => { - const buffer = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml.gz')) - - const results = parser({ buffer, channel, date, cached: false }) - - expect(results.length).toBe(40) - expect(results[0]).toMatchObject({ - title: 'The Palestine Laboratory', - description: - "Exposing how Israel's sales of military technology is aiding state control around the world.", - start: '2025-02-09T00:00:00.000Z', - stop: '2025-02-09T01:00:00.000Z' - }) - expect(results[39]).toMatchObject({ - title: 'Inside Story', - description: - 'Beyond the headlines to the heart of the news of the day. Al Jazeera gets the Inside Story from some of the best minds from around the globe.', - start: '2025-02-09T23:30:00.000Z', - stop: '2025-02-10T00:00:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ content: '', channel, date, cached: false }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./epgshare01.online.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-02-09', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'ALJAZEERA1#AlJazeera.English.net' } + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://epgshare01.online/epgshare01/epg_ripper_ALJAZEERA1.xml.gz') +}) + +it('can parse response', () => { + const buffer = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml.gz')) + + const results = parser({ buffer, channel, date, cached: false }) + + expect(results.length).toBe(40) + expect(results[0]).toMatchObject({ + title: 'The Palestine Laboratory', + description: + "Exposing how Israel's sales of military technology is aiding state control around the world.", + start: '2025-02-09T00:00:00.000Z', + stop: '2025-02-09T01:00:00.000Z' + }) + expect(results[39]).toMatchObject({ + title: 'Inside Story', + description: + 'Beyond the headlines to the heart of the news of the day. Al Jazeera gets the Inside Story from some of the best minds from around the globe.', + start: '2025-02-09T23:30:00.000Z', + stop: '2025-02-10T00:00:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ content: '', channel, date, cached: false }) + + expect(results).toMatchObject([]) +}) 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/firstmedia.com/__data__/content.json b/sites/firstmedia.com/__data__/content.json new file mode 100644 index 00000000..c6ec2901 --- /dev/null +++ b/sites/firstmedia.com/__data__/content.json @@ -0,0 +1 @@ +{"data":{"entries":{"243":[{"createdAt":"2023-11-05T17:09:34.000Z","updatedAt":"2023-11-05T17:09:34.000Z","id":"009f3a34-8164-4ff9-b981-9dcab1a518fc","channelNo":"243","programmeId":null,"title":"News Live","episode":null,"slug":"news-live","date":"2023-11-08 17:00:00","startTime":"2023-11-08 20:00:00","endTime":"2023-11-08 20:30:00","length":1800,"description":"News Live","long_description":"Up to date news and analysis from around the world.","status":true,"channel":{"id":"7fd7a9a6-af32-c861-d2b0-4ddc7846fad2","key":"AljaInt","no":243,"name":"Al Jazeera International","slug":"al-jazeera-international","website":null,"description":"

    An international 24-hour English-language It is the first English-language news channel brings you the latest global news stories, analysis from the Middle East & worldwide.

    ","shortDescription":null,"logo":"files/logos/channels/11-NEWS/AlJazeera Int SD-FirstMedia-Chl-243.jpg","externalId":"132","type":"radio","status":true,"chanel":"SD","locale":"id","relationId":"5a6ea4ae-a008-4889-9c68-7a6f1838e81d","onlyfm":null,"genress":[{"id":"1db3bb43-b00d-49af-b272-6c058a8c0b49","name":"International Free View"},{"id":"2e81a4bd-9719-4186-820a-7e035e07be13","name":"News"}]}}]}}} \ No newline at end of file diff --git a/sites/firstmedia.com/firstmedia.com.config.js b/sites/firstmedia.com/firstmedia.com.config.js index 7c6865fc..d80b29a6 100644 --- a/sites/firstmedia.com/firstmedia.com.config.js +++ b/sites/firstmedia.com/firstmedia.com.config.js @@ -1,102 +1,102 @@ -const dayjs = require('dayjs') -const timezone = require('dayjs/plugin/timezone') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(timezone) -dayjs.extend(utc) - -module.exports = { - site: 'firstmedia.com', - days: 2, - url({ channel, date }) { - return `https://api.firstmedia.com/api/content/tv-guide/list?date=${date.format( - 'DD/MM/YYYY' - )}&channel=${channel.site_id}&startTime=1&endTime=24` - }, - parser({ content, channel, date }) { - if (!content || !channel || !date) return [] - - const programs = [] - const items = parseItems(content, channel.site_id) - .map(item => { - item.start = toDelta(item.date, item.startTime) - item.stop = toDelta(item.date, item.endTime) - return item - }) - .sort((a, b) => a.start - b.start) - - const dt = date.tz('Asia/Jakarta').startOf('d') - let lastStop - items.forEach(item => { - if (lastStop === undefined || item.start >= lastStop) { - lastStop = item.stop - programs.push({ - title: parseTitle(item), - description: parseDescription(item), - start: asDate(parseStart({ item, date: dt })), - stop: asDate(parseStop({ item, date: dt })) - }) - } - }) - - return programs - }, - async channels() { - const axios = require('axios') - const result = await axios - .get( - `https://api.firstmedia.com/api/content/tv-guide/list?date=${dayjs().format( - 'DD/MM/YYYY' - )}&channel=&startTime=0&endTime=24` - ) - .then(response => response.data) - .catch(console.error) - - const channels = [] - if (result.data && result.data.entries) { - Object.values(result.data.entries).forEach(schedules => { - if (schedules.length) { - channels.push({ - lang: 'en', - site_id: schedules[0].channel.no, - name: schedules[0].channel.name - }) - } - }) - } - - return channels - } -} - -function parseItems(content, channel) { - return JSON.parse(content.trim()).data.entries[channel] || [] -} - -function parseTitle(item) { - return item.title -} - -function parseDescription(item) { - return item.long_description -} - -function parseStart({ item, date }) { - return date.add(item.start, 'ms') -} - -function parseStop({ item, date }) { - return date.add(item.stop, 'ms') -} - -function toDelta(from, to) { - return toDate(to).diff(toDate(from), 'milliseconds') -} - -function toDate(date) { - return dayjs(date, 'YYYY-MM-DD HH:mm:ss') -} - -function asDate(date) { - return date.toISOString() -} +const dayjs = require('dayjs') +const timezone = require('dayjs/plugin/timezone') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(timezone) +dayjs.extend(utc) + +module.exports = { + site: 'firstmedia.com', + days: 2, + url({ channel, date }) { + return `https://api.firstmedia.com/api/content/tv-guide/list?date=${date.format( + 'DD/MM/YYYY' + )}&channel=${channel.site_id}&startTime=1&endTime=24` + }, + parser({ content, channel, date }) { + if (!content || !channel || !date) return [] + + const programs = [] + const items = parseItems(content, channel.site_id) + .map(item => { + item.start = toDelta(item.date, item.startTime) + item.stop = toDelta(item.date, item.endTime) + return item + }) + .sort((a, b) => a.start - b.start) + + const dt = date.tz('Asia/Jakarta').startOf('d') + let lastStop + items.forEach(item => { + if (lastStop === undefined || item.start >= lastStop) { + lastStop = item.stop + programs.push({ + title: parseTitle(item), + description: parseDescription(item), + start: asDate(parseStart({ item, date: dt })), + stop: asDate(parseStop({ item, date: dt })) + }) + } + }) + + return programs + }, + async channels() { + const axios = require('axios') + const result = await axios + .get( + `https://api.firstmedia.com/api/content/tv-guide/list?date=${dayjs().format( + 'DD/MM/YYYY' + )}&channel=&startTime=0&endTime=24` + ) + .then(response => response.data) + .catch(console.error) + + const channels = [] + if (result.data && result.data.entries) { + Object.values(result.data.entries).forEach(schedules => { + if (schedules.length) { + channels.push({ + lang: 'en', + site_id: schedules[0].channel.no, + name: schedules[0].channel.name + }) + } + }) + } + + return channels + } +} + +function parseItems(content, channel) { + return JSON.parse(content.trim()).data.entries[channel] || [] +} + +function parseTitle(item) { + return item.title +} + +function parseDescription(item) { + return item.long_description +} + +function parseStart({ item, date }) { + return date.add(item.start, 'ms') +} + +function parseStop({ item, date }) { + return date.add(item.stop, 'ms') +} + +function toDelta(from, to) { + return toDate(to).diff(toDate(from), 'milliseconds') +} + +function toDate(date) { + return dayjs(date, 'YYYY-MM-DD HH:mm:ss') +} + +function asDate(date) { + return date.toISOString() +} diff --git a/sites/firstmedia.com/firstmedia.com.test.js b/sites/firstmedia.com/firstmedia.com.test.js index 19292b3f..f9420c8b 100644 --- a/sites/firstmedia.com/firstmedia.com.test.js +++ b/sites/firstmedia.com/firstmedia.com.test.js @@ -1,37 +1,38 @@ -const { url, parser } = require('./firstmedia.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2023-11-08').startOf('d') -const channel = { site_id: '243', xmltv_id: 'AlJazeeraEnglish.qa', lang: 'id' } - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://api.firstmedia.com/api/content/tv-guide/list?date=08/11/2023&channel=243&startTime=1&endTime=24' - ) -}) - -it('can parse response', () => { - const content = - '{"data":{"entries":{"243":[{"createdAt":"2023-11-05T17:09:34.000Z","updatedAt":"2023-11-05T17:09:34.000Z","id":"009f3a34-8164-4ff9-b981-9dcab1a518fc","channelNo":"243","programmeId":null,"title":"News Live","episode":null,"slug":"news-live","date":"2023-11-08 17:00:00","startTime":"2023-11-08 20:00:00","endTime":"2023-11-08 20:30:00","length":1800,"description":"News Live","long_description":"Up to date news and analysis from around the world.","status":true,"channel":{"id":"7fd7a9a6-af32-c861-d2b0-4ddc7846fad2","key":"AljaInt","no":243,"name":"Al Jazeera International","slug":"al-jazeera-international","website":null,"description":"

    An international 24-hour English-language It is the first English-language news channel brings you the latest global news stories, analysis from the Middle East & worldwide.

    ","shortDescription":null,"logo":"files/logos/channels/11-NEWS/AlJazeera Int SD-FirstMedia-Chl-243.jpg","externalId":"132","type":"radio","status":true,"chanel":"SD","locale":"id","relationId":"5a6ea4ae-a008-4889-9c68-7a6f1838e81d","onlyfm":null,"genress":[{"id":"1db3bb43-b00d-49af-b272-6c058a8c0b49","name":"International Free View"},{"id":"2e81a4bd-9719-4186-820a-7e035e07be13","name":"News"}]}}]}}}' - const results = parser({ content, channel, date }) - - // All time in Asia/Jakarta - // 2023-11-08 17:00:00 -> 2023-11-08 20:00:00 = 2023-11-08 03:00:00 - // 2023-11-08 17:00:00 -> 2023-11-08 20:30:00 = 2023-11-08 03:30:00 - expect(results).toMatchObject([ - { - start: '2023-11-07T20:00:00.000Z', - stop: '2023-11-07T20:30:00.000Z', - title: 'News Live', - description: 'Up to date news and analysis from around the world.' - } - ]) -}) - -it('can handle empty guide', () => { - const results = parser({ content: '' }) - - expect(results).toMatchObject([]) -}) +const { url, parser } = require('./firstmedia.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2023-11-08').startOf('d') +const channel = { site_id: '243', xmltv_id: 'AlJazeeraEnglish.qa', lang: 'id' } + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://api.firstmedia.com/api/content/tv-guide/list?date=08/11/2023&channel=243&startTime=1&endTime=24' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') + const results = parser({ content, channel, date }) + + // All time in Asia/Jakarta + // 2023-11-08 17:00:00 -> 2023-11-08 20:00:00 = 2023-11-08 03:00:00 + // 2023-11-08 17:00:00 -> 2023-11-08 20:30:00 = 2023-11-08 03:30:00 + expect(results).toMatchObject([ + { + start: '2023-11-07T20:00:00.000Z', + stop: '2023-11-07T20:30:00.000Z', + title: 'News Live', + description: 'Up to date news and analysis from around the world.' + } + ]) +}) + +it('can handle empty guide', () => { + const results = parser({ content: '' }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/foxsports.com.au/__data__/content.json b/sites/foxsports.com.au/__data__/content.json new file mode 100644 index 00000000..6ec175aa --- /dev/null +++ b/sites/foxsports.com.au/__data__/content.json @@ -0,0 +1,34 @@ +{ + "channel-programme":[ + { + "id":"31cc8b4c-3711-49f0-bf22-2ec3993b0a07", + "programmeTitle":"NRL", + "title":"Eels v Titans", + "startTime":"2022-12-14T00:00:00+11:00", + "endTime":"2022-12-14T01:00:00+11:00", + "duration":60, + "live":false, + "genreId":"5c389cf4-8db7-4b52-9773-52355bd28559", + "channelId":2, + "channelName":"FOX League", + "channelAbbreviation":"LEAGUE", + "programmeUID":235220, + "round":"R1", + "statsMatchId":null, + "closedCaptioned":true, + "statsFixtureId":10207, + "genreTitle":"Rugby League", + "parentGenreId":"a953f929-2d12-41a4-b0e9-97f401afff11", + "parentGenreTitle":"Sport", + "pmgId":"PMG01306944", + "statsSport":"league", + "type":"GAME", + "hiDef":true, + "widescreen":true, + "classification":"", + "synopsis":"The Eels and Titans have plenty of motivation this season after heartbreaking Finals losses in 2021. Parramatta has won their past five against Gold Coast.", + "preGameStartTime":null, + "closeCaptioned":true + } + ] +} \ No newline at end of file diff --git a/sites/foxsports.com.au/__data__/no_content.json b/sites/foxsports.com.au/__data__/no_content.json new file mode 100644 index 00000000..572d8710 --- /dev/null +++ b/sites/foxsports.com.au/__data__/no_content.json @@ -0,0 +1 @@ +{"channel-programme":[]} \ No newline at end of file diff --git a/sites/foxsports.com.au/foxsports.com.au.config.js b/sites/foxsports.com.au/foxsports.com.au.config.js index b7fc17ad..be91f498 100644 --- a/sites/foxsports.com.au/foxsports.com.au.config.js +++ b/sites/foxsports.com.au/foxsports.com.au.config.js @@ -1,69 +1,69 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'foxsports.com.au', - days: 3, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url({ date }) { - return `https://tvguide.foxsports.com.au/granite-api/programmes.json?from=${date.format( - 'YYYY-MM-DD' - )}&to=${date.add(1, 'd').format('YYYY-MM-DD')}` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.programmeTitle, - sub_title: item.title, - category: item.genreTitle, - description: item.synopsis, - start: dayjs.utc(item.startTime), - stop: dayjs.utc(item.endTime) - }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - const data = await axios - .get( - `https://tvguide.foxsports.com.au/granite-api/programmes.json?from=${dayjs().format( - 'YYYY-MM-DD' - )}&to=${dayjs().add(1, 'd').format('YYYY-MM-DD')}` - ) - .then(r => r.data) - .catch(console.log) - - let channels = {} - data['channel-programme'].forEach(item => { - if (channels[item.channelId]) return - - channels[item.channelId] = { - lang: 'en', - site_id: item.channelId, - name: item.channelName - } - }) - - return Object.values(channels) - } -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data) return [] - const programmes = data['channel-programme'] - if (!Array.isArray(programmes)) return [] - - const channelData = programmes.filter(i => i.channelId == channel.site_id) - return channelData && Array.isArray(channelData) ? channelData : [] -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'foxsports.com.au', + days: 3, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url({ date }) { + return `https://tvguide.foxsports.com.au/granite-api/programmes.json?from=${date.format( + 'YYYY-MM-DD' + )}&to=${date.add(1, 'd').format('YYYY-MM-DD')}` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.programmeTitle, + sub_title: item.title, + category: item.genreTitle, + description: item.synopsis, + start: dayjs.utc(item.startTime), + stop: dayjs.utc(item.endTime) + }) + }) + + return programs + }, + async channels() { + const axios = require('axios') + const data = await axios + .get( + `https://tvguide.foxsports.com.au/granite-api/programmes.json?from=${dayjs().format( + 'YYYY-MM-DD' + )}&to=${dayjs().add(1, 'd').format('YYYY-MM-DD')}` + ) + .then(r => r.data) + .catch(console.log) + + let channels = {} + data['channel-programme'].forEach(item => { + if (channels[item.channelId]) return + + channels[item.channelId] = { + lang: 'en', + site_id: item.channelId, + name: item.channelName + } + }) + + return Object.values(channels) + } +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data) return [] + const programmes = data['channel-programme'] + if (!Array.isArray(programmes)) return [] + + const channelData = programmes.filter(i => i.channelId == channel.site_id) + return channelData && Array.isArray(channelData) ? channelData : [] +} diff --git a/sites/foxsports.com.au/foxsports.com.au.test.js b/sites/foxsports.com.au/foxsports.com.au.test.js index eecfffd0..1978d8f0 100644 --- a/sites/foxsports.com.au/foxsports.com.au.test.js +++ b/sites/foxsports.com.au/foxsports.com.au.test.js @@ -1,48 +1,43 @@ -const { parser, url } = require('./foxsports.com.au.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2022-12-14', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2', - xmltv_id: 'FoxLeague.au' -} -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://tvguide.foxsports.com.au/granite-api/programmes.json?from=2022-12-14&to=2022-12-15' - ) -}) - -it('can parse response', () => { - const content = - '{"channel-programme":[{"id":"31cc8b4c-3711-49f0-bf22-2ec3993b0a07","programmeTitle":"NRL","title":"Eels v Titans","startTime":"2022-12-14T00:00:00+11:00","endTime":"2022-12-14T01:00:00+11:00","duration":60,"live":false,"genreId":"5c389cf4-8db7-4b52-9773-52355bd28559","channelId":2,"channelName":"FOX League","channelAbbreviation":"LEAGUE","programmeUID":235220,"round":"R1","statsMatchId":null,"closedCaptioned":true,"statsFixtureId":10207,"genreTitle":"Rugby League","parentGenreId":"a953f929-2d12-41a4-b0e9-97f401afff11","parentGenreTitle":"Sport","pmgId":"PMG01306944","statsSport":"league","type":"GAME","hiDef":true,"widescreen":true,"classification":"","synopsis":"The Eels and Titans have plenty of motivation this season after heartbreaking Finals losses in 2021. Parramatta has won their past five against Gold Coast.","preGameStartTime":null,"closeCaptioned":true}]}' - - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - title: 'NRL', - sub_title: 'Eels v Titans', - description: - 'The Eels and Titans have plenty of motivation this season after heartbreaking Finals losses in 2021. Parramatta has won their past five against Gold Coast.', - category: 'Rugby League', - start: '2022-12-13T13:00:00.000Z', - stop: '2022-12-13T14:00:00.000Z' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser( - { - content: '{"channel-programme":[]}' - }, - channel - ) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./foxsports.com.au.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2022-12-14', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2', + xmltv_id: 'FoxLeague.au' +} +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://tvguide.foxsports.com.au/granite-api/programmes.json?from=2022-12-14&to=2022-12-15' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + title: 'NRL', + sub_title: 'Eels v Titans', + description: + 'The Eels and Titans have plenty of motivation this season after heartbreaking Finals losses in 2021. Parramatta has won their past five against Gold Coast.', + category: 'Rugby League', + start: '2022-12-13T13:00:00.000Z', + stop: '2022-12-13T14:00:00.000Z' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))}, channel) + expect(result).toMatchObject([]) +}) diff --git a/sites/foxtel.com.au/foxtel.com.au.config.js b/sites/foxtel.com.au/foxtel.com.au.config.js index da6f04b1..27923410 100644 --- a/sites/foxtel.com.au/foxtel.com.au.config.js +++ b/sites/foxtel.com.au/foxtel.com.au.config.js @@ -1,138 +1,138 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const cheerio = require('cheerio') - -module.exports = { - site: 'foxtel.com.au', - days: 2, - url({ channel, date }) { - return `https://www.foxtel.com.au/tv-guide/channel/${channel.site_id}/${date.format( - 'YYYY/MM/DD' - )}` - }, - request: { - headers: { - 'Accept-Language': 'en-US,en;', - Cookie: 'AAMC_foxtel_0=REGION|7', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' - } - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - for (let item of items) { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - sub_title: parseSubTitle($item), - image: parseImage($item), - rating: parseRating($item), - season: parseSeason($item), - episode: parseEpisode($item), - start, - stop - }) - } - - return programs - }, - async channels() { - const data = await axios - .get('https://www.foxtel.com.au/webepg/ws/foxtel/channels?regionId=8336', { - headers: { - 'User-Agent': 'insomnia/2022.7.5' - } - }) - .then(r => r.data) - .catch(console.log) - - return data.channels.map(item => { - const slug = item.name - .replace(/\+/g, '-') - .replace(/&/g, '') - .replace(/[^a-z0-9\s]/gi, '') - .replace(/\s/g, '-') - - return { - lang: 'en', - name: item.name, - site_id: `${slug}/${item.channelTag}` - } - }) - } -} - -function parseSeason($item) { - let seasonString = $item('.epg-event-description > div > abbr:nth-child(1)').attr('title') - if (!seasonString) return null - let [, season] = seasonString.match(/^Season: (\d+)/) || [null, null] - - return season ? parseInt(season) : null -} - -function parseEpisode($item) { - let episodeString = $item('.epg-event-description > div > abbr:nth-child(2)').attr('title') - if (!episodeString) return null - let [, episode] = episodeString.match(/^Episode: (\d+)/) || [null, null] - - return episode ? parseInt(episode) : null -} - -function parseImage($item) { - return $item('.epg-event-thumbnail > img').attr('src') -} - -function parseTitle($item) { - return $item('.epg-event-description').clone().children().remove().end().text().trim() -} - -function parseSubTitle($item) { - let subtitle = $item('.epg-event-description > div') - .clone() - .children() - .remove() - .end() - .text() - .trim() - .split(',') - - subtitle = subtitle.pop() - const [, rating] = subtitle.match(/\(([^)]+)\)$/) || [null, null] - - return subtitle.replace(`(${rating})`, '').trim() -} - -function parseRating($item) { - const subtitle = $item('.epg-event-description > div').text().trim() - const [, rating] = subtitle.match(/\(([^)]+)\)$/) || [null, null] - - return rating - ? { - system: 'ACB', - value: rating - } - : null -} - -function parseStart($item) { - const unix = $item('*').data('scheduled-date') - - return dayjs(parseInt(unix)) -} - -function parseItems(content) { - if (!content) return [] - const $ = cheerio.load(content) - - return $('#epg-channel-events > a').toArray() -} +const axios = require('axios') +const dayjs = require('dayjs') +const cheerio = require('cheerio') + +module.exports = { + site: 'foxtel.com.au', + days: 2, + url({ channel, date }) { + return `https://www.foxtel.com.au/tv-guide/channel/${channel.site_id}/${date.format( + 'YYYY/MM/DD' + )}` + }, + request: { + headers: { + 'Accept-Language': 'en-US,en;', + Cookie: 'AAMC_foxtel_0=REGION|7', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' + } + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + for (let item of items) { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + sub_title: parseSubTitle($item), + image: parseImage($item), + rating: parseRating($item), + season: parseSeason($item), + episode: parseEpisode($item), + start, + stop + }) + } + + return programs + }, + async channels() { + const data = await axios + .get('https://www.foxtel.com.au/webepg/ws/foxtel/channels?regionId=8336', { + headers: { + 'User-Agent': 'insomnia/2022.7.5' + } + }) + .then(r => r.data) + .catch(console.log) + + return data.channels.map(item => { + const slug = item.name + .replace(/\+/g, '-') + .replace(/&/g, '') + .replace(/[^a-z0-9\s]/gi, '') + .replace(/\s/g, '-') + + return { + lang: 'en', + name: item.name, + site_id: `${slug}/${item.channelTag}` + } + }) + } +} + +function parseSeason($item) { + let seasonString = $item('.epg-event-description > div > abbr:nth-child(1)').attr('title') + if (!seasonString) return null + let [, season] = seasonString.match(/^Season: (\d+)/) || [null, null] + + return season ? parseInt(season) : null +} + +function parseEpisode($item) { + let episodeString = $item('.epg-event-description > div > abbr:nth-child(2)').attr('title') + if (!episodeString) return null + let [, episode] = episodeString.match(/^Episode: (\d+)/) || [null, null] + + return episode ? parseInt(episode) : null +} + +function parseImage($item) { + return $item('.epg-event-thumbnail > img').attr('src') +} + +function parseTitle($item) { + return $item('.epg-event-description').clone().children().remove().end().text().trim() +} + +function parseSubTitle($item) { + let subtitle = $item('.epg-event-description > div') + .clone() + .children() + .remove() + .end() + .text() + .trim() + .split(',') + + subtitle = subtitle.pop() + const [, rating] = subtitle.match(/\(([^)]+)\)$/) || [null, null] + + return subtitle.replace(`(${rating})`, '').trim() +} + +function parseRating($item) { + const subtitle = $item('.epg-event-description > div').text().trim() + const [, rating] = subtitle.match(/\(([^)]+)\)$/) || [null, null] + + return rating + ? { + system: 'ACB', + value: rating + } + : null +} + +function parseStart($item) { + const unix = $item('*').data('scheduled-date') + + return dayjs(parseInt(unix)) +} + +function parseItems(content) { + if (!content) return [] + const $ = cheerio.load(content) + + return $('#epg-channel-events > a').toArray() +} diff --git a/sites/foxtel.com.au/foxtel.com.au.test.js b/sites/foxtel.com.au/foxtel.com.au.test.js index 3485c6f2..ef715635 100644 --- a/sites/foxtel.com.au/foxtel.com.au.test.js +++ b/sites/foxtel.com.au/foxtel.com.au.test.js @@ -1,60 +1,60 @@ -const { parser, url, request } = require('./foxtel.com.au.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('2022-11-08', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'Channel-9-Sydney/NIN', - xmltv_id: 'Channel9Sydney.au' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.foxtel.com.au/tv-guide/channel/Channel-9-Sydney/NIN/2022/11/08' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Accept-Language': 'en-US,en;', - Cookie: 'AAMC_foxtel_0=REGION|7' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - - let results = parser({ content }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-07T12:40:00.000Z', - stop: '2022-11-07T13:30:00.000Z', - title: 'The Equalizer', - sub_title: 'Glory', - image: - 'https://images1.resources.foxtel.com.au/store2/mount1/16/3/69e0v.jpg?maxheight=90&limit=91aa1c7a2c485aeeba0706941f79f111adb35830', - rating: { - system: 'ACB', - value: 'M' - }, - season: 1, - episode: 2 - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html')) - }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./foxtel.com.au.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('2022-11-08', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'Channel-9-Sydney/NIN', + xmltv_id: 'Channel9Sydney.au' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.foxtel.com.au/tv-guide/channel/Channel-9-Sydney/NIN/2022/11/08' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Accept-Language': 'en-US,en;', + Cookie: 'AAMC_foxtel_0=REGION|7' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + + let results = parser({ content }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-07T12:40:00.000Z', + stop: '2022-11-07T13:30:00.000Z', + title: 'The Equalizer', + sub_title: 'Glory', + image: + 'https://images1.resources.foxtel.com.au/store2/mount1/16/3/69e0v.jpg?maxheight=90&limit=91aa1c7a2c485aeeba0706941f79f111adb35830', + rating: { + system: 'ACB', + value: 'M' + }, + season: 1, + episode: 2 + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html')) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/freetv.tv/__data__/content.json b/sites/freetv.tv/__data__/content.json new file mode 100644 index 00000000..3548be09 --- /dev/null +++ b/sites/freetv.tv/__data__/content.json @@ -0,0 +1 @@ +[{"id":5856194,"publicUid":"f17b5a1d-3f2a-42a0-a11b-a08e5c9785fd","title":"בוש 4 - פרק 3","lead":"עונה 4 חדשה לדרמה הבלשית. 3. השטן בתוך הבית: הכוח המיוחד מנסה לחקור, ומגלה אליבי שקרי עם השלכות מרעישות. הבלש סנטיאגו רוברטסון צריך לשים את קשריו האישיים בצד למען החקירה. בוש זוכה לביקור פתע לילי.כ עב","description":"עונה 4 חדשה לדרמה הבלשית. 3. השטן בתוך הבית: הכוח המיוחד מנסה לחקור, ומגלה אליבי שקרי עם השלכות מרעישות. הבלש סנטיאגו רוברטסון צריך לשים את קשריו האישיים בצד למען החקירה. בוש זוכה לביקור פתע לילי.כ עב","rating":14,"ratingEmbedded":false,"type":"PROGRAMME","uhd":false,"since":"2025-03-27T21:26:00Z","till":"2025-03-27T22:17:00Z","images":{"16x9":[{"url":"//d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1361162/COVER/images/1361162_1736767668746.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1","templateUrl":"//d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1361162/COVER/images/1361162_1736767668746.jpg?srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1&dsth={height:177}&dstw={width:315}"}]},"genres":[],"webUrl":"https://web.freetv.tv/news,1991/rw-11,3370462/2025-03-27/bw-4---prq-3,5856194","trailer":false,"live":{"type_":"LIVE","id":3370462},"available":true,"timeshiftAvailable":false,"startoverAvailable":false,"catchupTill":"2025-04-10T21:26:00Z","npvrTill":"2025-04-10T21:26:00Z","programRecordingId":7068534,"audio":false,"video":true,"showRecommendations":false,"recommended":false,"premiere":false,"liveBroadcast":false,"slug":"bw-4---prq-3"},{"id":5858584,"publicUid":"1dc296c9-0c28-4202-8b20-cbef56a763f5","title":"אבא משתדל - 5. חבר","lead":"סדרה קומית. יוסי מכיר אב לילד עם צרכים מיוחדים ובין השניים מתפתח קשר בסגנון חיזור גורלי שמערער את יוסי. כ עב.","description":"סדרה קומית. יוסי מכיר אב לילד עם צרכים מיוחדים ובין השניים מתפתח קשר בסגנון חיזור גורלי שמערער את יוסי. כ עב.","ratingEmbedded":false,"type":"PROGRAMME","uhd":false,"since":"2025-03-27T22:17:00Z","till":"2025-03-27T22:43:00Z","images":{"16x9":[{"url":"//d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1070668/COVER/images/1070668_1742202219830.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1","templateUrl":"//d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1070668/COVER/images/1070668_1742202219830.jpg?srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1&dsth={height:177}&dstw={width:315}"}]},"genres":[],"webUrl":"https://web.freetv.tv/news,1991/rw-11,3370462/2025-03-27/b-mdl---5-br,5858584","trailer":false,"live":{"type_":"LIVE","id":3370462},"available":true,"timeshiftAvailable":false,"startoverAvailable":false,"catchupTill":"2025-04-10T22:17:00Z","npvrTill":"2025-04-10T22:17:00Z","programRecordingId":7071052,"audio":false,"video":true,"showRecommendations":false,"recommended":false,"premiere":false,"liveBroadcast":false,"slug":"b-mdl---5-br"}] \ No newline at end of file diff --git a/sites/freetv.tv/freetv.tv.config.js b/sites/freetv.tv/freetv.tv.config.js index 4b21ba88..02cbc4c7 100644 --- a/sites/freetv.tv/freetv.tv.config.js +++ b/sites/freetv.tv/freetv.tv.config.js @@ -1,62 +1,62 @@ -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: 'freetv.tv', - days: 2, - url: function ({ channel, date }) { - const localDate = dayjs(date).tz('Asia/Jerusalem') - const since = localDate.startOf('day').format('YYYY-MM-DDTHH:mmZZ') - const till = localDate.add(1, 'day').startOf('day').format('YYYY-MM-DDTHH:mmZZ') - - return `https://web.freetv.tv/api/products/lives/programmes?liveId[]=${ - channel.site_id - }&since=${encodeURIComponent(since)}&till=${encodeURIComponent(till)}&lang=HEB&platform=BROWSER` - }, - parser: function ({ content }) { - const programs = [] - let items = [] - - try { - items = JSON.parse(content) - } catch { - return programs - } - - items.forEach(item => { - const start = parseStart(item) - const stop = parseStop(item) - if (!start.isValid() || !stop.isValid()) return - - programs.push({ - title: item.title, - description: item.description || item.lead, - image: getImageUrl(item), - icon: getImageUrl(item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item) { - return item.since ? dayjs.utc(item.since).tz('Asia/Jerusalem') : null -} - -function parseStop(item) { - return item.till ? dayjs.utc(item.till).tz('Asia/Jerusalem') : null -} - -function getImageUrl(item) { - const url = item.images?.['16x9']?.[0]?.url - return url ? `https:${url}` : null -} +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: 'freetv.tv', + days: 2, + url: function ({ channel, date }) { + const localDate = dayjs(date).tz('Asia/Jerusalem') + const since = localDate.startOf('day').format('YYYY-MM-DDTHH:mmZZ') + const till = localDate.add(1, 'day').startOf('day').format('YYYY-MM-DDTHH:mmZZ') + + return `https://web.freetv.tv/api/products/lives/programmes?liveId[]=${ + channel.site_id + }&since=${encodeURIComponent(since)}&till=${encodeURIComponent(till)}&lang=HEB&platform=BROWSER` + }, + parser: function ({ content }) { + const programs = [] + let items = [] + + try { + items = JSON.parse(content) + } catch { + return programs + } + + items.forEach(item => { + const start = parseStart(item) + const stop = parseStop(item) + if (!start.isValid() || !stop.isValid()) return + + programs.push({ + title: item.title, + description: item.description || item.lead, + image: getImageUrl(item), + icon: getImageUrl(item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item) { + return item.since ? dayjs.utc(item.since).tz('Asia/Jerusalem') : null +} + +function parseStop(item) { + return item.till ? dayjs.utc(item.till).tz('Asia/Jerusalem') : null +} + +function getImageUrl(item) { + const url = item.images?.['16x9']?.[0]?.url + return url ? `https:${url}` : null +} diff --git a/sites/freetv.tv/freetv.tv.test.js b/sites/freetv.tv/freetv.tv.test.js index e65c3cfe..bab8ef6a 100644 --- a/sites/freetv.tv/freetv.tv.test.js +++ b/sites/freetv.tv/freetv.tv.test.js @@ -1,50 +1,51 @@ -const { parser, url } = require('./freetv.tv.config.js') -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-03-28', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '3370462', - xmltv_id: 'Kan11.il' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://web.freetv.tv/api/products/lives/programmes?liveId[]=3370462&since=2025-03-28T00%3A00%2B0200&till=2025-03-29T00%3A00%2B0300&lang=HEB&platform=BROWSER') -}) - -it('can parse response', () => { - const content = '[{"id":5856194,"publicUid":"f17b5a1d-3f2a-42a0-a11b-a08e5c9785fd","title":"בוש 4 - פרק 3","lead":"עונה 4 חדשה לדרמה הבלשית. 3. השטן בתוך הבית: הכוח המיוחד מנסה לחקור, ומגלה אליבי שקרי עם השלכות מרעישות. הבלש סנטיאגו רוברטסון צריך לשים את קשריו האישיים בצד למען החקירה. בוש זוכה לביקור פתע לילי.כ עב","description":"עונה 4 חדשה לדרמה הבלשית. 3. השטן בתוך הבית: הכוח המיוחד מנסה לחקור, ומגלה אליבי שקרי עם השלכות מרעישות. הבלש סנטיאגו רוברטסון צריך לשים את קשריו האישיים בצד למען החקירה. בוש זוכה לביקור פתע לילי.כ עב","rating":14,"ratingEmbedded":false,"type":"PROGRAMME","uhd":false,"since":"2025-03-27T21:26:00Z","till":"2025-03-27T22:17:00Z","images":{"16x9":[{"url":"//d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1361162/COVER/images/1361162_1736767668746.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1","templateUrl":"//d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1361162/COVER/images/1361162_1736767668746.jpg?srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1&dsth={height:177}&dstw={width:315}"}]},"genres":[],"webUrl":"https://web.freetv.tv/news,1991/rw-11,3370462/2025-03-27/bw-4---prq-3,5856194","trailer":false,"live":{"type_":"LIVE","id":3370462},"available":true,"timeshiftAvailable":false,"startoverAvailable":false,"catchupTill":"2025-04-10T21:26:00Z","npvrTill":"2025-04-10T21:26:00Z","programRecordingId":7068534,"audio":false,"video":true,"showRecommendations":false,"recommended":false,"premiere":false,"liveBroadcast":false,"slug":"bw-4---prq-3"},{"id":5858584,"publicUid":"1dc296c9-0c28-4202-8b20-cbef56a763f5","title":"אבא משתדל - 5. חבר","lead":"סדרה קומית. יוסי מכיר אב לילד עם צרכים מיוחדים ובין השניים מתפתח קשר בסגנון חיזור גורלי שמערער את יוסי. כ עב.","description":"סדרה קומית. יוסי מכיר אב לילד עם צרכים מיוחדים ובין השניים מתפתח קשר בסגנון חיזור גורלי שמערער את יוסי. כ עב.","ratingEmbedded":false,"type":"PROGRAMME","uhd":false,"since":"2025-03-27T22:17:00Z","till":"2025-03-27T22:43:00Z","images":{"16x9":[{"url":"//d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1070668/COVER/images/1070668_1742202219830.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1","templateUrl":"//d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1070668/COVER/images/1070668_1742202219830.jpg?srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1&dsth={height:177}&dstw={width:315}"}]},"genres":[],"webUrl":"https://web.freetv.tv/news,1991/rw-11,3370462/2025-03-27/b-mdl---5-br,5858584","trailer":false,"live":{"type_":"LIVE","id":3370462},"available":true,"timeshiftAvailable":false,"startoverAvailable":false,"catchupTill":"2025-04-10T22:17:00Z","npvrTill":"2025-04-10T22:17:00Z","programRecordingId":7071052,"audio":false,"video":true,"showRecommendations":false,"recommended":false,"premiere":false,"liveBroadcast":false,"slug":"b-mdl---5-br"}]' - - const results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(2) - expect(results[0]).toMatchObject({ - title: 'בוש 4 - פרק 3', - description: 'עונה 4 חדשה לדרמה הבלשית. 3. השטן בתוך הבית: הכוח המיוחד מנסה לחקור, ומגלה אליבי שקרי עם השלכות מרעישות. הבלש סנטיאגו רוברטסון צריך לשים את קשריו האישיים בצד למען החקירה. בוש זוכה לביקור פתע לילי.כ עב', - image: 'https://d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1361162/COVER/images/1361162_1736767668746.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1', - icon: 'https://d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1361162/COVER/images/1361162_1736767668746.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1', - start: '2025-03-27T21:26:00.000Z', - stop: '2025-03-27T22:17:00.000Z' - }) - expect(results[1]).toMatchObject({ - title: 'אבא משתדל - 5. חבר', - description: 'סדרה קומית. יוסי מכיר אב לילד עם צרכים מיוחדים ובין השניים מתפתח קשר בסגנון חיזור גורלי שמערער את יוסי. כ עב.', - image: 'https://d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1070668/COVER/images/1070668_1742202219830.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1', - icon: 'https://d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1070668/COVER/images/1070668_1742202219830.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1', - start: '2025-03-27T22:17:00.000Z', - stop: '2025-03-27T22:43:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ content: '' }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./freetv.tv.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-03-28', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '3370462', + xmltv_id: 'Kan11.il' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://web.freetv.tv/api/products/lives/programmes?liveId[]=3370462&since=2025-03-28T00%3A00%2B0200&till=2025-03-29T00%3A00%2B0300&lang=HEB&platform=BROWSER') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(2) + expect(results[0]).toMatchObject({ + title: 'בוש 4 - פרק 3', + description: 'עונה 4 חדשה לדרמה הבלשית. 3. השטן בתוך הבית: הכוח המיוחד מנסה לחקור, ומגלה אליבי שקרי עם השלכות מרעישות. הבלש סנטיאגו רוברטסון צריך לשים את קשריו האישיים בצד למען החקירה. בוש זוכה לביקור פתע לילי.כ עב', + image: 'https://d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1361162/COVER/images/1361162_1736767668746.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1', + icon: 'https://d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1361162/COVER/images/1361162_1736767668746.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1', + start: '2025-03-27T21:26:00.000Z', + stop: '2025-03-27T22:17:00.000Z' + }) + expect(results[1]).toMatchObject({ + title: 'אבא משתדל - 5. חבר', + description: 'סדרה קומית. יוסי מכיר אב לילד עם צרכים מיוחדים ובין השניים מתפתח קשר בסגנון חיזור גורלי שמערער את יוסי. כ עב.', + image: 'https://d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1070668/COVER/images/1070668_1742202219830.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1', + icon: 'https://d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1070668/COVER/images/1070668_1742202219830.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1', + start: '2025-03-27T22:17:00.000Z', + stop: '2025-03-27T22:43:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ content: '' }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/freeview.co.uk/freeview.co.uk.config.js b/sites/freeview.co.uk/freeview.co.uk.config.js index fab60372..9b77a257 100644 --- a/sites/freeview.co.uk/freeview.co.uk.config.js +++ b/sites/freeview.co.uk/freeview.co.uk.config.js @@ -1,73 +1,73 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const parseDuration = require('parse-duration').default - -dayjs.extend(utc) - -module.exports = { - site: 'freeview.co.uk', - days: 2, - url({ date, channel }) { - const [networkId] = channel.site_id.split('#') - const startTimestamp = date.startOf('d').unix() - - return `https://www.freeview.co.uk/api/tv-guide?nid=${networkId}&start=${startTimestamp}` - }, - parser({ content, channel }) { - let programs = [] - let items = parseItems(content, channel) - items.forEach(item => { - const start = parseStart(item) - const duration = parseDuration(item.duration) - const stop = start.add(duration, 'ms') - programs.push({ - title: item.main_title, - subtitle: item.secondary_title, - image: parseImage(item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const networkId = '64257' // Great London - const startTimestamp = dayjs.utc().startOf('d').unix() - const data = await axios - .get(`https://www.freeview.co.uk/api/tv-guide?nid=${networkId}&start=${startTimestamp}`) - .then(r => r.data) - .catch(console.log) - - return data.data.programs.map(item => ({ - lang: 'en', - site_id: `${networkId}#${item.service_id}`, - name: item.title - })) - } -} - -function parseImage(item) { - return item.image_url ? `${item.image_url}?w=800` : null -} - -function parseStart(item) { - return dayjs(item.start_time) -} - -function parseItems(content, channel) { - try { - const data = JSON.parse(content) - const programs = data?.data?.programs - if (!Array.isArray(programs)) return [] - const [, channelId] = channel.site_id.split('#') - const channelData = programs.find(p => p.service_id === channelId) - const channelPrograms = channelData?.events - if (!Array.isArray(channelPrograms)) return [] - - return channelPrograms - } catch { - return [] - } -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const parseDuration = require('parse-duration').default + +dayjs.extend(utc) + +module.exports = { + site: 'freeview.co.uk', + days: 2, + url({ date, channel }) { + const [networkId] = channel.site_id.split('#') + const startTimestamp = date.startOf('d').unix() + + return `https://www.freeview.co.uk/api/tv-guide?nid=${networkId}&start=${startTimestamp}` + }, + parser({ content, channel }) { + let programs = [] + let items = parseItems(content, channel) + items.forEach(item => { + const start = parseStart(item) + const duration = parseDuration(item.duration) + const stop = start.add(duration, 'ms') + programs.push({ + title: item.main_title, + subtitle: item.secondary_title, + image: parseImage(item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const networkId = '64257' // Great London + const startTimestamp = dayjs.utc().startOf('d').unix() + const data = await axios + .get(`https://www.freeview.co.uk/api/tv-guide?nid=${networkId}&start=${startTimestamp}`) + .then(r => r.data) + .catch(console.log) + + return data.data.programs.map(item => ({ + lang: 'en', + site_id: `${networkId}#${item.service_id}`, + name: item.title + })) + } +} + +function parseImage(item) { + return item.image_url ? `${item.image_url}?w=800` : null +} + +function parseStart(item) { + return dayjs(item.start_time) +} + +function parseItems(content, channel) { + try { + const data = JSON.parse(content) + const programs = data?.data?.programs + if (!Array.isArray(programs)) return [] + const [, channelId] = channel.site_id.split('#') + const channelData = programs.find(p => p.service_id === channelId) + const channelPrograms = channelData?.events + if (!Array.isArray(channelPrograms)) return [] + + return channelPrograms + } catch { + return [] + } +} diff --git a/sites/freeview.co.uk/freeview.co.uk.test.js b/sites/freeview.co.uk/freeview.co.uk.test.js index b45fd266..7c33151a 100644 --- a/sites/freeview.co.uk/freeview.co.uk.test.js +++ b/sites/freeview.co.uk/freeview.co.uk.test.js @@ -1,55 +1,55 @@ -const { parser, url } = require('./freeview.co.uk.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-16', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '64257#4164', - xmltv_id: 'BBCOneLondon.uk' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://www.freeview.co.uk/api/tv-guide?nid=64257&start=1736985600' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - 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(25) - expect(results[0]).toMatchObject({ - start: '2025-01-16T00:00:00.000Z', - stop: '2025-01-16T00:45:00.000Z', - title: 'The Weakest Link', - subtitle: 'Series 4: Episode 7', - image: 'https://img.freeviewplay.tv/p0b041486e4378cbf074511098f74e78f?w=800' - }) - expect(results[24]).toMatchObject({ - start: '2025-01-16T23:40:00.000Z', - stop: '2025-01-17T00:10:00.000Z', - title: 'Newscast', - subtitle: 'Series 5: 16/01/2025', - image: 'https://img.freeviewplay.tv/pb43e790fe10fe5ba668caf22224bc312?w=800' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: '[]', - channel - }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./freeview.co.uk.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-16', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '64257#4164', + xmltv_id: 'BBCOneLondon.uk' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://www.freeview.co.uk/api/tv-guide?nid=64257&start=1736985600' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + 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(25) + expect(results[0]).toMatchObject({ + start: '2025-01-16T00:00:00.000Z', + stop: '2025-01-16T00:45:00.000Z', + title: 'The Weakest Link', + subtitle: 'Series 4: Episode 7', + image: 'https://img.freeviewplay.tv/p0b041486e4378cbf074511098f74e78f?w=800' + }) + expect(results[24]).toMatchObject({ + start: '2025-01-16T23:40:00.000Z', + stop: '2025-01-17T00:10:00.000Z', + title: 'Newscast', + subtitle: 'Series 5: 16/01/2025', + image: 'https://img.freeviewplay.tv/pb43e790fe10fe5ba668caf22224bc312?w=800' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: '[]', + channel + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/frikanalen.no/__data__/content.json b/sites/frikanalen.no/__data__/content.json new file mode 100644 index 00000000..221056a1 --- /dev/null +++ b/sites/frikanalen.no/__data__/content.json @@ -0,0 +1 @@ +{"count":83,"next":null,"previous":null,"results":[{"id":135605,"video":{"id":626094,"name":"FSCONS 2017 - Keynote: TBA - Linda Sandvik","header":"Linda Sandvik's keynote at FSCONS 2017\r\n\r\nRecorded by NUUG for FSCONS.","description":null,"creator":"davidwnoble@gmail.com","organization":{"id":82,"name":"NUUG","homepage":"https://www.nuug.no/","description":"Forening NUUG er for alle som er interessert i fri programvare, åpne standarder og Unix-lignende operativsystemer.","postalAddress":"","streetAddress":"","editorId":2148,"editorName":"David Noble","editorEmail":"davidwnoble@gmail.com","editorMsisdn":"","fkmember":true},"duration":"00:57:55.640000","categories":["Samfunn"]},"schedulereason":5,"starttime":"2022-01-19T00:47:00+01:00","endtime":"2022-01-19T01:44:55.640000+01:00","duration":"00:57:55.640000"}]} \ No newline at end of file diff --git a/sites/frikanalen.no/__data__/no_content.json b/sites/frikanalen.no/__data__/no_content.json new file mode 100644 index 00000000..1401014b --- /dev/null +++ b/sites/frikanalen.no/__data__/no_content.json @@ -0,0 +1 @@ +{"count":0,"next":null,"previous":null,"results":[]} \ No newline at end of file diff --git a/sites/frikanalen.no/frikanalen.no.config.js b/sites/frikanalen.no/frikanalen.no.config.js index a5886752..e66a6efe 100644 --- a/sites/frikanalen.no/frikanalen.no.config.js +++ b/sites/frikanalen.no/frikanalen.no.config.js @@ -1,52 +1,52 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'frikanalen.no', - days: 2, - url({ date }) { - return `https://frikanalen.no/api/scheduleitems/?date=${date.format( - 'YYYY-MM-DD' - )}&format=json&limit=100` - }, - parser({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: parseTitle(item), - category: parseCategory(item), - description: parseDescription(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseTitle(item) { - return item.video.name -} - -function parseCategory(item) { - return item.video.categories -} - -function parseDescription(item) { - return item.video.header -} - -function parseStart(item) { - return dayjs(item.starttime) -} - -function parseStop(item) { - return dayjs(item.endtime) -} - -function parseItems(content) { - const data = JSON.parse(content) - - return data && Array.isArray(data.results) ? data.results : [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'frikanalen.no', + days: 2, + url({ date }) { + return `https://frikanalen.no/api/scheduleitems/?date=${date.format( + 'YYYY-MM-DD' + )}&format=json&limit=100` + }, + parser({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: parseTitle(item), + category: parseCategory(item), + description: parseDescription(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseTitle(item) { + return item.video.name +} + +function parseCategory(item) { + return item.video.categories +} + +function parseDescription(item) { + return item.video.header +} + +function parseStart(item) { + return dayjs(item.starttime) +} + +function parseStop(item) { + return dayjs(item.endtime) +} + +function parseItems(content) { + const data = JSON.parse(content) + + return data && Array.isArray(data.results) ? data.results : [] +} diff --git a/sites/frikanalen.no/frikanalen.no.test.js b/sites/frikanalen.no/frikanalen.no.test.js index ff758dda..a7aee3c2 100644 --- a/sites/frikanalen.no/frikanalen.no.test.js +++ b/sites/frikanalen.no/frikanalen.no.test.js @@ -1,47 +1,48 @@ -const { parser, url } = require('./frikanalen.no.config.js') -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('2022-01-19', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '#', - xmltv_id: 'Frikanalen.no' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://frikanalen.no/api/scheduleitems/?date=2022-01-19&format=json&limit=100' - ) -}) - -it('can parse response', () => { - const content = - '{"count":83,"next":null,"previous":null,"results":[{"id":135605,"video":{"id":626094,"name":"FSCONS 2017 - Keynote: TBA - Linda Sandvik","header":"Linda Sandvik\'s keynote at FSCONS 2017\\r\\n\\r\\nRecorded by NUUG for FSCONS.","description":null,"creator":"davidwnoble@gmail.com","organization":{"id":82,"name":"NUUG","homepage":"https://www.nuug.no/","description":"Forening NUUG er for alle som er interessert i fri programvare, åpne standarder og Unix-lignende operativsystemer.","postalAddress":"","streetAddress":"","editorId":2148,"editorName":"David Noble","editorEmail":"davidwnoble@gmail.com","editorMsisdn":"","fkmember":true},"duration":"00:57:55.640000","categories":["Samfunn"]},"schedulereason":5,"starttime":"2022-01-19T00:47:00+01:00","endtime":"2022-01-19T01:44:55.640000+01:00","duration":"00:57:55.640000"}]}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-01-18T23:47:00.000Z', - stop: '2022-01-19T00:44:55.640Z', - title: 'FSCONS 2017 - Keynote: TBA - Linda Sandvik', - category: ['Samfunn'], - description: "Linda Sandvik's keynote at FSCONS 2017\r\n\r\nRecorded by NUUG for FSCONS." - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"count":0,"next":null,"previous":null,"results":[]}' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./frikanalen.no.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('2022-01-19', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '#', + xmltv_id: 'Frikanalen.no' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://frikanalen.no/api/scheduleitems/?date=2022-01-19&format=json&limit=100' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-01-18T23:47:00.000Z', + stop: '2022-01-19T00:44:55.640Z', + title: 'FSCONS 2017 - Keynote: TBA - Linda Sandvik', + category: ['Samfunn'], + description: "Linda Sandvik's keynote at FSCONS 2017\r\n\r\nRecorded by NUUG for FSCONS." + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/galamtv.kz/__data__/content.json b/sites/galamtv.kz/__data__/content.json new file mode 100644 index 00000000..2fedf8bb --- /dev/null +++ b/sites/galamtv.kz/__data__/content.json @@ -0,0 +1,21 @@ +{ + "programs": [ + { + "metaInfo": { + "title": "Гимн", + "description": "Государственный гимн Республики Казахстан" + }, + "scheduleInfo": { + "start": 1736470800, + "end": 1736471100 + }, + "mediaInfo": { + "thumbnails": [ + { + "url": "http://galam.server-img.lfstrm.tv:80/image/aHR0cDovL2dhbGFtLmltZy1vcmlnaW5hbHMubGZzdHJtLnR2OjgwL3R2aW1hZ2VzL3RodW1iL2YyNWFmYWY2ZDkzYjU5YjdkMjBiZDNiODhiZjg4NWI0X29yaWcuanBn" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/sites/galamtv.kz/__data__/no_content.json b/sites/galamtv.kz/__data__/no_content.json new file mode 100644 index 00000000..bfa842bb --- /dev/null +++ b/sites/galamtv.kz/__data__/no_content.json @@ -0,0 +1 @@ +{"programs":[]} \ No newline at end of file diff --git a/sites/galamtv.kz/galamtv.kz.config.js b/sites/galamtv.kz/galamtv.kz.config.js index f64b9199..625980ba 100644 --- a/sites/galamtv.kz/galamtv.kz.config.js +++ b/sites/galamtv.kz/galamtv.kz.config.js @@ -1,64 +1,64 @@ -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') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'galamtv.kz', - timezone: 'Asia/Almaty', - days: 2, - request: { - method: 'GET', - headers: { - Referer: 'https://galamtv.kz/', - Origin: 'https://galamtv.kz', - Accept: '*/*', - 'Accept-Encoding': 'gzip, deflate, br, zstd' - } - }, - url({ channel, date }) { - const todayEpoch = date.startOf('day').unix() - const nextDayEpoch = date.add(1, 'day').startOf('day').unix() - return `https://galam.server-api.lfstrm.tv/channels/${channel.site_id}/programs?period=${todayEpoch}:${nextDayEpoch}` - }, - parser: function ({ content }) { - let programs = [] - const data = JSON.parse(content) - const programsData = data.programs || [] - - programsData.forEach(program => { - const start = dayjs.unix(program.scheduleInfo.start) - const stop = dayjs.unix(program.scheduleInfo.end) - - programs.push({ - title: program.metaInfo.title, - description: program.metaInfo.description, - image: program.mediaInfo.thumbnails[0].url, - start, - stop - }) - }) - - return programs - }, - async channels() { - try { - const response = await axios.get('https://galam.server-api.lfstrm.tv/channels-now') - return response.data.channels.map(item => { - return { - lang: 'kk', - site_id: item.channels.id, - name: item.channels.info.metaInfo.title - } - }) - } catch (error) { - console.error('Error fetching channels:', error) - return [] - } - } -} +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') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'galamtv.kz', + timezone: 'Asia/Almaty', + days: 2, + request: { + method: 'GET', + headers: { + Referer: 'https://galamtv.kz/', + Origin: 'https://galamtv.kz', + Accept: '*/*', + 'Accept-Encoding': 'gzip, deflate, br, zstd' + } + }, + url({ channel, date }) { + const todayEpoch = date.startOf('day').unix() + const nextDayEpoch = date.add(1, 'day').startOf('day').unix() + return `https://galam.server-api.lfstrm.tv/channels/${channel.site_id}/programs?period=${todayEpoch}:${nextDayEpoch}` + }, + parser: function ({ content }) { + let programs = [] + const data = JSON.parse(content) + const programsData = data.programs || [] + + programsData.forEach(program => { + const start = dayjs.unix(program.scheduleInfo.start) + const stop = dayjs.unix(program.scheduleInfo.end) + + programs.push({ + title: program.metaInfo.title, + description: program.metaInfo.description, + image: program.mediaInfo.thumbnails[0].url, + start, + stop + }) + }) + + return programs + }, + async channels() { + try { + const response = await axios.get('https://galam.server-api.lfstrm.tv/channels-now') + return response.data.channels.map(item => { + return { + lang: 'kk', + site_id: item.channels.id, + name: item.channels.info.metaInfo.title + } + }) + } catch (error) { + console.error('Error fetching channels:', error) + return [] + } + } +} diff --git a/sites/galamtv.kz/galamtv.kz.test.js b/sites/galamtv.kz/galamtv.kz.test.js index 8842e654..9e762edb 100644 --- a/sites/galamtv.kz/galamtv.kz.test.js +++ b/sites/galamtv.kz/galamtv.kz.test.js @@ -1,71 +1,52 @@ -const { parser, url } = require('./galamtv.kz.config.js') -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-10', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '636e54cf8a8f73bae8244f41', - xmltv_id: 'Qazaqstan.kz' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - `https://galam.server-api.lfstrm.tv/channels/${ - channel.site_id - }/programs?period=${date.unix()}:${date.add(1, 'day').unix()}` - ) -}) - -it('can parse response', () => { - const content = JSON.stringify({ - programs: [ - { - metaInfo: { - title: 'Гимн', - description: 'Государственный гимн Республики Казахстан' - }, - scheduleInfo: { - start: 1736470800, - end: 1736471100 - }, - mediaInfo: { - thumbnails: [ - { - url: 'http://galam.server-img.lfstrm.tv:80/image/aHR0cDovL2dhbGFtLmltZy1vcmlnaW5hbHMubGZzdHJtLnR2OjgwL3R2aW1hZ2VzL3RodW1iL2YyNWFmYWY2ZDkzYjU5YjdkMjBiZDNiODhiZjg4NWI0X29yaWcuanBn' - } - ] - } - } - ] - }) - - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2025-01-10T01:00:00.000Z', - stop: '2025-01-10T01:05:00.000Z', - title: 'Гимн', - description: 'Государственный гимн Республики Казахстан', - image: - 'http://galam.server-img.lfstrm.tv:80/image/aHR0cDovL2dhbGFtLmltZy1vcmlnaW5hbHMubGZzdHJtLnR2OjgwL3R2aW1hZ2VzL3RodW1iL2YyNWFmYWY2ZDkzYjU5YjdkMjBiZDNiODhiZjg4NWI0X29yaWcuanBn' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"programs":[]}' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./galamtv.kz.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-10', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '636e54cf8a8f73bae8244f41', + xmltv_id: 'Qazaqstan.kz' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + `https://galam.server-api.lfstrm.tv/channels/${ + channel.site_id + }/programs?period=${date.unix()}:${date.add(1, 'day').unix()}` + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2025-01-10T01:00:00.000Z', + stop: '2025-01-10T01:05:00.000Z', + title: 'Гимн', + description: 'Государственный гимн Республики Казахстан', + image: + 'http://galam.server-img.lfstrm.tv:80/image/aHR0cDovL2dhbGFtLmltZy1vcmlnaW5hbHMubGZzdHJtLnR2OjgwL3R2aW1hZ2VzL3RodW1iL2YyNWFmYWY2ZDkzYjU5YjdkMjBiZDNiODhiZjg4NWI0X29yaWcuanBn' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/gatotv.com/gatotv.com.config.js b/sites/gatotv.com/gatotv.com.config.js index b814da0a..8eadaa9e 100644 --- a/sites/gatotv.com/gatotv.com.config.js +++ b/sites/gatotv.com/gatotv.com.config.js @@ -1,102 +1,102 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const url = require('url') -const path = require('path') -const { DateTime } = require('luxon') - -module.exports = { - site: 'gatotv.com', - days: 2, - url({ channel, date }) { - return `https://www.gatotv.com/canal/${channel.site_id}/${date.format('YYYY-MM-DD')}` - }, - parser({ content, date }) { - let programs = [] - const items = parseItems(content) - date = date.subtract(1, 'd') - items.forEach((item, i) => { - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (i === 0 && start.hour >= 5) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - let stop = parseStop($item, date) - if (stop < start) { - stop = stop.plus({ days: 1 }) - date = date.add(1, 'd') - } - - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - image: parseImage($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://www.gatotv.com/guia_tv/completa') - .then(response => response.data) - .catch(console.log) - - const $ = cheerio.load(data) - const items = $('.tbl_EPG_row,.tbl_EPG_rowAlternate').toArray() - - return items.map(item => { - const $item = cheerio.load(item) - const link = $item('td:nth-child(1) > div:nth-child(2) > a:nth-child(3)').attr('href') - const parsed = url.parse(link) - - return { - lang: 'es', - site_id: path.basename(parsed.pathname), - name: $item('td:nth-child(1) > div:nth-child(2) > a:nth-child(3)').text() - } - }) - } -} - -function parseTitle($item) { - return $item( - 'td:nth-child(4) > div > div > a > span,td:nth-child(3) > div > div > span,td:nth-child(3) > div > div > a > span' - ).text() -} - -function parseDescription($item) { - return $item('td:nth-child(4) > div').clone().children().remove().end().text().trim() -} - -function parseImage($item) { - return $item('td:nth-child(3) > a > img').attr('src') -} - -function parseStart($item, date) { - const time = $item('td:nth-child(1) > div > time').attr('datetime') - - return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { - zone: 'EST' - }).toUTC() -} - -function parseStop($item, date) { - const time = $item('td:nth-child(2) > div > time').attr('datetime') - - return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { - zone: 'EST' - }).toUTC() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $( - 'body > div.div_content > table:nth-child(8) > tbody > tr:nth-child(2) > td:nth-child(1) > table.tbl_EPG' - ) - .find('.tbl_EPG_row,.tbl_EPG_rowAlternate,.tbl_EPG_row_selected') - .toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const url = require('url') +const path = require('path') +const { DateTime } = require('luxon') + +module.exports = { + site: 'gatotv.com', + days: 2, + url({ channel, date }) { + return `https://www.gatotv.com/canal/${channel.site_id}/${date.format('YYYY-MM-DD')}` + }, + parser({ content, date }) { + let programs = [] + const items = parseItems(content) + date = date.subtract(1, 'd') + items.forEach((item, i) => { + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (i === 0 && start.hour >= 5) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + let stop = parseStop($item, date) + if (stop < start) { + stop = stop.plus({ days: 1 }) + date = date.add(1, 'd') + } + + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + image: parseImage($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://www.gatotv.com/guia_tv/completa') + .then(response => response.data) + .catch(console.log) + + const $ = cheerio.load(data) + const items = $('.tbl_EPG_row,.tbl_EPG_rowAlternate').toArray() + + return items.map(item => { + const $item = cheerio.load(item) + const link = $item('td:nth-child(1) > div:nth-child(2) > a:nth-child(3)').attr('href') + const parsed = url.parse(link) + + return { + lang: 'es', + site_id: path.basename(parsed.pathname), + name: $item('td:nth-child(1) > div:nth-child(2) > a:nth-child(3)').text() + } + }) + } +} + +function parseTitle($item) { + return $item( + 'td:nth-child(4) > div > div > a > span,td:nth-child(3) > div > div > span,td:nth-child(3) > div > div > a > span' + ).text() +} + +function parseDescription($item) { + return $item('td:nth-child(4) > div').clone().children().remove().end().text().trim() +} + +function parseImage($item) { + return $item('td:nth-child(3) > a > img').attr('src') +} + +function parseStart($item, date) { + const time = $item('td:nth-child(1) > div > time').attr('datetime') + + return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { + zone: 'EST' + }).toUTC() +} + +function parseStop($item, date) { + const time = $item('td:nth-child(2) > div > time').attr('datetime') + + return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { + zone: 'EST' + }).toUTC() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $( + 'body > div.div_content > table:nth-child(8) > tbody > tr:nth-child(2) > td:nth-child(1) > table.tbl_EPG' + ) + .find('.tbl_EPG_row,.tbl_EPG_rowAlternate,.tbl_EPG_row_selected') + .toArray() +} diff --git a/sites/gatotv.com/gatotv.com.test.js b/sites/gatotv.com/gatotv.com.test.js index f33dd541..ac8a6f5b 100644 --- a/sites/gatotv.com/gatotv.com.test.js +++ b/sites/gatotv.com/gatotv.com.test.js @@ -1,79 +1,79 @@ -const { parser, url } = require('./gatotv.com.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) - -let date = dayjs.utc('2023-06-13', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'm_0', - xmltv_id: '0porMovistarPlus.es' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://www.gatotv.com/canal/m_0/2023-06-13') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0.html'), 'utf8') - const results = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-06-13T04:30:00.000Z', - stop: '2023-06-13T05:32:00.000Z', - title: 'Supergarcía' - }) - - expect(results[1]).toMatchObject({ - start: '2023-06-13T05:32:00.000Z', - stop: '2023-06-13T06:59:00.000Z', - title: 'La resistencia' - }) - - expect(results[25]).toMatchObject({ - start: '2023-06-14T04:46:00.000Z', - stop: '2023-06-14T05:00:00.000Z', - title: 'Una familia absolutamente normal' - }) -}) - -it('can parse response when the guide starts from midnight', () => { - date = date.add(1, 'd') - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_1.html'), 'utf8') - const results = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-06-14T05:00:00.000Z', - stop: '2023-06-14T05:32:00.000Z', - title: 'Ilustres Ignorantes' - }) - - expect(results[26]).toMatchObject({ - start: '2023-06-15T04:30:00.000Z', - stop: '2023-06-15T05:30:00.000Z', - title: 'Showriano' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - channel, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./gatotv.com.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) + +let date = dayjs.utc('2023-06-13', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'm_0', + xmltv_id: '0porMovistarPlus.es' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://www.gatotv.com/canal/m_0/2023-06-13') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0.html'), 'utf8') + const results = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-06-13T04:30:00.000Z', + stop: '2023-06-13T05:32:00.000Z', + title: 'Supergarcía' + }) + + expect(results[1]).toMatchObject({ + start: '2023-06-13T05:32:00.000Z', + stop: '2023-06-13T06:59:00.000Z', + title: 'La resistencia' + }) + + expect(results[25]).toMatchObject({ + start: '2023-06-14T04:46:00.000Z', + stop: '2023-06-14T05:00:00.000Z', + title: 'Una familia absolutamente normal' + }) +}) + +it('can parse response when the guide starts from midnight', () => { + date = date.add(1, 'd') + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_1.html'), 'utf8') + const results = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-06-14T05:00:00.000Z', + stop: '2023-06-14T05:32:00.000Z', + title: 'Ilustres Ignorantes' + }) + + expect(results[26]).toMatchObject({ + start: '2023-06-15T04:30:00.000Z', + stop: '2023-06-15T05:30:00.000Z', + title: 'Showriano' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/getafteritmedia.com/getafteritmedia.com.config.js b/sites/getafteritmedia.com/getafteritmedia.com.config.js index a86068cb..5fdcaed4 100644 --- a/sites/getafteritmedia.com/getafteritmedia.com.config.js +++ b/sites/getafteritmedia.com/getafteritmedia.com.config.js @@ -1,64 +1,64 @@ -const table2array = require('table2array') -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 isoWeek = require('dayjs/plugin/isoWeek') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) -dayjs.extend(isoWeek) - -module.exports = { - site: 'getafteritmedia.com', - days: 2, - url: 'https://docs.google.com/spreadsheets/d/e/2PACX-1vQcDmb9OnO0HpbjINfGaepqgGTp3VSmPs7hs654n3sRKrq4Q9y6uPSEvVvq9MwTLYG_n_V7vh0rFYP9/pubhtml', - parser({ content, channel, date }) { - const programs = [] - const items = parseItems(content, channel, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: item.title, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item, date) { - return dayjs.tz( - `${date.format('YYYY-MM-DD')} ${item.time}`, - 'YYYY-MM-DD HH:mm A', - 'America/New_York' - ) -} - -function parseItems(content, channel, date) { - const day = date.isoWeekday() - const $ = cheerio.load(content) - const table = $.html($(`#${channel.site_id} table`)) - let data = table2array(table) - data.splice(0, 5) - - return data.map(row => { - return { - time: row[1], - title: row[day + 1] - } - }) -} +const table2array = require('table2array') +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 isoWeek = require('dayjs/plugin/isoWeek') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) +dayjs.extend(isoWeek) + +module.exports = { + site: 'getafteritmedia.com', + days: 2, + url: 'https://docs.google.com/spreadsheets/d/e/2PACX-1vQcDmb9OnO0HpbjINfGaepqgGTp3VSmPs7hs654n3sRKrq4Q9y6uPSEvVvq9MwTLYG_n_V7vh0rFYP9/pubhtml', + parser({ content, channel, date }) { + const programs = [] + const items = parseItems(content, channel, date) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: item.title, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item, date) { + return dayjs.tz( + `${date.format('YYYY-MM-DD')} ${item.time}`, + 'YYYY-MM-DD HH:mm A', + 'America/New_York' + ) +} + +function parseItems(content, channel, date) { + const day = date.isoWeekday() + const $ = cheerio.load(content) + const table = $.html($(`#${channel.site_id} table`)) + let data = table2array(table) + data.splice(0, 5) + + return data.map(row => { + return { + time: row[1], + title: row[day + 1] + } + }) +} diff --git a/sites/getafteritmedia.com/getafteritmedia.com.test.js b/sites/getafteritmedia.com/getafteritmedia.com.test.js index 5c5b5cb0..09937070 100644 --- a/sites/getafteritmedia.com/getafteritmedia.com.test.js +++ b/sites/getafteritmedia.com/getafteritmedia.com.test.js @@ -1,45 +1,45 @@ -const { parser, url } = require('./getafteritmedia.com.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('2022-11-26', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '494637005', - xmltv_id: 'REVNWebFeed.us' -} - -it('can generate valid url', () => { - expect(url).toBe( - 'https://docs.google.com/spreadsheets/d/e/2PACX-1vQcDmb9OnO0HpbjINfGaepqgGTp3VSmPs7hs654n3sRKrq4Q9y6uPSEvVvq9MwTLYG_n_V7vh0rFYP9/pubhtml' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-26T05:00:00.000Z', - stop: '2022-11-26T05:30:00.000Z', - title: 'The Appraisers' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./getafteritmedia.com.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('2022-11-26', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '494637005', + xmltv_id: 'REVNWebFeed.us' +} + +it('can generate valid url', () => { + expect(url).toBe( + 'https://docs.google.com/spreadsheets/d/e/2PACX-1vQcDmb9OnO0HpbjINfGaepqgGTp3VSmPs7hs654n3sRKrq4Q9y6uPSEvVvq9MwTLYG_n_V7vh0rFYP9/pubhtml' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-26T05:00:00.000Z', + stop: '2022-11-26T05:30:00.000Z', + title: 'The Appraisers' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/gigatv.3bbtv.co.th/gigatv.3bbtv.co.th.config.js b/sites/gigatv.3bbtv.co.th/gigatv.3bbtv.co.th.config.js index 61405de0..30a7c480 100644 --- a/sites/gigatv.3bbtv.co.th/gigatv.3bbtv.co.th.config.js +++ b/sites/gigatv.3bbtv.co.th/gigatv.3bbtv.co.th.config.js @@ -1,65 +1,65 @@ -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') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'gigatv.3bbtv.co.th', - days: 1, - url({ channel }) { - return `https://gigatv.3bbtv.co.th/wp-content/themes/changwattana/epg/${channel.site_id}.json` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content, date) - items.forEach(item => { - programs.push({ - title: item.programName, - start: parseTime(item.startTime), - stop: parseTime(item.endTime) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://gigatv.3bbtv.co.th/wp-content/themes/changwattana/epg/channel.json') - .then(r => r.data) - .catch(console.log) - - const channels = [] - data.forEach(group => { - group.channel_list.forEach(channel => { - channels.push({ - lang: 'th', - site_id: channel.channel_id, - name: channel.channel_name - }) - }) - }) - - return channels - } -} - -function parseTime(string) { - return dayjs.tz(string, 'YYYY-MM-DD HH:mm:ss', 'Asia/Bangkok') -} - -function parseItems(content, date) { - try { - let data = JSON.parse(content) - if (!Array.isArray(data)) return [] - data = data.filter(p => date.isSame(parseTime(p.startTime), 'day')) - - return data - } catch { - return [] - } -} +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') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'gigatv.3bbtv.co.th', + days: 1, + url({ channel }) { + return `https://gigatv.3bbtv.co.th/wp-content/themes/changwattana/epg/${channel.site_id}.json` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content, date) + items.forEach(item => { + programs.push({ + title: item.programName, + start: parseTime(item.startTime), + stop: parseTime(item.endTime) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://gigatv.3bbtv.co.th/wp-content/themes/changwattana/epg/channel.json') + .then(r => r.data) + .catch(console.log) + + const channels = [] + data.forEach(group => { + group.channel_list.forEach(channel => { + channels.push({ + lang: 'th', + site_id: channel.channel_id, + name: channel.channel_name + }) + }) + }) + + return channels + } +} + +function parseTime(string) { + return dayjs.tz(string, 'YYYY-MM-DD HH:mm:ss', 'Asia/Bangkok') +} + +function parseItems(content, date) { + try { + let data = JSON.parse(content) + if (!Array.isArray(data)) return [] + data = data.filter(p => date.isSame(parseTime(p.startTime), 'day')) + + return data + } catch { + return [] + } +} diff --git a/sites/gigatv.3bbtv.co.th/gigatv.3bbtv.co.th.test.js b/sites/gigatv.3bbtv.co.th/gigatv.3bbtv.co.th.test.js index 22f95224..37866c9d 100644 --- a/sites/gigatv.3bbtv.co.th/gigatv.3bbtv.co.th.test.js +++ b/sites/gigatv.3bbtv.co.th/gigatv.3bbtv.co.th.test.js @@ -1,45 +1,45 @@ -const { parser, url } = require('./gigatv.3bbtv.co.th.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-12', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '222', - xmltv_id: 'ThainessTV.th' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe( - 'https://gigatv.3bbtv.co.th/wp-content/themes/changwattana/epg/222.json' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(32) - expect(results[0]).toMatchObject({ - start: '2025-01-12T00:00:00.000Z', - stop: '2025-01-12T00:30:00.000Z', - title: 'THAILAND FORM ABOVE : TAK' - }) - expect(results[31]).toMatchObject({ - start: '2025-01-12T23:30:00.000Z', - stop: '2025-01-13T00:00:00.000Z', - title: 'MAESA ELEPHANT CAMP' - }) -}) - -it('can handle empty guide', () => { - expect(parser({ content: '', date })).toMatchObject([]) -}) +const { parser, url } = require('./gigatv.3bbtv.co.th.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-12', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '222', + xmltv_id: 'ThainessTV.th' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe( + 'https://gigatv.3bbtv.co.th/wp-content/themes/changwattana/epg/222.json' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(32) + expect(results[0]).toMatchObject({ + start: '2025-01-12T00:00:00.000Z', + stop: '2025-01-12T00:30:00.000Z', + title: 'THAILAND FORM ABOVE : TAK' + }) + expect(results[31]).toMatchObject({ + start: '2025-01-12T23:30:00.000Z', + stop: '2025-01-13T00:00:00.000Z', + title: 'MAESA ELEPHANT CAMP' + }) +}) + +it('can handle empty guide', () => { + expect(parser({ content: '', date })).toMatchObject([]) +}) diff --git a/sites/guiadetv.com/guiadetv.com.config.js b/sites/guiadetv.com/guiadetv.com.config.js index b63f49b2..d0e0a278 100644 --- a/sites/guiadetv.com/guiadetv.com.config.js +++ b/sites/guiadetv.com/guiadetv.com.config.js @@ -1,101 +1,101 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const dayjs = require('dayjs') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(customParseFormat) - -require('dayjs/locale/pt') - -module.exports = { - site: 'guiadetv.com', - days: 2, - url({ channel }) { - return `https://www.guiadetv.com/canal/${channel.site_id}` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - const title = parseTitle($item) - let start = parseStart($item) - if (!start || !title) return - if (prev) { - prev.stop = start - } - const stop = start.add(30, 'm') - - programs.push({ - title, - description: parseDescription($item), - category: parseCategory($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const categories = [ - 'variedades', - 'tv-aberta', - 'noticias', - 'infantil', - 'filmes-e-series', - 'esportes', - 'documentarios' - ] - const promises = categories.map(category => - axios.get(`https://www.guiadetv.com/categorias/${category}.html`) - ) - - const channels = [] - const results = await Promise.all(promises).catch(console.log) - results.forEach(r => { - const $ = cheerio.load(r.data) - $('.cardchannel').each((i, el) => { - const link = $(el).find('a') - const name = link.attr('title') - const url = link.attr('href') - const site_id = url.replace('https://www.guiadetv.com/canal/', '') - - channels.push({ - lang: 'pt', - name, - site_id - }) - }) - }) - - return channels - } -} - -function parseTitle($item) { - return $item('h3').text().trim() -} - -function parseDescription($item) { - return $item('p').clone().children().remove().end().text().trim() || null -} - -function parseCategory($item) { - return $item('p > i').text().trim() || null -} - -function parseStart($item) { - const dt = $item('b span:nth-child(1)').data('dt') || $item('b').data('dt') - if (!dt) return null - - return dayjs(dt, 'YYYY-MM-DD HH:mm:ssZ') -} - -function parseItems(content, date) { - const $ = cheerio.load(content) - const localDate = date.locale('pt').format('D MMMM YYYY') - - return $(`.row:contains(${localDate})`).nextUntil('.row:not(.mt-1)').toArray() -} +const cheerio = require('cheerio') +const axios = require('axios') +const dayjs = require('dayjs') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(customParseFormat) + +require('dayjs/locale/pt') + +module.exports = { + site: 'guiadetv.com', + days: 2, + url({ channel }) { + return `https://www.guiadetv.com/canal/${channel.site_id}` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + const title = parseTitle($item) + let start = parseStart($item) + if (!start || !title) return + if (prev) { + prev.stop = start + } + const stop = start.add(30, 'm') + + programs.push({ + title, + description: parseDescription($item), + category: parseCategory($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const categories = [ + 'variedades', + 'tv-aberta', + 'noticias', + 'infantil', + 'filmes-e-series', + 'esportes', + 'documentarios' + ] + const promises = categories.map(category => + axios.get(`https://www.guiadetv.com/categorias/${category}.html`) + ) + + const channels = [] + const results = await Promise.all(promises).catch(console.log) + results.forEach(r => { + const $ = cheerio.load(r.data) + $('.cardchannel').each((i, el) => { + const link = $(el).find('a') + const name = link.attr('title') + const url = link.attr('href') + const site_id = url.replace('https://www.guiadetv.com/canal/', '') + + channels.push({ + lang: 'pt', + name, + site_id + }) + }) + }) + + return channels + } +} + +function parseTitle($item) { + return $item('h3').text().trim() +} + +function parseDescription($item) { + return $item('p').clone().children().remove().end().text().trim() || null +} + +function parseCategory($item) { + return $item('p > i').text().trim() || null +} + +function parseStart($item) { + const dt = $item('b span:nth-child(1)').data('dt') || $item('b').data('dt') + if (!dt) return null + + return dayjs(dt, 'YYYY-MM-DD HH:mm:ssZ') +} + +function parseItems(content, date) { + const $ = cheerio.load(content) + const localDate = date.locale('pt').format('D MMMM YYYY') + + return $(`.row:contains(${localDate})`).nextUntil('.row:not(.mt-1)').toArray() +} diff --git a/sites/guiadetv.com/guiadetv.com.test.js b/sites/guiadetv.com/guiadetv.com.test.js index d28bd6dd..3d7c41ca 100644 --- a/sites/guiadetv.com/guiadetv.com.test.js +++ b/sites/guiadetv.com/guiadetv.com.test.js @@ -1,80 +1,80 @@ -const { parser, url } = require('./guiadetv.com.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: 'canal-rural', - xmltv_id: 'CanalRural.br' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://www.guiadetv.com/canal/canal-rural') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(16) - expect(results[0]).toMatchObject({ - start: '2025-01-18T03:00:00.000Z', - stop: '2025-01-18T04:00:00.000Z', - title: 'Leilão', - description: null, - category: null - }) - expect(results[2]).toMatchObject({ - start: '2025-01-18T06:00:00.000Z', - stop: '2025-01-18T09:00:00.000Z', - title: 'TV Verdade', - description: null, - category: 'Jornalismo' - }) - expect(results[15]).toMatchObject({ - start: '2025-01-19T00:00:00.000Z', - stop: '2025-01-19T00:30:00.000Z', - title: 'Leilão', - description: null, - category: null - }) -}) - -it('can parse response for current day', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date: dayjs.utc('2025-01-15', 'YYYY-MM-DD').startOf('d') }).map( - p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - } - ) - - expect(results.length).toBe(7) - expect(results[0]).toMatchObject({ - start: '2025-01-15T21:15:00.000Z', - stop: '2025-01-15T21:45:00.000Z', - title: 'Planeta Campo Talks', - description: - 'Grandes reportagens, notícias, entrevistas e debates com foco em ações de sustentabilidade e indicadores ESG. Informações para apoiar o produtor rural a plantar e criar com olhar para o futuro.', - category: null - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./guiadetv.com.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: 'canal-rural', + xmltv_id: 'CanalRural.br' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://www.guiadetv.com/canal/canal-rural') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(16) + expect(results[0]).toMatchObject({ + start: '2025-01-18T03:00:00.000Z', + stop: '2025-01-18T04:00:00.000Z', + title: 'Leilão', + description: null, + category: null + }) + expect(results[2]).toMatchObject({ + start: '2025-01-18T06:00:00.000Z', + stop: '2025-01-18T09:00:00.000Z', + title: 'TV Verdade', + description: null, + category: 'Jornalismo' + }) + expect(results[15]).toMatchObject({ + start: '2025-01-19T00:00:00.000Z', + stop: '2025-01-19T00:30:00.000Z', + title: 'Leilão', + description: null, + category: null + }) +}) + +it('can parse response for current day', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date: dayjs.utc('2025-01-15', 'YYYY-MM-DD').startOf('d') }).map( + p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + } + ) + + expect(results.length).toBe(7) + expect(results[0]).toMatchObject({ + start: '2025-01-15T21:15:00.000Z', + stop: '2025-01-15T21:45:00.000Z', + title: 'Planeta Campo Talks', + description: + 'Grandes reportagens, notícias, entrevistas e debates com foco em ações de sustentabilidade e indicadores ESG. Informações para apoiar o produtor rural a plantar e criar com olhar para o futuro.', + category: null + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/guida.tv/guida.tv.config.js b/sites/guida.tv/guida.tv.config.js index 0c14a1cd..810c53a2 100644 --- a/sites/guida.tv/guida.tv.config.js +++ b/sites/guida.tv/guida.tv.config.js @@ -1,99 +1,98 @@ -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: 'guida.tv', - days: 2, - url: function ({ date, channel }) { - return `https://www.guida.tv/programmi-tv/palinsesto/canale/${ - channel.site_id - }.html?dt=${date.format('YYYY-MM-DD')}` - }, - parser: function ({ content, date, channel }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date, channel) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - const _ = require('lodash') - - const providers = ['-1', '-2', '-3'] - - const channels = [] - for (let provider of providers) { - const data = await axios - .post('https://www.guida.tv/guide/schedule', null, { - params: { - provider, - region: 'Italy', - TVperiod: 'Night', - date: dayjs().format('YYYY-MM-DD'), - st: 0, - u_time: 1429, - is_mobile: 1 - } - }) - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(data) - $('.channelname').each((i, el) => { - const name = $(el).find('center > a:eq(1)').text() - const url = $(el).find('center > a:eq(1)').attr('href') - const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/) - - channels.push({ - lang: 'it', - name, - site_id: `${number}/${slug}` - }) - }) - } - - return _.uniqBy(channels, 'site_id') - } -} - -function parseStart($item, date) { - const timeString = $item('td:eq(0)').text().trim() - const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` - - return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Europe/Rome') -} - -function parseTitle($item) { - return $item('td:eq(1)').text().trim() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('table.table > tbody > tr').toArray() -} +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 uniqBy = require('lodash.uniqby') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'guida.tv', + days: 2, + url: function ({ date, channel }) { + return `https://www.guida.tv/programmi-tv/palinsesto/canale/${ + channel.site_id + }.html?dt=${date.format('YYYY-MM-DD')}` + }, + parser: function ({ content, date, channel }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date, channel) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const providers = ['-1', '-2', '-3'] + + const channels = [] + for (let provider of providers) { + const data = await axios + .post('https://www.guida.tv/guide/schedule', null, { + params: { + provider, + region: 'Italy', + TVperiod: 'Night', + date: dayjs().format('YYYY-MM-DD'), + st: 0, + u_time: 1429, + is_mobile: 1 + } + }) + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(data) + $('.channelname').each((i, el) => { + const name = $(el).find('center > a:eq(1)').text() + const url = $(el).find('center > a:eq(1)').attr('href') + const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/) + + channels.push({ + lang: 'it', + name, + site_id: `${number}/${slug}` + }) + }) + } + + return uniqBy(channels, 'site_id') + } +} + +function parseStart($item, date) { + const timeString = $item('td:eq(0)').text().trim() + const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` + + return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Europe/Rome') +} + +function parseTitle($item) { + return $item('td:eq(1)').text().trim() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('table.table > tbody > tr').toArray() +} diff --git a/sites/guida.tv/guida.tv.test.js b/sites/guida.tv/guida.tv.test.js index 7c2d32c3..fda470ca 100644 --- a/sites/guida.tv/guida.tv.test.js +++ b/sites/guida.tv/guida.tv.test.js @@ -1,50 +1,50 @@ -const { parser, url } = require('./guida.tv.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('2023-11-24', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '826573/rai-1', - xmltv_id: 'Rai1.it' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.guida.tv/programmi-tv/palinsesto/canale/826573/rai-1.html?dt=2023-11-24' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-11-24T00:10:00.000Z', - stop: '2023-11-24T01:05:00.000Z', - title: 'Viva Rai2!' - }) - - expect(results[30]).toMatchObject({ - start: '2023-11-24T23:00:00.000Z', - stop: '2023-11-24T23:30:00.000Z', - title: 'TV 7' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./guida.tv.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('2023-11-24', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '826573/rai-1', + xmltv_id: 'Rai1.it' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.guida.tv/programmi-tv/palinsesto/canale/826573/rai-1.html?dt=2023-11-24' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-11-24T00:10:00.000Z', + stop: '2023-11-24T01:05:00.000Z', + title: 'Viva Rai2!' + }) + + expect(results[30]).toMatchObject({ + start: '2023-11-24T23:00:00.000Z', + stop: '2023-11-24T23:30:00.000Z', + title: 'TV 7' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/guidatv.sky.it/__data__/content.json b/sites/guidatv.sky.it/__data__/content.json new file mode 100644 index 00000000..a391e8bf --- /dev/null +++ b/sites/guidatv.sky.it/__data__/content.json @@ -0,0 +1 @@ +{"events": [ { "channel": { "id": 10458, "logo": "/logo/545820mediasethd_Light_Fit.png", "logoPadding": "/logo/545820mediasethd_Light_Padding.png", "logoDark": "/logo/545820mediasethd_Dark_Fit.png", "logoDarkPadding": "/logo/545820mediasethd_Dark_Padding.png", "logoLight": "/logo/545820mediasethd_Light_Padding.png", "name": "20Mediaset HD", "number": 151, "category": { "id": 3, "name": "Intrattenimento" } }, "content": { "uuid": "77c630aa-4744-44cb-a88e-3e871c6b73d9", "contentTitle": "Distretto di Polizia", "episodeNumber": 26, "seasonNumber": 6, "url": "/serie-tv/distretto-di-polizia/stagione-6/episodio-26/77c630aa-4744-44cb-a88e-3e871c6b73d9", "genre": { "id": 1, "name": "Intrattenimento" }, "subgenre": { "id": 9, "name": "Fiction" }, "imagesMap": [ { "key": "background", "img": { "url": "/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/background?md5ChecksumParam=88d3f48ce855316f4be25ab9bb846d32" } }, { "key": "cover", "img": { "url": "/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/cover?md5ChecksumParam=61135b999a63e3d3f4a933b9edeb0c1b" } }, { "key": "scene", "img": { "url": "/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/16-9?md5ChecksumParam=f41bfe414bec32505abdab19d00b8b43" } } ] }, "eventId": "139585132", "starttime": "2022-05-06T00:35:40Z", "endtime": "2022-05-06T01:15:40Z", "eventTitle": "Distretto di Polizia", "eventSynopsis": "S6 Ep26 La resa dei conti - Fino all'ultimo la sfida tra Ardenzi e Carrano, nemici di vecchia data, riserva clamorosi colpi di scena. E si scopre che non e' tutto come sembrava.", "epgEventTitle": "S6 Ep26 - Distretto di Polizia", "primeVision": false, "resolutions": [ { "resolutionType": "resolution4k", "value": false } ] }]} \ No newline at end of file diff --git a/sites/guidatv.sky.it/__data__/no_content.json b/sites/guidatv.sky.it/__data__/no_content.json new file mode 100644 index 00000000..33ba57bf --- /dev/null +++ b/sites/guidatv.sky.it/__data__/no_content.json @@ -0,0 +1 @@ +{"events":[],"total":0} \ No newline at end of file diff --git a/sites/guidatv.sky.it/guidatv.sky.it.config.js b/sites/guidatv.sky.it/guidatv.sky.it.config.js index ddafb89d..0649292f 100644 --- a/sites/guidatv.sky.it/guidatv.sky.it.config.js +++ b/sites/guidatv.sky.it/guidatv.sky.it.config.js @@ -1,98 +1,98 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'guidatv.sky.it', - days: 2, - url: function ({ date, channel }) { - const [env, id] = channel.site_id.split('#') - return `https://apid.sky.it/gtv/v1/events?from=${date.format('YYYY-MM-DD')}T00:00:00Z&to=${date - .add(1, 'd') - .format('YYYY-MM-DD')}T00:00:00Z&pageSize=999&pageNum=0&env=${env}&channels=${id}` - }, - parser: function ({ content }) { - const programs = [] - const data = JSON.parse(content) - const items = data.events - if (!items.length) return programs - items.forEach(item => { - programs.push({ - title: item.eventTitle, - description: item.eventSynopsis, - category: parseCategory(item), - season: parseSeason(item), - episode: parseEpisode(item), - start: parseStart(item), - stop: parseStop(item), - url: parseURL(item), - image: parseImage(item) - }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - const cheerio = require('cheerio') - - const data = await axios - .get('https://guidatv.sky.it/canali') - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(data) - - let channels = [] - $('.c-channelsCard__container').each((i, el) => { - const name = $(el).find('.c-channelsCard__title').text() - const url = $(el).find('.c-channelsCard__link').attr('href') - const [, channelId] = url.match(/\/(\d+)$/) - - channels.push({ - lang: 'it', - site_id: `DTH#${channelId}`, - name - }) - }) - - return channels - } -} - -function parseCategory(item) { - let category = item.content.genre.name || null - const subcategory = item.content.subgenre.name || null - if (category && subcategory) { - category += `/${subcategory}` - } - return category -} - -function parseStart(item) { - return item.starttime ? dayjs(item.starttime) : null -} - -function parseStop(item) { - return item.endtime ? dayjs(item.endtime) : null -} - -function parseURL(item) { - return item.content.url ? `https://guidatv.sky.it${item.content.url}` : null -} - -function parseImage(item) { - const cover = item.content.imagesMap ? item.content.imagesMap.find(i => i.key === 'cover') : null - - return cover && cover.img && cover.img.url ? `https://guidatv.sky.it${cover.img.url}` : null -} - -function parseSeason(item) { - if (!item.content.seasonNumber) return null - if (String(item.content.seasonNumber).length > 2) return null - return item.content.seasonNumber -} - -function parseEpisode(item) { - if (!item.content.episodeNumber) return null - if (String(item.content.episodeNumber).length > 3) return null - return item.content.episodeNumber -} +const dayjs = require('dayjs') + +module.exports = { + site: 'guidatv.sky.it', + days: 2, + url: function ({ date, channel }) { + const [env, id] = channel.site_id.split('#') + return `https://apid.sky.it/gtv/v1/events?from=${date.format('YYYY-MM-DD')}T00:00:00Z&to=${date + .add(1, 'd') + .format('YYYY-MM-DD')}T00:00:00Z&pageSize=999&pageNum=0&env=${env}&channels=${id}` + }, + parser: function ({ content }) { + const programs = [] + const data = JSON.parse(content) + const items = data.events + if (!items.length) return programs + items.forEach(item => { + programs.push({ + title: item.eventTitle, + description: item.eventSynopsis, + category: parseCategory(item), + season: parseSeason(item), + episode: parseEpisode(item), + start: parseStart(item), + stop: parseStop(item), + url: parseURL(item), + image: parseImage(item) + }) + }) + + return programs + }, + async channels() { + const axios = require('axios') + const cheerio = require('cheerio') + + const data = await axios + .get('https://guidatv.sky.it/canali') + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(data) + + let channels = [] + $('.c-channelsCard__container').each((i, el) => { + const name = $(el).find('.c-channelsCard__title').text() + const url = $(el).find('.c-channelsCard__link').attr('href') + const [, channelId] = url.match(/\/(\d+)$/) + + channels.push({ + lang: 'it', + site_id: `DTH#${channelId}`, + name + }) + }) + + return channels + } +} + +function parseCategory(item) { + let category = item.content.genre.name || null + const subcategory = item.content.subgenre.name || null + if (category && subcategory) { + category += `/${subcategory}` + } + return category +} + +function parseStart(item) { + return item.starttime ? dayjs(item.starttime) : null +} + +function parseStop(item) { + return item.endtime ? dayjs(item.endtime) : null +} + +function parseURL(item) { + return item.content.url ? `https://guidatv.sky.it${item.content.url}` : null +} + +function parseImage(item) { + const cover = item.content.imagesMap ? item.content.imagesMap.find(i => i.key === 'cover') : null + + return cover && cover.img && cover.img.url ? `https://guidatv.sky.it${cover.img.url}` : null +} + +function parseSeason(item) { + if (!item.content.seasonNumber) return null + if (String(item.content.seasonNumber).length > 2) return null + return item.content.seasonNumber +} + +function parseEpisode(item) { + if (!item.content.episodeNumber) return null + if (String(item.content.episodeNumber).length > 3) return null + return item.content.episodeNumber +} diff --git a/sites/guidatv.sky.it/guidatv.sky.it.test.js b/sites/guidatv.sky.it/guidatv.sky.it.test.js index a114c1e0..6ed28155 100644 --- a/sites/guidatv.sky.it/guidatv.sky.it.test.js +++ b/sites/guidatv.sky.it/guidatv.sky.it.test.js @@ -1,51 +1,52 @@ -const { parser, url } = require('./guidatv.sky.it.config.js') -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('2022-05-06', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'DTH#10458', - xmltv_id: '20Mediaset.it' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://apid.sky.it/gtv/v1/events?from=2022-05-06T00:00:00Z&to=2022-05-07T00:00:00Z&pageSize=999&pageNum=0&env=DTH&channels=10458' - ) -}) - -it('can parse response', () => { - const content = - '{"events": [ { "channel": { "id": 10458, "logo": "/logo/545820mediasethd_Light_Fit.png", "logoPadding": "/logo/545820mediasethd_Light_Padding.png", "logoDark": "/logo/545820mediasethd_Dark_Fit.png", "logoDarkPadding": "/logo/545820mediasethd_Dark_Padding.png", "logoLight": "/logo/545820mediasethd_Light_Padding.png", "name": "20Mediaset HD", "number": 151, "category": { "id": 3, "name": "Intrattenimento" } }, "content": { "uuid": "77c630aa-4744-44cb-a88e-3e871c6b73d9", "contentTitle": "Distretto di Polizia", "episodeNumber": 26, "seasonNumber": 6, "url": "/serie-tv/distretto-di-polizia/stagione-6/episodio-26/77c630aa-4744-44cb-a88e-3e871c6b73d9", "genre": { "id": 1, "name": "Intrattenimento" }, "subgenre": { "id": 9, "name": "Fiction" }, "imagesMap": [ { "key": "background", "img": { "url": "/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/background?md5ChecksumParam=88d3f48ce855316f4be25ab9bb846d32" } }, { "key": "cover", "img": { "url": "/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/cover?md5ChecksumParam=61135b999a63e3d3f4a933b9edeb0c1b" } }, { "key": "scene", "img": { "url": "/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/16-9?md5ChecksumParam=f41bfe414bec32505abdab19d00b8b43" } } ] }, "eventId": "139585132", "starttime": "2022-05-06T00:35:40Z", "endtime": "2022-05-06T01:15:40Z", "eventTitle": "Distretto di Polizia", "eventSynopsis": "S6 Ep26 La resa dei conti - Fino all\'ultimo la sfida tra Ardenzi e Carrano, nemici di vecchia data, riserva clamorosi colpi di scena. E si scopre che non e\' tutto come sembrava.", "epgEventTitle": "S6 Ep26 - Distretto di Polizia", "primeVision": false, "resolutions": [ { "resolutionType": "resolution4k", "value": false } ] }]}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-05-06T00:35:40.000Z', - stop: '2022-05-06T01:15:40.000Z', - title: 'Distretto di Polizia', - description: - "S6 Ep26 La resa dei conti - Fino all'ultimo la sfida tra Ardenzi e Carrano, nemici di vecchia data, riserva clamorosi colpi di scena. E si scopre che non e' tutto come sembrava.", - season: 6, - episode: 26, - image: - 'https://guidatv.sky.it/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/cover?md5ChecksumParam=61135b999a63e3d3f4a933b9edeb0c1b', - category: 'Intrattenimento/Fiction', - url: 'https://guidatv.sky.it/serie-tv/distretto-di-polizia/stagione-6/episodio-26/77c630aa-4744-44cb-a88e-3e871c6b73d9' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '{"events":[],"total":0}' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./guidatv.sky.it.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('2022-05-06', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'DTH#10458', + xmltv_id: '20Mediaset.it' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://apid.sky.it/gtv/v1/events?from=2022-05-06T00:00:00Z&to=2022-05-07T00:00:00Z&pageSize=999&pageNum=0&env=DTH&channels=10458' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-05-06T00:35:40.000Z', + stop: '2022-05-06T01:15:40.000Z', + title: 'Distretto di Polizia', + description: + "S6 Ep26 La resa dei conti - Fino all'ultimo la sfida tra Ardenzi e Carrano, nemici di vecchia data, riserva clamorosi colpi di scena. E si scopre che non e' tutto come sembrava.", + season: 6, + episode: 26, + image: + 'https://guidatv.sky.it/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/cover?md5ChecksumParam=61135b999a63e3d3f4a933b9edeb0c1b', + category: 'Intrattenimento/Fiction', + url: 'https://guidatv.sky.it/serie-tv/distretto-di-polizia/stagione-6/episodio-26/77c630aa-4744-44cb-a88e-3e871c6b73d9' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/guidetnt.com/guidetnt.com.config.js b/sites/guidetnt.com/guidetnt.com.config.js index 9e00934c..bc0fcf1b 100755 --- a/sites/guidetnt.com/guidetnt.com.config.js +++ b/sites/guidetnt.com/guidetnt.com.config.js @@ -19,11 +19,11 @@ module.exports = { const now = dayjs() const demain = now.add(1, 'd') if (date && date.isSame(demain, 'day')) { - return `https://www.guidetnt.com/tv-demain/programme-${channel.site_id}` + return `https://www.guidetnt.com/tv-demain/programme-${channel.site_id}` } else if (!date || date.isSame(now, 'day')) { - return `https://www.guidetnt.com/tv/programme-${channel.site_id}` + return `https://www.guidetnt.com/tv/programme-${channel.site_id}` } else { - return null + return null } }, async parser({ content, date }) { @@ -57,8 +57,8 @@ module.exports = { let category = parseCategory($item) let description = parseDescription($item) const itemDetailsURL = parseDescriptionURL($item) - if(itemDetailsURL) { - const url = 'https://www.guidetnt.com' + itemDetailsURL + if (itemDetailsURL) { + const url = 'https://www.guidetnt.com' + itemDetailsURL try { const response = await axios.get(url) itemDetails = parseItemDetails(response.data) @@ -66,21 +66,21 @@ module.exports = { console.error(`Erreur lors du fetch des détails pour l'item: ${url}`, err) } - const timeRange = parseTimeRange(itemDetails?.programHour, date.format('YYYY-MM-DD')) - start = timeRange?.start - stop = timeRange?.stop + const timeRange = parseTimeRange(itemDetails?.programHour, date.format('YYYY-MM-DD')) + start = timeRange?.start + stop = timeRange?.stop - subTitle = itemDetails?.subTitle - if (title == subTitle) subTitle = null - description = itemDetails?.description + subTitle = itemDetails?.subTitle + if (title == subTitle) subTitle = null + description = itemDetails?.description - const categoryDetails = parseCategoryText(itemDetails?.category) - //duration = categoryDetails?.duration - country = categoryDetails?.country - productionDate = categoryDetails?.productionDate - season = categoryDetails?.season - episode = categoryDetails?.episode - } + const categoryDetails = parseCategoryText(itemDetails?.category) + //duration = categoryDetails?.duration + country = categoryDetails?.country + productionDate = categoryDetails?.productionDate + season = categoryDetails?.season + episode = categoryDetails?.episode + } // See https://www.npmjs.com/package/epg-parser for parameters programs.push({ title, @@ -110,119 +110,124 @@ module.exports = { // Look inside each .tvlogo container $('.tvlogo').each((i, el) => { // Find all descendants that have an alt attribute - $(el).find('[alt]').each((j, subEl) => { - const alt = $(subEl).attr('alt') - const href = $(subEl).attr('href') - if (href && alt && alt.trim() !== '') { - const name = alt.trim() - const site_id = href.replace(/^\/tv\/programme-/, '') - channels.push({ - lang: 'fr', - name, - site_id - }) - } - }) + $(el) + .find('[alt]') + .each((j, subEl) => { + const alt = $(subEl).attr('alt') + const href = $(subEl).attr('href') + if (href && alt && alt.trim() !== '') { + const name = alt.trim() + const site_id = href.replace(/^\/tv\/programme-/, '') + channels.push({ + lang: 'fr', + name, + site_id + }) + } + }) }) return channels } } function parseTimeRange(timeRange, baseDate) { - // Split times - const [startStr, endStr] = timeRange.split(' - ').map(s => s.trim()) + // Split times + const [startStr, endStr] = timeRange.split(' - ').map(s => s.trim()) - // Parse with base date - const start = dayjs(`${baseDate} ${startStr}`, 'YYYY-MM-DD HH:mm') - let end = dayjs(`${baseDate} ${endStr}`, 'YYYY-MM-DD HH:mm') + // Parse with base date + const start = dayjs(`${baseDate} ${startStr}`, 'YYYY-MM-DD HH:mm') + let end = dayjs(`${baseDate} ${endStr}`, 'YYYY-MM-DD HH:mm') - // Handle possible day wrap (e.g., 23:30 - 00:15) - if (end.isBefore(start)) { - end = end.add(1, 'day') - } + // Handle possible day wrap (e.g., 23:30 - 00:15) + if (end.isBefore(start)) { + end = end.add(1, 'day') + } - // Calculate duration in minutes - const diffMinutes = end.diff(start, 'minute') + // Calculate duration in minutes + const diffMinutes = end.diff(start, 'minute') - return { - start: start.format(), - stop: end.format(), - duration: diffMinutes - } + return { + start: start.format(), + stop: end.format(), + duration: diffMinutes + } } function parseItemDetails(itemDetails) { - const $ = cheerio.load(itemDetails) + const $ = cheerio.load(itemDetails) - const program = $('.program-wrapper').first() + const program = $('.program-wrapper').first() - const programHour = program.find('.program-hour').text().trim() - const programTitle = program.find('.program-title').text().trim() - const programElementBold = program.find('.program-element-bold').text().trim() - const programArea1 = program.find('.program-element.program-area-1').text().trim() + const programHour = program.find('.program-hour').text().trim() + const programTitle = program.find('.program-title').text().trim() + const programElementBold = program.find('.program-element-bold').text().trim() + const programArea1 = program.find('.program-element.program-area-1').text().trim() - let description = '' - const programElements = $('.program-element').filter((i, el) => { - const classAttr = $(el).attr('class') - // Return true only if it is exactly "program-element" (no extra classes) - return classAttr.trim() === 'program-element' - }) + let description = '' + const programElements = $('.program-element').filter((i, el) => { + const classAttr = $(el).attr('class') + // Return true only if it is exactly "program-element" (no extra classes) + return classAttr.trim() === 'program-element' + }) - programElements.each((i, el) => { - description += $(el).text().trim() - }) + programElements.each((i, el) => { + description += $(el).text().trim() + }) - const area2Node = $('.program-area-2').first() - const area2 = $(area2Node) - const data = {} - let currentLabel = null - let texts = [] + const area2Node = $('.program-area-2').first() + const area2 = $(area2Node) + const data = {} + let currentLabel = null + let texts = [] - area2.contents().each((i, node) => { - if (node.type === 'tag' && node.name === 'strong') { - // If we had collected some text for the previous label, save it - if (currentLabel && texts.length) { - data[currentLabel] = texts.join('').trim().replace(/,\s*$/, '') // Remove trailing comma - } - // New label - get text without colon - currentLabel = $(node).text().replace(/:$/, '').trim() - texts = [] - } else if (currentLabel) { - // Append the text content (text node or others) - if (node.type === 'text') { - texts.push(node.data) - } else if (node.type === 'tag' && node.name !== 'strong' && node.name !== 'br') { - texts.push($(node).text()) - } + area2.contents().each((i, node) => { + if (node.type === 'tag' && node.name === 'strong') { + // If we had collected some text for the previous label, save it + if (currentLabel && texts.length) { + data[currentLabel] = texts.join('').trim().replace(/,\s*$/, '') // Remove trailing comma + } + // New label - get text without colon + currentLabel = $(node).text().replace(/:$/, '').trim() + texts = [] + } else if (currentLabel) { + // Append the text content (text node or others) + if (node.type === 'text') { + texts.push(node.data) + } else if (node.type === 'tag' && node.name !== 'strong' && node.name !== 'br') { + texts.push($(node).text()) + } } - }) + }) - // Save last label text - if (currentLabel && texts.length) { - data[currentLabel] = texts.join('').trim().replace(/,\s*$/, '') - } + // Save last label text + if (currentLabel && texts.length) { + data[currentLabel] = texts.join('').trim().replace(/,\s*$/, '') + } - const imgSrc = program.find('div[style*="float:left"]')?.find('img')?.attr('src') || null + const imgSrc = program.find('div[style*="float:left"]')?.find('img')?.attr('src') || null - return { - programHour, - title: programTitle, - subTitle: programElementBold, - category: programArea1, - description: description, - directorActors: data, - image: imgSrc - } + return { + programHour, + title: programTitle, + subTitle: programElementBold, + category: programArea1, + description: description, + directorActors: data, + image: imgSrc + } } function parseCategoryText(text) { if (!text) return null - const parts = text.split(',').map(s => s.trim()).filter(Boolean) + const parts = text + .split(',') + .map(s => s.trim()) + .filter(Boolean) const len = parts.length const category = parts[0] || null - + if (len < 3) { return { category: category, @@ -252,18 +257,18 @@ function parseCategoryText(text) { //duration = [{ units: 'minutes', value: durationMinute }], durationIndex = i } else if (parts[i].toLowerCase().includes('épisode')) { - const match = text.match(/épisode\s+(\d+)(?:\/(\d+))?/i) - if (match) { - episode = parseInt(match[1], 10) - } + const match = text.match(/épisode\s+(\d+)(?:\/(\d+))?/i) + if (match) { + episode = parseInt(match[1], 10) + } } else if (parts[i].toLowerCase().includes('saison')) { - season = parts[i].replace('saison', '').trim() + season = parts[i].replace('saison', '').trim() } } // Country: second to last const countryIndex = len - 2 - let country = (durationIndex === countryIndex) ? null : parts[countryIndex] + let country = durationIndex === countryIndex ? null : parts[countryIndex] return { category, @@ -325,9 +330,9 @@ function parseItems(content) { const rows = $('.channel-row').toArray() return { - rows, - logoSrc, - title, - formattedDate + rows, + logoSrc, + title, + formattedDate } -} \ No newline at end of file +} diff --git a/sites/guidetnt.com/guidetnt.com.test.js b/sites/guidetnt.com/guidetnt.com.test.js index 0ee3906a..9b83b7d0 100644 --- a/sites/guidetnt.com/guidetnt.com.test.js +++ b/sites/guidetnt.com/guidetnt.com.test.js @@ -32,7 +32,8 @@ it('can parse response', async () => { expect(results.length).toBe(29) expect(results[0]).toMatchObject({ category: 'Série', - description: 'Grande effervescence pour toute l\'équipe du Camping Paradis, qui prépare les Olympiades. Côté arrivants, Hélène et sa fille Eva viennent passer quelques jours dans le but d\'optimiser les révisions d\'E...', + description: + "Grande effervescence pour toute l'équipe du Camping Paradis, qui prépare les Olympiades. Côté arrivants, Hélène et sa fille Eva viennent passer quelques jours dans le but d'optimiser les révisions d'E...", start: '2025-06-30T22:55:00.000Z', stop: '2025-06-30T23:45:00.000Z', title: 'Camping Paradis' @@ -45,11 +46,12 @@ it('can parse response', async () => { title: 'Programmes de la nuit' }) expect(results[15]).toMatchObject({ - category: 'Téléfilm', - description: 'La vie quasi parfaite de Riley bascule brutalement lorsqu\'un accident de voiture lui coûte la vie, laissant derrière elle sa famille. Alors que l\'enquête débute, l\'affaire prend une tournure étrange l...', + category: 'Téléfilm', + description: + "La vie quasi parfaite de Riley bascule brutalement lorsqu'un accident de voiture lui coûte la vie, laissant derrière elle sa famille. Alors que l'enquête débute, l'affaire prend une tournure étrange l...", start: '2025-07-01T12:25:00.000Z', stop: '2025-07-01T14:00:00.000Z', - title: 'Trahie par l\'amour' + title: "Trahie par l'amour" }) }) @@ -57,16 +59,16 @@ it('can parse response for current day', async () => { const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) let results = await parser({ content, date: dayjs.utc('2025-07-01', 'YYYY-MM-DD').startOf('d') }) results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - } - ) + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) expect(results.length).toBe(29) expect(results[0]).toMatchObject({ category: 'Série', - description: 'Grande effervescence pour toute l\'équipe du Camping Paradis, qui prépare les Olympiades. Côté arrivants, Hélène et sa fille Eva viennent passer quelques jours dans le but d\'optimiser les révisions d\'E...', + description: + "Grande effervescence pour toute l'équipe du Camping Paradis, qui prépare les Olympiades. Côté arrivants, Hélène et sa fille Eva viennent passer quelques jours dans le but d'optimiser les révisions d'E...", start: '2025-06-30T22:55:00.000Z', stop: '2025-06-30T23:45:00.000Z', title: 'Camping Paradis' diff --git a/sites/horizon.tv/__data__/content.json b/sites/horizon.tv/__data__/content.json new file mode 100644 index 00000000..663646ab --- /dev/null +++ b/sites/horizon.tv/__data__/content.json @@ -0,0 +1 @@ +{"entryCount":184,"totalResults":184,"updated":1675790518889,"expires":1675791343825,"title":"EPG","periods":4,"periodStartTime":1675724400000,"periodEndTime":1675746000000,"entries":[{"o":"lgi-obolite-sk-prod-master:10024","l":[{"i":"crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78","t":"Avengement","s":1675719300000,"e":1675724700000,"c":"lgi-obolite-sk-prod-master:genre-9","a":false,"r":true,"rm":true,"rs":0,"re":604800,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]} \ No newline at end of file diff --git a/sites/horizon.tv/__data__/content_1.json b/sites/horizon.tv/__data__/content_1.json new file mode 100644 index 00000000..9dd620fb --- /dev/null +++ b/sites/horizon.tv/__data__/content_1.json @@ -0,0 +1 @@ +{"entryCount":184,"totalResults":184,"updated":1675790518889,"expires":1675791376097,"title":"EPG","periods":4,"periodStartTime":1675746000000,"periodEndTime":1675767600000,"entries":[{"o":"lgi-obolite-sk-prod-master:10024","l":[{"i":"crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b","t":"Zoom In","s":1675744500000,"e":1675746000000,"c":"lgi-obolite-sk-prod-master:genre-21","a":false,"r":true,"rm":true,"rs":0,"re":604800,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]} \ No newline at end of file diff --git a/sites/horizon.tv/__data__/content_2.json b/sites/horizon.tv/__data__/content_2.json new file mode 100644 index 00000000..b2265e4c --- /dev/null +++ b/sites/horizon.tv/__data__/content_2.json @@ -0,0 +1 @@ +{"entryCount":184,"totalResults":184,"updated":1675789948804,"expires":1675791024984,"title":"EPG","periods":4,"periodStartTime":1675767600000,"periodEndTime":1675789200000,"entries":[{"o":"lgi-obolite-sk-prod-master:10024","l":[{"i":"crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad","t":"Studentka","s":1675761000000,"e":1675767600000,"c":"lgi-obolite-sk-prod-master:genre-14","a":false,"r":true,"rm":true,"rs":0,"re":604800,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]} \ No newline at end of file diff --git a/sites/horizon.tv/__data__/content_3.json b/sites/horizon.tv/__data__/content_3.json new file mode 100644 index 00000000..d09b3423 --- /dev/null +++ b/sites/horizon.tv/__data__/content_3.json @@ -0,0 +1 @@ +{"entryCount":184,"totalResults":184,"updated":1675789948804,"expires":1675790973469,"title":"EPG","periods":4,"periodStartTime":1675789200000,"periodEndTime":1675810800000,"entries":[{"o":"lgi-obolite-sk-prod-master:10024","l":[{"i":"crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7","t":"Zilionáři","s":1675785900000,"e":1675791900000,"c":"lgi-obolite-sk-prod-master:genre-9","a":false,"r":true,"rm":true,"rs":0,"re":604800,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]} \ No newline at end of file diff --git a/sites/horizon.tv/__data__/content_listings_1.json b/sites/horizon.tv/__data__/content_listings_1.json new file mode 100644 index 00000000..991a55bd --- /dev/null +++ b/sites/horizon.tv/__data__/content_listings_1.json @@ -0,0 +1 @@ +{"id":"crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78","startTime":1675719300000,"endTime":1675724700000,"actualStartTime":1675719300000,"actualEndTime":1675724700000,"expirationDate":1676324100000,"stationId":"lgi-obolite-sk-prod-master:10024","imi":"imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78","scCridImi":"crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78","mediaGroupId":"crid:~~2F~~2Fport.cs~~2F122941980","program":{"id":"crid:~~2F~~2Fport.cs~~2F122941980","title":"Avengement","description":"Během propustky z vězení za účelem návštěvy umírající matky v nemocnici zločinec Cain Burgess (Scott Adkins) unikne svým dozorcům a mizí v ulicích Londýna. Jde o epickou cestu krve a bolesti za...","longDescription":"Během propustky z vězení za účelem návštěvy umírající matky v nemocnici zločinec Cain Burgess (Scott Adkins) unikne svým dozorcům a mizí v ulicích Londýna. Jde o epickou cestu krve a bolesti za dosažením vytoužené pomsty na těch, kteří z něj udělali chladnokrevného vraha.","medium":"Movie","categories":[{"id":"lgi-obolite-sk-prod-master:genre-9","title":"Drama","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-obolite-sk-prod-master:genre-33","title":"Akcia","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"parentalRating":"18","cast":["Scott Adkins","Craig Fairbrass","Thomas Turgoose","Nick Moran","Kierston Wareing","Leo Gregory","Mark Strange","Luke LaFontaine","Beau Fowler","Dan Styles","Christopher Sciueref","Matt Routledge","Jane Thorne","Louis Mandylor","Terence Maynard","Greg Burridge","Michael Higgs","Damian Gallagher","Daniel Adegboyega","John Ioannou","Sofie Golding-Spittle","Joe Egan","Darren Swain","Lee Charles","Dominic Kinnaird","Ross O'Hennessy","Teresa Mahoney","Andrew Dunkelberger","Sam Hardy","Ivan Moy","Mark Sears","Phillip Ray Tommy"],"directors":["Jesse V. Johnson"],"images":[{"assetType":"HighResLandscape","assetTypes":["HighResLandscape"],"url":"http://62.179.125.152/SK/Images/hrl_3fa8387df870473fdacb1024635b52b2496b159c.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"http://62.179.125.152/SK/Images/hrp_19e3a660e637cd39e31046c284a66b3a95d698e4.jpg"},{"assetType":"boxCover","assetTypes":["boxCover"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg"},{"assetType":"boxart-small","assetTypes":["boxart-small"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg?w=75&h=108&mode=box"},{"assetType":"boxart-medium","assetTypes":["boxart-medium"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg?w=110&h=159&mode=box"},{"assetType":"boxart-xlarge","assetTypes":["boxart-xlarge"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg?w=210&h=303&mode=box"},{"assetType":"boxart-large","assetTypes":["boxart-large"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg?w=180&h=260&mode=box"}],"rootId":"crid:~~2F~~2Fport.cs~~2F122941980","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fport.cs~~2F122941980","shortDescription":"Během propustky z vězení za účelem návštěvy umírající matky v nemocnici zločinec Cain Burgess (Scott Adkins) unikne svým dozorcům a mizí v ulicích Londýna. Jde o epickou cestu krve a bolesti za...","mediaType":"FeatureFilm","year":"2019","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"rootId":"crid:~~2F~~2Fport.cs~~2F122941980","replayTvAvailable":true,"audioTracks":[],"ratings":[],"offersLatestExpirationDate":1676247300000,"replayTvStartOffset":0,"replayTvEndOffset":604800,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false} \ No newline at end of file diff --git a/sites/horizon.tv/__data__/content_listings_2.json b/sites/horizon.tv/__data__/content_listings_2.json new file mode 100644 index 00000000..5b33c125 --- /dev/null +++ b/sites/horizon.tv/__data__/content_listings_2.json @@ -0,0 +1 @@ +{"id":"crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b","startTime":1675744500000,"endTime":1675746000000,"actualStartTime":1675744500000,"actualEndTime":1675746000000,"expirationDate":1676349300000,"stationId":"lgi-obolite-sk-prod-master:10024","imi":"imi:e85129f9d1e211406a521df7a36f22237c22651b","scCridImi":"crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b","mediaGroupId":"crid:~~2F~~2Fport.cs~~2F41764266","program":{"id":"crid:~~2F~~2Fport.cs~~2F248281986","title":"Zoom In","description":"Film/Kino","longDescription":"Film/Kino","medium":"TV","categories":[{"id":"lgi-obolite-sk-prod-master:genre-21","title":"Hudba a umenie","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-obolite-sk-prod-master:genre-14","title":"Film","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"parentalRating":"9","cast":[],"directors":[],"images":[{"assetType":"HighResLandscape","assetTypes":["HighResLandscape"],"url":"http://62.179.125.152/SK/Images/hrl_cbed64b557e83227a2292604cbcae2d193877b1c.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"http://62.179.125.152/SK/Images/hrp_cfe405e669385365846b69196e1e94caa3e60de0.jpg"},{"assetType":"boxCover","assetTypes":["boxCover"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg"},{"assetType":"boxart-small","assetTypes":["boxart-small"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg?w=75&h=108&mode=box"},{"assetType":"boxart-medium","assetTypes":["boxart-medium"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg?w=110&h=159&mode=box"},{"assetType":"boxart-xlarge","assetTypes":["boxart-xlarge"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg?w=210&h=303&mode=box"},{"assetType":"boxart-large","assetTypes":["boxart-large"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg?w=180&h=260&mode=box"}],"parentId":"crid:~~2F~~2Fport.cs~~2F41764266_series","rootId":"crid:~~2F~~2Fport.cs~~2F41764266","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fport.cs~~2F41764266","shortDescription":"Film/Kino","mediaType":"Episode","year":"2010","seriesEpisodeNumber":"1302070535","seriesNumber":"1302080520","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"parentId":"crid:~~2F~~2Fport.cs~~2F41764266_series","rootId":"crid:~~2F~~2Fport.cs~~2F41764266","replayTvAvailable":true,"audioTracks":[],"ratings":[],"offersLatestExpirationDate":1675746000000,"replayTvStartOffset":0,"replayTvEndOffset":604800,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false} \ No newline at end of file diff --git a/sites/horizon.tv/__data__/content_listings_3.json b/sites/horizon.tv/__data__/content_listings_3.json new file mode 100644 index 00000000..367ab8bd --- /dev/null +++ b/sites/horizon.tv/__data__/content_listings_3.json @@ -0,0 +1 @@ +{"id":"crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad","startTime":1675761000000,"endTime":1675767600000,"actualStartTime":1675761000000,"actualEndTime":1675767600000,"expirationDate":1676365800000,"stationId":"lgi-obolite-sk-prod-master:10024","imi":"imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad","scCridImi":"crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad","mediaGroupId":"crid:~~2F~~2Fport.cs~~2F1379541","program":{"id":"crid:~~2F~~2Fport.cs~~2F1379541","title":"Studentka","description":"Ambiciózní vysokoškolačka Valentina (Sophie Marceau) studuje literaturu na pařížské Sorbonně a právě se připravuje k závěrečným zkouškám. Žádný odpočinek, žádné volno, žádné večírky, téměř žádný...","longDescription":"Ambiciózní vysokoškolačka Valentina (Sophie Marceau) studuje literaturu na pařížské Sorbonně a právě se připravuje k závěrečným zkouškám. Žádný odpočinek, žádné volno, žádné večírky, téměř žádný spánek a především a hlavně ... žádná láska! Věří, že jedině tak obstojí před zkušební komisí. Jednoho dne se však odehraje něco, s čím nepočítala. Potká charismatického hudebníka Neda - a bláznivě se zamiluje. V tuto chvíli stojí před osudovým rozhodnutím: zahodí roky obrovského studijního nasazení, nebo odmítne lásku? Nebo se snad dá obojí skloubit dohromady?","medium":"Movie","categories":[{"id":"lgi-obolite-sk-prod-master:genre-14","title":"Film","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-obolite-sk-prod-master:genre-4","title":"Komédia","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"parentalRating":"9","cast":["Sophie Marceauová","Vincent Lindon","Elisabeth Vitali","Elena Pompei","Jean-Claude Leguay","Brigitte Chamarande","Christian Pereira","Gérard Dacier","Roberto Attias","Beppe Chierici","Nathalie Mann","Anne Macina","Janine Souchon","Virginie Demians","Hugues Leforestier","Jacqueline Noëlle","Marc-André Brunet","Isabelle Caubère","André Chazel","Med Salah Cheurfi","Guillaume Corea","Eric Denize","Gilles Gaston-Dreyfuss","Benoît Gourley","Marc Innocenti","Najim Laouriga","Laurent Ledermann","Philippe Maygal","Dominique Pifarely","Ysé Tran"],"directors":["Francis De Gueltz","Dominique Talmon","Claude Pinoteau"],"images":[{"assetType":"HighResLandscape","assetTypes":["HighResLandscape"],"url":"http://62.179.125.152/SK/Images/hrl_a8abceaa59bbb0aae8031dcdd5deba03aba8a100.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"http://62.179.125.152/SK/Images/hrp_72b11621270454812ac8474698fc75670db4a49d.jpg"},{"assetType":"boxCover","assetTypes":["boxCover"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg"},{"assetType":"boxart-small","assetTypes":["boxart-small"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg?w=75&h=108&mode=box"},{"assetType":"boxart-medium","assetTypes":["boxart-medium"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg?w=110&h=159&mode=box"},{"assetType":"boxart-xlarge","assetTypes":["boxart-xlarge"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg?w=210&h=303&mode=box"},{"assetType":"boxart-large","assetTypes":["boxart-large"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg?w=180&h=260&mode=box"}],"rootId":"crid:~~2F~~2Fport.cs~~2F1379541","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fport.cs~~2F1379541","shortDescription":"Ambiciózní vysokoškolačka Valentina (Sophie Marceau) studuje literaturu na pařížské Sorbonně a právě se připravuje k závěrečným zkouškám. Žádný odpočinek, žádné volno, žádné večírky, téměř žádný...","mediaType":"FeatureFilm","year":"1988","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"rootId":"crid:~~2F~~2Fport.cs~~2F1379541","replayTvAvailable":true,"audioTracks":[],"ratings":[],"offersLatestExpirationDate":1675767600000,"replayTvStartOffset":0,"replayTvEndOffset":604800,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false} \ No newline at end of file diff --git a/sites/horizon.tv/__data__/content_listings_4.json b/sites/horizon.tv/__data__/content_listings_4.json new file mode 100644 index 00000000..a701660b --- /dev/null +++ b/sites/horizon.tv/__data__/content_listings_4.json @@ -0,0 +1 @@ +{"id":"crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7","startTime":1675785900000,"endTime":1675791900000,"actualStartTime":1675785900000,"actualEndTime":1675791900000,"expirationDate":1676390700000,"stationId":"lgi-obolite-sk-prod-master:10024","imi":"imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7","scCridImi":"crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7","mediaGroupId":"crid:~~2F~~2Fport.cs~~2F71927954","program":{"id":"crid:~~2F~~2Fport.cs~~2F71927954","title":"Zilionáři","description":"David (Zach Galifianakis) je nekomplikovaný muž, který uvízl v monotónním životě. Den co den usedá za volant svého obrněného automobilu, aby odvážel obrovské sumy peněz jiných lidí. Jediným...","longDescription":"David (Zach Galifianakis) je nekomplikovaný muž, který uvízl v monotónním životě. Den co den usedá za volant svého obrněného automobilu, aby odvážel obrovské sumy peněz jiných lidí. Jediným vzrušujícím momentem v jeho životě je flirtování s kolegyní Kelly (Kristen Wiig), která ho však brzy zatáhne do těžko uvěřitelného dobrodružství. Skupinka nepříliš inteligentních loserů, pod vedením Steva (Owen Wilson), plánuje vyloupit banku a David jim v tom má samozřejmě pomoci. Navzdory absolutně amatérskému plánu se ale stane nemožné a oni mají najednou v kapse 17 miliónů dolarů. A protože tato partička je opravdu bláznivá, začne je hned ve velkém roztáčet. Peníze létají vzduchem za luxusní a kolikrát i zbytečné věci, ale nedochází jim, že pro policii tak zanechávají jasné stopy...","medium":"Movie","categories":[{"id":"lgi-obolite-sk-prod-master:genre-9","title":"Drama","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-obolite-sk-prod-master:genre-33","title":"Akcia","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"parentalRating":"15","cast":["Zach Galifianakis","Kristen Wiigová","Owen Wilson","Kate McKinnon","Leslie Jones","Jason Sudeikis","Ross Kimball","Devin Ratray","Mary Elizabeth Ellisová","Jon Daly","Ken Marino","Daniel Zacapa","Tom Werme","Njema Williams","Nils Cruz","Michael Fraguada","Christian Gonzalez","Candace Blanchard","Karsten Friske","Dallas Edwards","Barry Ratcliffe","Shelton Grant","Laura Palka","Reegus Flenory","Wynn Reichert","Jill Jane Clements","Joseph S. Wilson","Jee An","Rhoda Griffisová","Nicole Dupre Sobchack"],"directors":["Scott August","Richard L. Fox","Michelle Malley-Campos","Sebastian Mazzola","Steven Ritzi","Pete Waterman","Jared Hess"],"images":[{"assetType":"HighResLandscape","assetTypes":["HighResLandscape"],"url":"http://62.179.125.152/SK/Images/hrl_fd098116bac1429318aaf5fdae498ce76e258782.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"http://62.179.125.152/SK/Images/hrp_6f857ae9375b3bcceb6353a5b35775f52cd85302.jpg"},{"assetType":"boxCover","assetTypes":["boxCover"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg"},{"assetType":"boxart-small","assetTypes":["boxart-small"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg?w=75&h=108&mode=box"},{"assetType":"boxart-medium","assetTypes":["boxart-medium"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg?w=110&h=159&mode=box"},{"assetType":"boxart-xlarge","assetTypes":["boxart-xlarge"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg?w=210&h=303&mode=box"},{"assetType":"boxart-large","assetTypes":["boxart-large"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg?w=180&h=260&mode=box"}],"rootId":"crid:~~2F~~2Fport.cs~~2F71927954","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fport.cs~~2F71927954","shortDescription":"David (Zach Galifianakis) je nekomplikovaný muž, který uvízl v monotónním životě. Den co den usedá za volant svého obrněného automobilu, aby odvážel obrovské sumy peněz jiných lidí. Jediným...","mediaType":"FeatureFilm","year":"2016","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"rootId":"crid:~~2F~~2Fport.cs~~2F71927954","replayTvAvailable":true,"audioTracks":[],"ratings":[],"offersLatestExpirationDate":1676187900000,"replayTvStartOffset":0,"replayTvEndOffset":604800,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false} \ No newline at end of file diff --git a/sites/horizon.tv/__data__/no_content.json b/sites/horizon.tv/__data__/no_content.json new file mode 100644 index 00000000..105dad50 --- /dev/null +++ b/sites/horizon.tv/__data__/no_content.json @@ -0,0 +1 @@ +[{"type":"PATH_PARAM","code":"period","reason":"INVALID"}] \ No newline at end of file diff --git a/sites/horizon.tv/horizon.tv.config.js b/sites/horizon.tv/horizon.tv.config.js index 225021ca..0444317c 100644 --- a/sites/horizon.tv/horizon.tv.config.js +++ b/sites/horizon.tv/horizon.tv.config.js @@ -1,145 +1,145 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const API_ENDPOINT = 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web' - -module.exports = { - site: 'horizon.tv', - days: 3, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: function ({ date }) { - return `${API_ENDPOINT}/programschedules/${date.format('YYYYMMDD')}/1` - }, - async parser({ content, channel, date }) { - let programs = [] - let items = parseItems(content, channel) - if (!items.length) return programs - const d = date.format('YYYYMMDD') - const promises = [ - axios.get(`${API_ENDPOINT}/programschedules/${d}/2`), - axios.get(`${API_ENDPOINT}/programschedules/${d}/3`), - axios.get(`${API_ENDPOINT}/programschedules/${d}/4`) - ] - await Promise.allSettled(promises) - .then(results => { - results.forEach(r => { - if (r.status === 'fulfilled') { - items = items.concat(parseItems(r.value.data, channel)) - } - }) - }) - .catch(console.error) - for (let item of items) { - const detail = await loadProgramDetails(item) - programs.push({ - title: item.t, - description: parseDescription(detail), - category: parseCategory(detail), - season: parseSeason(detail), - episode: parseEpisode(detail), - actors: parseActors(detail), - directors: parseDirectors(detail), - date: parseYear(detail), - start: parseStart(item), - stop: parseStop(item) - }) - } - - return programs - }, - async channels() { - const data = await axios - .get(`${API_ENDPOINT}/channels`) - .then(r => r.data) - .catch(console.log) - - return data.channels.map(item => { - return { - lang: 'sk', - site_id: item.id.replace('lgi-obolite-sk-prod-master:5-', ''), - name: item.title - } - }) - } -} - -async function loadProgramDetails(item) { - if (!item.i) return {} - const url = `${API_ENDPOINT}/listings/${item.i}` - const data = await axios - .get(url) - .then(r => r.data) - .catch(console.log) - return data || {} -} - -function parseStart(item) { - return dayjs(item.s) -} - -function parseStop(item) { - return dayjs(item.e) -} - -function parseItems(content, channel) { - if (!content) return [] - const data = typeof content === 'string' ? JSON.parse(content) : content - if (!data || !Array.isArray(data.entries)) return [] - const entity = data.entries.find(e => e.o === `lgi-obolite-sk-prod-master:${channel.site_id}`) - return entity ? entity.l : [] -} - -function parseDescription(detail) { - if (!detail) return [] - if (!detail.program) return [] - return detail.program.longDescription || null -} - -function parseCategory(detail) { - if (!detail) return [] - if (!detail.program) return [] - if (!detail.program.categories) return [] - let categories = [] - detail.program.categories.forEach(category => { - categories.push(category.title) - }) - return categories -} - -function parseSeason(detail) { - if (!detail) return null - if (!detail.program) return null - if (!detail.program.seriesNumber) return null - if (String(detail.program.seriesNumber).length > 2) return null - return detail.program.seriesNumber -} - -function parseEpisode(detail) { - if (!detail) return null - if (!detail.program) return null - if (!detail.program.seriesEpisodeNumber) return null - if (String(detail.program.seriesEpisodeNumber).length > 3) return null - return detail.program.seriesEpisodeNumber -} - -function parseDirectors(detail) { - if (!detail) return [] - if (!detail.program) return [] - return detail.program.directors || [] -} - -function parseActors(detail) { - if (!detail) return [] - if (!detail.program) return [] - return detail.program.cast || [] -} - -function parseYear(detail) { - if (!detail) return null - if (!detail.program) return null - return detail.program.year || null -} +const axios = require('axios') +const dayjs = require('dayjs') + +const API_ENDPOINT = 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web' + +module.exports = { + site: 'horizon.tv', + days: 3, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: function ({ date }) { + return `${API_ENDPOINT}/programschedules/${date.format('YYYYMMDD')}/1` + }, + async parser({ content, channel, date }) { + let programs = [] + let items = parseItems(content, channel) + if (!items.length) return programs + const d = date.format('YYYYMMDD') + const promises = [ + axios.get(`${API_ENDPOINT}/programschedules/${d}/2`), + axios.get(`${API_ENDPOINT}/programschedules/${d}/3`), + axios.get(`${API_ENDPOINT}/programschedules/${d}/4`) + ] + await Promise.allSettled(promises) + .then(results => { + results.forEach(r => { + if (r.status === 'fulfilled') { + items = items.concat(parseItems(r.value.data, channel)) + } + }) + }) + .catch(console.error) + for (let item of items) { + const detail = await loadProgramDetails(item) + programs.push({ + title: item.t, + description: parseDescription(detail), + category: parseCategory(detail), + season: parseSeason(detail), + episode: parseEpisode(detail), + actors: parseActors(detail), + directors: parseDirectors(detail), + date: parseYear(detail), + start: parseStart(item), + stop: parseStop(item) + }) + } + + return programs + }, + async channels() { + const data = await axios + .get(`${API_ENDPOINT}/channels`) + .then(r => r.data) + .catch(console.log) + + return data.channels.map(item => { + return { + lang: 'sk', + site_id: item.id.replace('lgi-obolite-sk-prod-master:5-', ''), + name: item.title + } + }) + } +} + +async function loadProgramDetails(item) { + if (!item.i) return {} + const url = `${API_ENDPOINT}/listings/${item.i}` + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + return data || {} +} + +function parseStart(item) { + return dayjs(item.s) +} + +function parseStop(item) { + return dayjs(item.e) +} + +function parseItems(content, channel) { + if (!content) return [] + const data = typeof content === 'string' ? JSON.parse(content) : content + if (!data || !Array.isArray(data.entries)) return [] + const entity = data.entries.find(e => e.o === `lgi-obolite-sk-prod-master:${channel.site_id}`) + return entity ? entity.l : [] +} + +function parseDescription(detail) { + if (!detail) return [] + if (!detail.program) return [] + return detail.program.longDescription || null +} + +function parseCategory(detail) { + if (!detail) return [] + if (!detail.program) return [] + if (!detail.program.categories) return [] + let categories = [] + detail.program.categories.forEach(category => { + categories.push(category.title) + }) + return categories +} + +function parseSeason(detail) { + if (!detail) return null + if (!detail.program) return null + if (!detail.program.seriesNumber) return null + if (String(detail.program.seriesNumber).length > 2) return null + return detail.program.seriesNumber +} + +function parseEpisode(detail) { + if (!detail) return null + if (!detail.program) return null + if (!detail.program.seriesEpisodeNumber) return null + if (String(detail.program.seriesEpisodeNumber).length > 3) return null + return detail.program.seriesEpisodeNumber +} + +function parseDirectors(detail) { + if (!detail) return [] + if (!detail.program) return [] + return detail.program.directors || [] +} + +function parseActors(detail) { + if (!detail) return [] + if (!detail.program) return [] + return detail.program.cast || [] +} + +function parseYear(detail) { + if (!detail) return null + if (!detail.program) return null + return detail.program.year || null +} diff --git a/sites/horizon.tv/horizon.tv.test.js b/sites/horizon.tv/horizon.tv.test.js index 5f1c78d7..3d27a296 100644 --- a/sites/horizon.tv/horizon.tv.test.js +++ b/sites/horizon.tv/horizon.tv.test.js @@ -1,263 +1,246 @@ -const { parser, url } = require('./horizon.tv.config.js') -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('2023-02-07', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '10024', - xmltv_id: 'AMCCzechRepublic.cz' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/1' - ) -}) - -it('can parse response', done => { - const content = - '{"entryCount":184,"totalResults":184,"updated":1675790518889,"expires":1675791343825,"title":"EPG","periods":4,"periodStartTime":1675724400000,"periodEndTime":1675746000000,"entries":[{"o":"lgi-obolite-sk-prod-master:10024","l":[{"i":"crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78","t":"Avengement","s":1675719300000,"e":1675724700000,"c":"lgi-obolite-sk-prod-master:genre-9","a":false,"r":true,"rm":true,"rs":0,"re":604800,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' - - axios.get.mockImplementation(url => { - if ( - url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/2' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"entryCount":184,"totalResults":184,"updated":1675790518889,"expires":1675791376097,"title":"EPG","periods":4,"periodStartTime":1675746000000,"periodEndTime":1675767600000,"entries":[{"o":"lgi-obolite-sk-prod-master:10024","l":[{"i":"crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b","t":"Zoom In","s":1675744500000,"e":1675746000000,"c":"lgi-obolite-sk-prod-master:genre-21","a":false,"r":true,"rm":true,"rs":0,"re":604800,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' - ) - }) - } else if ( - url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/3' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"entryCount":184,"totalResults":184,"updated":1675789948804,"expires":1675791024984,"title":"EPG","periods":4,"periodStartTime":1675767600000,"periodEndTime":1675789200000,"entries":[{"o":"lgi-obolite-sk-prod-master:10024","l":[{"i":"crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad","t":"Studentka","s":1675761000000,"e":1675767600000,"c":"lgi-obolite-sk-prod-master:genre-14","a":false,"r":true,"rm":true,"rs":0,"re":604800,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' - ) - }) - } else if ( - url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/4' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"entryCount":184,"totalResults":184,"updated":1675789948804,"expires":1675790973469,"title":"EPG","periods":4,"periodStartTime":1675789200000,"periodEndTime":1675810800000,"entries":[{"o":"lgi-obolite-sk-prod-master:10024","l":[{"i":"crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7","t":"Zilionáři","s":1675785900000,"e":1675791900000,"c":"lgi-obolite-sk-prod-master:genre-9","a":false,"r":true,"rm":true,"rs":0,"re":604800,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' - ) - }) - } else if ( - url === - 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"id":"crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78","startTime":1675719300000,"endTime":1675724700000,"actualStartTime":1675719300000,"actualEndTime":1675724700000,"expirationDate":1676324100000,"stationId":"lgi-obolite-sk-prod-master:10024","imi":"imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78","scCridImi":"crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78","mediaGroupId":"crid:~~2F~~2Fport.cs~~2F122941980","program":{"id":"crid:~~2F~~2Fport.cs~~2F122941980","title":"Avengement","description":"Během propustky z vězení za účelem návštěvy umírající matky v nemocnici zločinec Cain Burgess (Scott Adkins) unikne svým dozorcům a mizí v ulicích Londýna. Jde o epickou cestu krve a bolesti za...","longDescription":"Během propustky z vězení za účelem návštěvy umírající matky v nemocnici zločinec Cain Burgess (Scott Adkins) unikne svým dozorcům a mizí v ulicích Londýna. Jde o epickou cestu krve a bolesti za dosažením vytoužené pomsty na těch, kteří z něj udělali chladnokrevného vraha.","medium":"Movie","categories":[{"id":"lgi-obolite-sk-prod-master:genre-9","title":"Drama","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-obolite-sk-prod-master:genre-33","title":"Akcia","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"parentalRating":"18","cast":["Scott Adkins","Craig Fairbrass","Thomas Turgoose","Nick Moran","Kierston Wareing","Leo Gregory","Mark Strange","Luke LaFontaine","Beau Fowler","Dan Styles","Christopher Sciueref","Matt Routledge","Jane Thorne","Louis Mandylor","Terence Maynard","Greg Burridge","Michael Higgs","Damian Gallagher","Daniel Adegboyega","John Ioannou","Sofie Golding-Spittle","Joe Egan","Darren Swain","Lee Charles","Dominic Kinnaird","Ross O\'Hennessy","Teresa Mahoney","Andrew Dunkelberger","Sam Hardy","Ivan Moy","Mark Sears","Phillip Ray Tommy"],"directors":["Jesse V. Johnson"],"images":[{"assetType":"HighResLandscape","assetTypes":["HighResLandscape"],"url":"http://62.179.125.152/SK/Images/hrl_3fa8387df870473fdacb1024635b52b2496b159c.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"http://62.179.125.152/SK/Images/hrp_19e3a660e637cd39e31046c284a66b3a95d698e4.jpg"},{"assetType":"boxCover","assetTypes":["boxCover"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg"},{"assetType":"boxart-small","assetTypes":["boxart-small"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg?w=75&h=108&mode=box"},{"assetType":"boxart-medium","assetTypes":["boxart-medium"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg?w=110&h=159&mode=box"},{"assetType":"boxart-xlarge","assetTypes":["boxart-xlarge"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg?w=210&h=303&mode=box"},{"assetType":"boxart-large","assetTypes":["boxart-large"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg?w=180&h=260&mode=box"}],"rootId":"crid:~~2F~~2Fport.cs~~2F122941980","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fport.cs~~2F122941980","shortDescription":"Během propustky z vězení za účelem návštěvy umírající matky v nemocnici zločinec Cain Burgess (Scott Adkins) unikne svým dozorcům a mizí v ulicích Londýna. Jde o epickou cestu krve a bolesti za...","mediaType":"FeatureFilm","year":"2019","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"rootId":"crid:~~2F~~2Fport.cs~~2F122941980","replayTvAvailable":true,"audioTracks":[],"ratings":[],"offersLatestExpirationDate":1676247300000,"replayTvStartOffset":0,"replayTvEndOffset":604800,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false}' - ) - }) - } else if ( - url === - 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"id":"crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b","startTime":1675744500000,"endTime":1675746000000,"actualStartTime":1675744500000,"actualEndTime":1675746000000,"expirationDate":1676349300000,"stationId":"lgi-obolite-sk-prod-master:10024","imi":"imi:e85129f9d1e211406a521df7a36f22237c22651b","scCridImi":"crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b","mediaGroupId":"crid:~~2F~~2Fport.cs~~2F41764266","program":{"id":"crid:~~2F~~2Fport.cs~~2F248281986","title":"Zoom In","description":"Film/Kino","longDescription":"Film/Kino","medium":"TV","categories":[{"id":"lgi-obolite-sk-prod-master:genre-21","title":"Hudba a umenie","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-obolite-sk-prod-master:genre-14","title":"Film","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"parentalRating":"9","cast":[],"directors":[],"images":[{"assetType":"HighResLandscape","assetTypes":["HighResLandscape"],"url":"http://62.179.125.152/SK/Images/hrl_cbed64b557e83227a2292604cbcae2d193877b1c.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"http://62.179.125.152/SK/Images/hrp_cfe405e669385365846b69196e1e94caa3e60de0.jpg"},{"assetType":"boxCover","assetTypes":["boxCover"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg"},{"assetType":"boxart-small","assetTypes":["boxart-small"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg?w=75&h=108&mode=box"},{"assetType":"boxart-medium","assetTypes":["boxart-medium"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg?w=110&h=159&mode=box"},{"assetType":"boxart-xlarge","assetTypes":["boxart-xlarge"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg?w=210&h=303&mode=box"},{"assetType":"boxart-large","assetTypes":["boxart-large"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg?w=180&h=260&mode=box"}],"parentId":"crid:~~2F~~2Fport.cs~~2F41764266_series","rootId":"crid:~~2F~~2Fport.cs~~2F41764266","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fport.cs~~2F41764266","shortDescription":"Film/Kino","mediaType":"Episode","year":"2010","seriesEpisodeNumber":"1302070535","seriesNumber":"1302080520","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"parentId":"crid:~~2F~~2Fport.cs~~2F41764266_series","rootId":"crid:~~2F~~2Fport.cs~~2F41764266","replayTvAvailable":true,"audioTracks":[],"ratings":[],"offersLatestExpirationDate":1675746000000,"replayTvStartOffset":0,"replayTvEndOffset":604800,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false}' - ) - }) - } else if ( - url === - 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"id":"crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad","startTime":1675761000000,"endTime":1675767600000,"actualStartTime":1675761000000,"actualEndTime":1675767600000,"expirationDate":1676365800000,"stationId":"lgi-obolite-sk-prod-master:10024","imi":"imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad","scCridImi":"crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad","mediaGroupId":"crid:~~2F~~2Fport.cs~~2F1379541","program":{"id":"crid:~~2F~~2Fport.cs~~2F1379541","title":"Studentka","description":"Ambiciózní vysokoškolačka Valentina (Sophie Marceau) studuje literaturu na pařížské Sorbonně a právě se připravuje k závěrečným zkouškám. Žádný odpočinek, žádné volno, žádné večírky, téměř žádný...","longDescription":"Ambiciózní vysokoškolačka Valentina (Sophie Marceau) studuje literaturu na pařížské Sorbonně a právě se připravuje k závěrečným zkouškám. Žádný odpočinek, žádné volno, žádné večírky, téměř žádný spánek a především a hlavně ... žádná láska! Věří, že jedině tak obstojí před zkušební komisí. Jednoho dne se však odehraje něco, s čím nepočítala. Potká charismatického hudebníka Neda - a bláznivě se zamiluje. V tuto chvíli stojí před osudovým rozhodnutím: zahodí roky obrovského studijního nasazení, nebo odmítne lásku? Nebo se snad dá obojí skloubit dohromady?","medium":"Movie","categories":[{"id":"lgi-obolite-sk-prod-master:genre-14","title":"Film","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-obolite-sk-prod-master:genre-4","title":"Komédia","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"parentalRating":"9","cast":["Sophie Marceauová","Vincent Lindon","Elisabeth Vitali","Elena Pompei","Jean-Claude Leguay","Brigitte Chamarande","Christian Pereira","Gérard Dacier","Roberto Attias","Beppe Chierici","Nathalie Mann","Anne Macina","Janine Souchon","Virginie Demians","Hugues Leforestier","Jacqueline Noëlle","Marc-André Brunet","Isabelle Caubère","André Chazel","Med Salah Cheurfi","Guillaume Corea","Eric Denize","Gilles Gaston-Dreyfuss","Benoît Gourley","Marc Innocenti","Najim Laouriga","Laurent Ledermann","Philippe Maygal","Dominique Pifarely","Ysé Tran"],"directors":["Francis De Gueltz","Dominique Talmon","Claude Pinoteau"],"images":[{"assetType":"HighResLandscape","assetTypes":["HighResLandscape"],"url":"http://62.179.125.152/SK/Images/hrl_a8abceaa59bbb0aae8031dcdd5deba03aba8a100.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"http://62.179.125.152/SK/Images/hrp_72b11621270454812ac8474698fc75670db4a49d.jpg"},{"assetType":"boxCover","assetTypes":["boxCover"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg"},{"assetType":"boxart-small","assetTypes":["boxart-small"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg?w=75&h=108&mode=box"},{"assetType":"boxart-medium","assetTypes":["boxart-medium"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg?w=110&h=159&mode=box"},{"assetType":"boxart-xlarge","assetTypes":["boxart-xlarge"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg?w=210&h=303&mode=box"},{"assetType":"boxart-large","assetTypes":["boxart-large"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg?w=180&h=260&mode=box"}],"rootId":"crid:~~2F~~2Fport.cs~~2F1379541","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fport.cs~~2F1379541","shortDescription":"Ambiciózní vysokoškolačka Valentina (Sophie Marceau) studuje literaturu na pařížské Sorbonně a právě se připravuje k závěrečným zkouškám. Žádný odpočinek, žádné volno, žádné večírky, téměř žádný...","mediaType":"FeatureFilm","year":"1988","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"rootId":"crid:~~2F~~2Fport.cs~~2F1379541","replayTvAvailable":true,"audioTracks":[],"ratings":[],"offersLatestExpirationDate":1675767600000,"replayTvStartOffset":0,"replayTvEndOffset":604800,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false}' - ) - }) - } else if ( - url === - 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"id":"crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7","startTime":1675785900000,"endTime":1675791900000,"actualStartTime":1675785900000,"actualEndTime":1675791900000,"expirationDate":1676390700000,"stationId":"lgi-obolite-sk-prod-master:10024","imi":"imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7","scCridImi":"crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7","mediaGroupId":"crid:~~2F~~2Fport.cs~~2F71927954","program":{"id":"crid:~~2F~~2Fport.cs~~2F71927954","title":"Zilionáři","description":"David (Zach Galifianakis) je nekomplikovaný muž, který uvízl v monotónním životě. Den co den usedá za volant svého obrněného automobilu, aby odvážel obrovské sumy peněz jiných lidí. Jediným...","longDescription":"David (Zach Galifianakis) je nekomplikovaný muž, který uvízl v monotónním životě. Den co den usedá za volant svého obrněného automobilu, aby odvážel obrovské sumy peněz jiných lidí. Jediným vzrušujícím momentem v jeho životě je flirtování s kolegyní Kelly (Kristen Wiig), která ho však brzy zatáhne do těžko uvěřitelného dobrodružství. Skupinka nepříliš inteligentních loserů, pod vedením Steva (Owen Wilson), plánuje vyloupit banku a David jim v tom má samozřejmě pomoci. Navzdory absolutně amatérskému plánu se ale stane nemožné a oni mají najednou v kapse 17 miliónů dolarů. A protože tato partička je opravdu bláznivá, začne je hned ve velkém roztáčet. Peníze létají vzduchem za luxusní a kolikrát i zbytečné věci, ale nedochází jim, že pro policii tak zanechávají jasné stopy...","medium":"Movie","categories":[{"id":"lgi-obolite-sk-prod-master:genre-9","title":"Drama","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-obolite-sk-prod-master:genre-33","title":"Akcia","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"parentalRating":"15","cast":["Zach Galifianakis","Kristen Wiigová","Owen Wilson","Kate McKinnon","Leslie Jones","Jason Sudeikis","Ross Kimball","Devin Ratray","Mary Elizabeth Ellisová","Jon Daly","Ken Marino","Daniel Zacapa","Tom Werme","Njema Williams","Nils Cruz","Michael Fraguada","Christian Gonzalez","Candace Blanchard","Karsten Friske","Dallas Edwards","Barry Ratcliffe","Shelton Grant","Laura Palka","Reegus Flenory","Wynn Reichert","Jill Jane Clements","Joseph S. Wilson","Jee An","Rhoda Griffisová","Nicole Dupre Sobchack"],"directors":["Scott August","Richard L. Fox","Michelle Malley-Campos","Sebastian Mazzola","Steven Ritzi","Pete Waterman","Jared Hess"],"images":[{"assetType":"HighResLandscape","assetTypes":["HighResLandscape"],"url":"http://62.179.125.152/SK/Images/hrl_fd098116bac1429318aaf5fdae498ce76e258782.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"http://62.179.125.152/SK/Images/hrp_6f857ae9375b3bcceb6353a5b35775f52cd85302.jpg"},{"assetType":"boxCover","assetTypes":["boxCover"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg"},{"assetType":"boxart-small","assetTypes":["boxart-small"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg?w=75&h=108&mode=box"},{"assetType":"boxart-medium","assetTypes":["boxart-medium"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg?w=110&h=159&mode=box"},{"assetType":"boxart-xlarge","assetTypes":["boxart-xlarge"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg?w=210&h=303&mode=box"},{"assetType":"boxart-large","assetTypes":["boxart-large"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg?w=180&h=260&mode=box"}],"rootId":"crid:~~2F~~2Fport.cs~~2F71927954","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fport.cs~~2F71927954","shortDescription":"David (Zach Galifianakis) je nekomplikovaný muž, který uvízl v monotónním životě. Den co den usedá za volant svého obrněného automobilu, aby odvážel obrovské sumy peněz jiných lidí. Jediným...","mediaType":"FeatureFilm","year":"2016","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"rootId":"crid:~~2F~~2Fport.cs~~2F71927954","replayTvAvailable":true,"audioTracks":[],"ratings":[],"offersLatestExpirationDate":1676187900000,"replayTvStartOffset":0,"replayTvEndOffset":604800,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false}' - ) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - parser({ content, channel, date }) - .then(result => { - result = result.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2023-02-06T21:35:00.000Z', - stop: '2023-02-06T23:05:00.000Z', - title: 'Avengement', - description: - 'Během propustky z vězení za účelem návštěvy umírající matky v nemocnici zločinec Cain Burgess (Scott Adkins) unikne svým dozorcům a mizí v ulicích Londýna. Jde o epickou cestu krve a bolesti za dosažením vytoužené pomsty na těch, kteří z něj udělali chladnokrevného vraha.', - category: ['Drama', 'Akcia'], - directors: ['Jesse V. Johnson'], - actors: [ - 'Scott Adkins', - 'Craig Fairbrass', - 'Thomas Turgoose', - 'Nick Moran', - 'Kierston Wareing', - 'Leo Gregory', - 'Mark Strange', - 'Luke LaFontaine', - 'Beau Fowler', - 'Dan Styles', - 'Christopher Sciueref', - 'Matt Routledge', - 'Jane Thorne', - 'Louis Mandylor', - 'Terence Maynard', - 'Greg Burridge', - 'Michael Higgs', - 'Damian Gallagher', - 'Daniel Adegboyega', - 'John Ioannou', - 'Sofie Golding-Spittle', - 'Joe Egan', - 'Darren Swain', - 'Lee Charles', - 'Dominic Kinnaird', - "Ross O'Hennessy", - 'Teresa Mahoney', - 'Andrew Dunkelberger', - 'Sam Hardy', - 'Ivan Moy', - 'Mark Sears', - 'Phillip Ray Tommy' - ], - date: '2019' - }, - { - start: '2023-02-07T04:35:00.000Z', - stop: '2023-02-07T05:00:00.000Z', - title: 'Zoom In', - description: 'Film/Kino', - category: ['Hudba a umenie', 'Film'], - date: '2010' - }, - { - start: '2023-02-07T09:10:00.000Z', - stop: '2023-02-07T11:00:00.000Z', - title: 'Studentka', - description: - 'Ambiciózní vysokoškolačka Valentina (Sophie Marceau) studuje literaturu na pařížské Sorbonně a právě se připravuje k závěrečným zkouškám. Žádný odpočinek, žádné volno, žádné večírky, téměř žádný spánek a především a hlavně ... žádná láska! Věří, že jedině tak obstojí před zkušební komisí. Jednoho dne se však odehraje něco, s čím nepočítala. Potká charismatického hudebníka Neda - a bláznivě se zamiluje. V tuto chvíli stojí před osudovým rozhodnutím: zahodí roky obrovského studijního nasazení, nebo odmítne lásku? Nebo se snad dá obojí skloubit dohromady?', - category: ['Film', 'Komédia'], - actors: [ - 'Sophie Marceauová', - 'Vincent Lindon', - 'Elisabeth Vitali', - 'Elena Pompei', - 'Jean-Claude Leguay', - 'Brigitte Chamarande', - 'Christian Pereira', - 'Gérard Dacier', - 'Roberto Attias', - 'Beppe Chierici', - 'Nathalie Mann', - 'Anne Macina', - 'Janine Souchon', - 'Virginie Demians', - 'Hugues Leforestier', - 'Jacqueline Noëlle', - 'Marc-André Brunet', - 'Isabelle Caubère', - 'André Chazel', - 'Med Salah Cheurfi', - 'Guillaume Corea', - 'Eric Denize', - 'Gilles Gaston-Dreyfuss', - 'Benoît Gourley', - 'Marc Innocenti', - 'Najim Laouriga', - 'Laurent Ledermann', - 'Philippe Maygal', - 'Dominique Pifarely', - 'Ysé Tran' - ], - directors: ['Francis De Gueltz', 'Dominique Talmon', 'Claude Pinoteau'], - date: '1988' - }, - { - start: '2023-02-07T16:05:00.000Z', - stop: '2023-02-07T17:45:00.000Z', - title: 'Zilionáři', - description: - 'David (Zach Galifianakis) je nekomplikovaný muž, který uvízl v monotónním životě. Den co den usedá za volant svého obrněného automobilu, aby odvážel obrovské sumy peněz jiných lidí. Jediným vzrušujícím momentem v jeho životě je flirtování s kolegyní Kelly (Kristen Wiig), která ho však brzy zatáhne do těžko uvěřitelného dobrodružství. Skupinka nepříliš inteligentních loserů, pod vedením Steva (Owen Wilson), plánuje vyloupit banku a David jim v tom má samozřejmě pomoci. Navzdory absolutně amatérskému plánu se ale stane nemožné a oni mají najednou v kapse 17 miliónů dolarů. A protože tato partička je opravdu bláznivá, začne je hned ve velkém roztáčet. Peníze létají vzduchem za luxusní a kolikrát i zbytečné věci, ale nedochází jim, že pro policii tak zanechávají jasné stopy...', - category: ['Drama', 'Akcia'], - actors: [ - 'Zach Galifianakis', - 'Kristen Wiigová', - 'Owen Wilson', - 'Kate McKinnon', - 'Leslie Jones', - 'Jason Sudeikis', - 'Ross Kimball', - 'Devin Ratray', - 'Mary Elizabeth Ellisová', - 'Jon Daly', - 'Ken Marino', - 'Daniel Zacapa', - 'Tom Werme', - 'Njema Williams', - 'Nils Cruz', - 'Michael Fraguada', - 'Christian Gonzalez', - 'Candace Blanchard', - 'Karsten Friske', - 'Dallas Edwards', - 'Barry Ratcliffe', - 'Shelton Grant', - 'Laura Palka', - 'Reegus Flenory', - 'Wynn Reichert', - 'Jill Jane Clements', - 'Joseph S. Wilson', - 'Jee An', - 'Rhoda Griffisová', - 'Nicole Dupre Sobchack' - ], - directors: [ - 'Scott August', - 'Richard L. Fox', - 'Michelle Malley-Campos', - 'Sebastian Mazzola', - 'Steven Ritzi', - 'Pete Waterman', - 'Jared Hess' - ], - date: '2016' - } - ]) - done() - }) - .catch(done) -}) - -it('can handle empty guide', done => { - parser({ - content: '[{"type":"PATH_PARAM","code":"period","reason":"INVALID"}]', - channel, - date - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(done) -}) +const { parser, url } = require('./horizon.tv.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('2023-02-07', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '10024', + xmltv_id: 'AMCCzechRepublic.cz' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/1' + ) +}) + +it('can parse response', done => { + const content = JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')) + + axios.get.mockImplementation(url => { + if ( + url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/2' + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_1.json'), 'utf8')) + }) + } else if ( + url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/3' + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_2.json'), 'utf8')) + }) + } else if ( + url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/4' + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_3.json'), 'utf8')) + }) + } else if ( + url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78' + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_listings_1.json'), 'utf8')) + }) + } else if ( + url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b' + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_listings_2.json'), 'utf8')) + }) + } else if ( + url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad' + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_listings_3.json'), 'utf8')) + }) + } else if ( + url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7' + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_listings_4.json'), 'utf8')) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + parser({ content, channel, date }) + .then(result => { + result = result.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2023-02-06T21:35:00.000Z', + stop: '2023-02-06T23:05:00.000Z', + title: 'Avengement', + description: + 'Během propustky z vězení za účelem návštěvy umírající matky v nemocnici zločinec Cain Burgess (Scott Adkins) unikne svým dozorcům a mizí v ulicích Londýna. Jde o epickou cestu krve a bolesti za dosažením vytoužené pomsty na těch, kteří z něj udělali chladnokrevného vraha.', + category: ['Drama', 'Akcia'], + directors: ['Jesse V. Johnson'], + actors: [ + 'Scott Adkins', + 'Craig Fairbrass', + 'Thomas Turgoose', + 'Nick Moran', + 'Kierston Wareing', + 'Leo Gregory', + 'Mark Strange', + 'Luke LaFontaine', + 'Beau Fowler', + 'Dan Styles', + 'Christopher Sciueref', + 'Matt Routledge', + 'Jane Thorne', + 'Louis Mandylor', + 'Terence Maynard', + 'Greg Burridge', + 'Michael Higgs', + 'Damian Gallagher', + 'Daniel Adegboyega', + 'John Ioannou', + 'Sofie Golding-Spittle', + 'Joe Egan', + 'Darren Swain', + 'Lee Charles', + 'Dominic Kinnaird', + "Ross O'Hennessy", + 'Teresa Mahoney', + 'Andrew Dunkelberger', + 'Sam Hardy', + 'Ivan Moy', + 'Mark Sears', + 'Phillip Ray Tommy' + ], + date: '2019' + }, + { + start: '2023-02-07T04:35:00.000Z', + stop: '2023-02-07T05:00:00.000Z', + title: 'Zoom In', + description: 'Film/Kino', + category: ['Hudba a umenie', 'Film'], + date: '2010' + }, + { + start: '2023-02-07T09:10:00.000Z', + stop: '2023-02-07T11:00:00.000Z', + title: 'Studentka', + description: + 'Ambiciózní vysokoškolačka Valentina (Sophie Marceau) studuje literaturu na pařížské Sorbonně a právě se připravuje k závěrečným zkouškám. Žádný odpočinek, žádné volno, žádné večírky, téměř žádný spánek a především a hlavně ... žádná láska! Věří, že jedině tak obstojí před zkušební komisí. Jednoho dne se však odehraje něco, s čím nepočítala. Potká charismatického hudebníka Neda - a bláznivě se zamiluje. V tuto chvíli stojí před osudovým rozhodnutím: zahodí roky obrovského studijního nasazení, nebo odmítne lásku? Nebo se snad dá obojí skloubit dohromady?', + category: ['Film', 'Komédia'], + actors: [ + 'Sophie Marceauová', + 'Vincent Lindon', + 'Elisabeth Vitali', + 'Elena Pompei', + 'Jean-Claude Leguay', + 'Brigitte Chamarande', + 'Christian Pereira', + 'Gérard Dacier', + 'Roberto Attias', + 'Beppe Chierici', + 'Nathalie Mann', + 'Anne Macina', + 'Janine Souchon', + 'Virginie Demians', + 'Hugues Leforestier', + 'Jacqueline Noëlle', + 'Marc-André Brunet', + 'Isabelle Caubère', + 'André Chazel', + 'Med Salah Cheurfi', + 'Guillaume Corea', + 'Eric Denize', + 'Gilles Gaston-Dreyfuss', + 'Benoît Gourley', + 'Marc Innocenti', + 'Najim Laouriga', + 'Laurent Ledermann', + 'Philippe Maygal', + 'Dominique Pifarely', + 'Ysé Tran' + ], + directors: ['Francis De Gueltz', 'Dominique Talmon', 'Claude Pinoteau'], + date: '1988' + }, + { + start: '2023-02-07T16:05:00.000Z', + stop: '2023-02-07T17:45:00.000Z', + title: 'Zilionáři', + description: + 'David (Zach Galifianakis) je nekomplikovaný muž, který uvízl v monotónním životě. Den co den usedá za volant svého obrněného automobilu, aby odvážel obrovské sumy peněz jiných lidí. Jediným vzrušujícím momentem v jeho životě je flirtování s kolegyní Kelly (Kristen Wiig), která ho však brzy zatáhne do těžko uvěřitelného dobrodružství. Skupinka nepříliš inteligentních loserů, pod vedením Steva (Owen Wilson), plánuje vyloupit banku a David jim v tom má samozřejmě pomoci. Navzdory absolutně amatérskému plánu se ale stane nemožné a oni mají najednou v kapse 17 miliónů dolarů. A protože tato partička je opravdu bláznivá, začne je hned ve velkém roztáčet. Peníze létají vzduchem za luxusní a kolikrát i zbytečné věci, ale nedochází jim, že pro policii tak zanechávají jasné stopy...', + category: ['Drama', 'Akcia'], + actors: [ + 'Zach Galifianakis', + 'Kristen Wiigová', + 'Owen Wilson', + 'Kate McKinnon', + 'Leslie Jones', + 'Jason Sudeikis', + 'Ross Kimball', + 'Devin Ratray', + 'Mary Elizabeth Ellisová', + 'Jon Daly', + 'Ken Marino', + 'Daniel Zacapa', + 'Tom Werme', + 'Njema Williams', + 'Nils Cruz', + 'Michael Fraguada', + 'Christian Gonzalez', + 'Candace Blanchard', + 'Karsten Friske', + 'Dallas Edwards', + 'Barry Ratcliffe', + 'Shelton Grant', + 'Laura Palka', + 'Reegus Flenory', + 'Wynn Reichert', + 'Jill Jane Clements', + 'Joseph S. Wilson', + 'Jee An', + 'Rhoda Griffisová', + 'Nicole Dupre Sobchack' + ], + directors: [ + 'Scott August', + 'Richard L. Fox', + 'Michelle Malley-Campos', + 'Sebastian Mazzola', + 'Steven Ritzi', + 'Pete Waterman', + 'Jared Hess' + ], + date: '2016' + } + ]) + done() + }) + .catch(done) +}) + +it('can handle empty guide', done => { + parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')), + channel, + date + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(done) +}) diff --git a/sites/hoy.tv/__data__/content.xml b/sites/hoy.tv/__data__/content.xml new file mode 100644 index 00000000..696703f9 --- /dev/null +++ b/sites/hoy.tv/__data__/content.xml @@ -0,0 +1,73 @@ + + + + + 2024-09-13 11:30:00 + 2024-09-13 12:30:00 + [PG] + false + false + 2024-09-27 11:30:00 + + 0 + + 0 + + http://tv.fantv.hk/images/thumbnail_1920_1080_fantv.jpg + + + EQ00135 + 46 + 點講都係一家人 + + http://tv.fantv.hk/images/nosuchthumbnail.jpg + + + + 點講都係一家人 + 0 + EQ00135 + 點講都係一家人 Episode 46 + 1 + 20240913 + 1130 + 0001 + 3704000 + + + + 2024-09-13 12:30:00 + 2024-09-13 13:30:00 + + false + false + 2024-09-27 12:30:00 + + 0 + + 0 + + http://tv.fantv.hk/images/thumbnail_1920_1080_fantv.jpg + + + ED00311 + 0 + 麝香之路 + Ep. 2 .The Secret of disappeared kingdom.shows the mysterious disappearance of the ancient Tibetan kingdom which gained world + http://tv.fantv.hk/images/nosuchthumbnail.jpg + + + + 麝香之路 + 0 + ED00311 + 麝香之路 2024-09-13 + 1 + 20240913 + 1230 + 0001 + 3704000 + + + + \ No newline at end of file diff --git a/sites/hoy.tv/hoy.tv.config.js b/sites/hoy.tv/hoy.tv.config.js index 30cc2b82..7ae7ad7e 100644 --- a/sites/hoy.tv/hoy.tv.config.js +++ b/sites/hoy.tv/hoy.tv.config.js @@ -1,63 +1,63 @@ -const axios = require('axios') -const convert = require('xml-js') -const dayjs = require('dayjs') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(timezone) - -module.exports = { - site: 'hoy.tv', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1h - } - }, - url: function ({ channel, date }) { - return `https://epg-file.hoy.tv/hoy/OTT${channel.site_id}${date.format('YYYYMMDD')}.xml` - }, - parser({ content, date }) { - const data = convert.xml2js(content, { - compact: true, - ignoreDeclaration: true, - ignoreAttributes: true - }) - - const programs = [] - - for (let item of data.ProgramGuide.Channel.EpgItem) { - const start = dayjs.tz(item.EpgStartDateTime._text, 'YYYY-MM-DD HH:mm:ss', 'Asia/Hong_Kong') - - if (!date.isSame(start, 'day')) { - continue - } - - const epIndex = item.EpisodeInfo.EpisodeIndex._text - const subtitle = parseInt(epIndex) > 0 ? `第${epIndex}集` : undefined - - programs.push({ - title: `${item.ComScore.ns_st_pr._text}${item.EpgOtherInfo?._text || ''}`, - sub_title: subtitle, - description: item.EpisodeInfo.EpisodeLongDescription._text, - start, - stop: dayjs.tz(item.EpgEndDateTime._text, 'YYYY-MM-DD HH:mm:ss', 'Asia/Hong_Kong') - }) - } - - return programs - }, - async channels() { - const data = await axios - .get('https://api2.hoy.tv/api/v2/a/channel') - .then(r => r.data) - .catch(console.error) - - return data.data.map(c => { - return { - site_id: c.videos.id, - name: c.name.zh_hk, - lang: 'zh' - } - }) - } -} +const axios = require('axios') +const convert = require('xml-js') +const dayjs = require('dayjs') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(timezone) + +module.exports = { + site: 'hoy.tv', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1h + } + }, + url: function ({ channel, date }) { + return `https://epg-file.hoy.tv/hoy/OTT${channel.site_id}${date.format('YYYYMMDD')}.xml` + }, + parser({ content, date }) { + const data = convert.xml2js(content, { + compact: true, + ignoreDeclaration: true, + ignoreAttributes: true + }) + + const programs = [] + + for (let item of data.ProgramGuide.Channel.EpgItem) { + const start = dayjs.tz(item.EpgStartDateTime._text, 'YYYY-MM-DD HH:mm:ss', 'Asia/Hong_Kong') + + if (!date.isSame(start, 'day')) { + continue + } + + const epIndex = item.EpisodeInfo.EpisodeIndex._text + const subtitle = parseInt(epIndex) > 0 ? `第${epIndex}集` : undefined + + programs.push({ + title: `${item.ComScore.ns_st_pr._text}${item.EpgOtherInfo?._text || ''}`, + sub_title: subtitle, + description: item.EpisodeInfo.EpisodeLongDescription._text, + start, + stop: dayjs.tz(item.EpgEndDateTime._text, 'YYYY-MM-DD HH:mm:ss', 'Asia/Hong_Kong') + }) + } + + return programs + }, + async channels() { + const data = await axios + .get('https://api2.hoy.tv/api/v2/a/channel') + .then(r => r.data) + .catch(console.error) + + return data.data.map(c => { + return { + site_id: c.videos.id, + name: c.name.zh_hk, + lang: 'zh' + } + }) + } +} diff --git a/sites/hoy.tv/hoy.tv.test.js b/sites/hoy.tv/hoy.tv.test.js index 155c2963..ea0b2569 100644 --- a/sites/hoy.tv/hoy.tv.test.js +++ b/sites/hoy.tv/hoy.tv.test.js @@ -1,115 +1,46 @@ -const { parser, url } = require('./hoy.tv.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2024-09-13', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '76', - xmltv_id: 'HOYIBC.hk', - lang: 'zh' -} -const content = ` - - - - 2024-09-13 11:30:00 - 2024-09-13 12:30:00 - [PG] - false - false - 2024-09-27 11:30:00 - - 0 - - 0 - - http://tv.fantv.hk/images/thumbnail_1920_1080_fantv.jpg - - - EQ00135 - 46 - 點講都係一家人 - - http://tv.fantv.hk/images/nosuchthumbnail.jpg - - - - 點講都係一家人 - 0 - EQ00135 - 點講都係一家人 Episode 46 - 1 - 20240913 - 1130 - 0001 - 3704000 - - - - 2024-09-13 12:30:00 - 2024-09-13 13:30:00 - - false - false - 2024-09-27 12:30:00 - - 0 - - 0 - - http://tv.fantv.hk/images/thumbnail_1920_1080_fantv.jpg - - - ED00311 - 0 - 麝香之路 - Ep. 2 .The Secret of disappeared kingdom.shows the mysterious disappearance of the ancient Tibetan kingdom which gained world - http://tv.fantv.hk/images/nosuchthumbnail.jpg - - - - 麝香之路 - 0 - ED00311 - 麝香之路 2024-09-13 - 1 - 20240913 - 1230 - 0001 - 3704000 - - - -` - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://epg-file.hoy.tv/hoy/OTT7620240913.xml') -}) - -it('can parse response', () => { - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2024-09-13T03:30:00.000Z', - stop: '2024-09-13T04:30:00.000Z', - title: '點講都係一家人[PG]', - sub_title: '第46集' - }, - { - start: '2024-09-13T04:30:00.000Z', - stop: '2024-09-13T05:30:00.000Z', - title: '麝香之路', - description: - 'Ep. 2 .The Secret of disappeared kingdom.shows the mysterious disappearance of the ancient Tibetan kingdom which gained world' - } - ]) -}) +const { parser, url } = require('./hoy.tv.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2024-09-13', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '76', + xmltv_id: 'HOYIBC.hk', + lang: 'zh' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://epg-file.hoy.tv/hoy/OTT7620240913.xml') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml'), 'utf8') + + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2024-09-13T03:30:00.000Z', + stop: '2024-09-13T04:30:00.000Z', + title: '點講都係一家人[PG]', + sub_title: '第46集' + }, + { + start: '2024-09-13T04:30:00.000Z', + stop: '2024-09-13T05:30:00.000Z', + title: '麝香之路', + description: + 'Ep. 2 .The Secret of disappeared kingdom.shows the mysterious disappearance of the ancient Tibetan kingdom which gained world' + } + ]) +}) diff --git a/sites/i.mjh.nz/i.mjh.nz.config.js b/sites/i.mjh.nz/i.mjh.nz.config.js index d5c82f08..d2ec4620 100644 --- a/sites/i.mjh.nz/i.mjh.nz.config.js +++ b/sites/i.mjh.nz/i.mjh.nz.config.js @@ -1,188 +1,188 @@ -const dayjs = require('dayjs') -const axios = require('axios') -const parser = require('epg-parser') -const isBetween = require('dayjs/plugin/isBetween') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(isBetween) -dayjs.extend(customParseFormat) - -const API_ENDPOINT = 'https://raw.githubusercontent.com/matthuisman/i.mjh.nz/master' - -module.exports = { - site: 'i.mjh.nz', - days: 2, - request: { - cache: { - ttl: 3 * 60 * 60 * 1000 // 3h - }, - maxContentLength: 100 * 1024 * 1024 // 100Mb - }, - url({ channel }) { - const [path] = channel.site_id.split('#') - - return `${API_ENDPOINT}/${path}.xml` - }, - parser({ content, channel, date }) { - const items = parseItems(content, channel, date) - - const programs = items.map(item => { - return { - ...item, - title: getTitle(item), - description: getDescription(item), - categories: getCategories(item), - icon: getIcon(item) - } - }) - - return mergeMovieParts(programs) - }, - async channels({ provider }) { - const providers = { - pluto: [ - { path: 'PlutoTV/br', lang: 'pt' }, - { path: 'PlutoTV/ca', lang: 'en' }, - { path: 'PlutoTV/cl', lang: 'es' }, - { path: 'PlutoTV/de', lang: 'de' }, - { path: 'PlutoTV/dk', lang: 'da' }, - { path: 'PlutoTV/es', lang: 'es' }, - { path: 'PlutoTV/fr', lang: 'fr' }, - { path: 'PlutoTV/gb', lang: 'en' }, - { path: 'PlutoTV/it', lang: 'it' }, - { path: 'PlutoTV/mx', lang: 'es' }, - { path: 'PlutoTV/no', lang: 'no' }, - { path: 'PlutoTV/se', lang: 'sv' }, - { path: 'PlutoTV/us', lang: 'en' } - ], - plex: [ - { path: 'Plex/au', lang: 'en' }, - { path: 'Plex/ca', lang: 'en' }, - { path: 'Plex/es', lang: 'es' }, - { path: 'Plex/mx', lang: 'es' }, - { path: 'Plex/nz', lang: 'en' }, - { path: 'Plex/us', lang: 'en' } - ], - samsung: [ - { path: 'SamsungTVPlus/at', lang: 'de' }, - { path: 'SamsungTVPlus/ca', lang: 'en' }, - { path: 'SamsungTVPlus/ch', lang: 'de' }, - { path: 'SamsungTVPlus/de', lang: 'de' }, - { path: 'SamsungTVPlus/es', lang: 'es' }, - { path: 'SamsungTVPlus/fr', lang: 'fr' }, - { path: 'SamsungTVPlus/gb', lang: 'en' }, - { path: 'SamsungTVPlus/in', lang: 'en' }, - { path: 'SamsungTVPlus/it', lang: 'it' }, - { path: 'SamsungTVPlus/kr', lang: 'ko' }, - { path: 'SamsungTVPlus/us', lang: 'en' } - ], - skygo: [{ path: 'SkyGo/epg', lang: 'en' }], - stirr: [{ path: 'Stirr/all', lang: 'en' }], - foxtel: [{ path: 'Foxtel/epg', lang: 'en' }], - binge: [{ path: 'Binge/epg', lang: 'en' }], - dstv: [{ path: 'DStv/za', lang: 'en' }], - flash: [{ path: 'Flash/epg', lang: 'en' }], - kayo: [{ path: 'Kayo/epg', lang: 'en' }], - metv: [{ path: 'MeTV/epg', lang: 'en' }], - optus: [{ path: 'Optus/epg', lang: 'en' }], - pbs: [{ path: 'PBS/all', lang: 'en' }], - roku: [{ path: 'Roku/epg', lang: 'en' }], - singtel: [{ path: 'Singtel/epg', lang: 'en' }], - skysportnow: [{ path: 'SkySportNow/epg', lang: 'en' }], - au: [ - { path: 'au/Adelaide/epg', lang: 'en' }, - { path: 'au/Brisbane/epg', lang: 'en' }, - { path: 'au/Canberra/epg', lang: 'en' }, - { path: 'au/Darwin/epg', lang: 'en' }, - { path: 'au/Hobart/epg', lang: 'en' }, - { path: 'au/Melbourne/epg', lang: 'en' }, - { path: 'au/Perth/epg', lang: 'en' }, - { path: 'au/Sydney/epg', lang: 'en' } - ], - hgtvgo: [{ path: 'hgtv_go/epg', lang: 'en' }], - nz: [{ path: 'nz/epg', lang: 'en' }] - } - - const channels = [] - - const providerOptions = providers[provider] - for (const option of providerOptions) { - const xml = await axios - .get(`${API_ENDPOINT}/${option.path}.xml`) - .then(r => r.data) - .catch(console.error) - const data = parser.parse(xml) - - data.channels.forEach(item => { - channels.push({ - lang: option.lang, - site_id: `${option.path}#${item.id}`, - name: item.name[0].value - }) - }) - } - - return channels - } -} - -function mergeMovieParts(programs) { - const output = [] - - programs.forEach(prog => { - let prev = output[output.length - 1] - let found = - prev && - prog.categories.includes('Movie') && - prev.title === prog.title && - prev.description === prog.description - - if (found) { - prev.stop = prog.stop - } else { - output.push(prog) - } - }) - - return output -} - -function getTitle(item) { - return item.title.length ? item.title[0].value : null -} - -function getDescription(item) { - return item.desc.length ? item.desc[0].value : null -} - -function getCategories(item) { - return item.category.map(c => c.value) -} - -function getIcon(item) { - return item.icon && item.icon.length ? item.icon[0].src : null -} - -function parseItems(content, channel, date) { - try { - const curr_day = date - const next_day = date.add(1, 'd') - const [, site_id] = channel.site_id.split('#') - const data = parser.parse(content) - if (!data || !Array.isArray(data.programs)) return [] - - return data.programs - .filter( - p => - p.channel === site_id && dayjs(p.start, 'YYYYMMDDHHmmss ZZ').isBetween(curr_day, next_day) - ) - .map(p => { - if (Array.isArray(p.date) && p.date.length) { - p.date = p.date[0] - } - return p - }) - } catch { - return [] - } -} +const dayjs = require('dayjs') +const axios = require('axios') +const parser = require('epg-parser') +const isBetween = require('dayjs/plugin/isBetween') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(isBetween) +dayjs.extend(customParseFormat) + +const API_ENDPOINT = 'https://raw.githubusercontent.com/matthuisman/i.mjh.nz/master' + +module.exports = { + site: 'i.mjh.nz', + days: 2, + request: { + cache: { + ttl: 3 * 60 * 60 * 1000 // 3h + }, + maxContentLength: 100 * 1024 * 1024 // 100Mb + }, + url({ channel }) { + const [path] = channel.site_id.split('#') + + return `${API_ENDPOINT}/${path}.xml` + }, + parser({ content, channel, date }) { + const items = parseItems(content, channel, date) + + const programs = items.map(item => { + return { + ...item, + title: getTitle(item), + description: getDescription(item), + categories: getCategories(item), + icon: getIcon(item) + } + }) + + return mergeMovieParts(programs) + }, + async channels({ provider }) { + const providers = { + pluto: [ + { path: 'PlutoTV/br', lang: 'pt' }, + { path: 'PlutoTV/ca', lang: 'en' }, + { path: 'PlutoTV/cl', lang: 'es' }, + { path: 'PlutoTV/de', lang: 'de' }, + { path: 'PlutoTV/dk', lang: 'da' }, + { path: 'PlutoTV/es', lang: 'es' }, + { path: 'PlutoTV/fr', lang: 'fr' }, + { path: 'PlutoTV/gb', lang: 'en' }, + { path: 'PlutoTV/it', lang: 'it' }, + { path: 'PlutoTV/mx', lang: 'es' }, + { path: 'PlutoTV/no', lang: 'no' }, + { path: 'PlutoTV/se', lang: 'sv' }, + { path: 'PlutoTV/us', lang: 'en' } + ], + plex: [ + { path: 'Plex/au', lang: 'en' }, + { path: 'Plex/ca', lang: 'en' }, + { path: 'Plex/es', lang: 'es' }, + { path: 'Plex/mx', lang: 'es' }, + { path: 'Plex/nz', lang: 'en' }, + { path: 'Plex/us', lang: 'en' } + ], + samsung: [ + { path: 'SamsungTVPlus/at', lang: 'de' }, + { path: 'SamsungTVPlus/ca', lang: 'en' }, + { path: 'SamsungTVPlus/ch', lang: 'de' }, + { path: 'SamsungTVPlus/de', lang: 'de' }, + { path: 'SamsungTVPlus/es', lang: 'es' }, + { path: 'SamsungTVPlus/fr', lang: 'fr' }, + { path: 'SamsungTVPlus/gb', lang: 'en' }, + { path: 'SamsungTVPlus/in', lang: 'en' }, + { path: 'SamsungTVPlus/it', lang: 'it' }, + { path: 'SamsungTVPlus/kr', lang: 'ko' }, + { path: 'SamsungTVPlus/us', lang: 'en' } + ], + skygo: [{ path: 'SkyGo/epg', lang: 'en' }], + stirr: [{ path: 'Stirr/all', lang: 'en' }], + foxtel: [{ path: 'Foxtel/epg', lang: 'en' }], + binge: [{ path: 'Binge/epg', lang: 'en' }], + dstv: [{ path: 'DStv/za', lang: 'en' }], + flash: [{ path: 'Flash/epg', lang: 'en' }], + kayo: [{ path: 'Kayo/epg', lang: 'en' }], + metv: [{ path: 'MeTV/epg', lang: 'en' }], + optus: [{ path: 'Optus/epg', lang: 'en' }], + pbs: [{ path: 'PBS/all', lang: 'en' }], + roku: [{ path: 'Roku/epg', lang: 'en' }], + singtel: [{ path: 'Singtel/epg', lang: 'en' }], + skysportnow: [{ path: 'SkySportNow/epg', lang: 'en' }], + au: [ + { path: 'au/Adelaide/epg', lang: 'en' }, + { path: 'au/Brisbane/epg', lang: 'en' }, + { path: 'au/Canberra/epg', lang: 'en' }, + { path: 'au/Darwin/epg', lang: 'en' }, + { path: 'au/Hobart/epg', lang: 'en' }, + { path: 'au/Melbourne/epg', lang: 'en' }, + { path: 'au/Perth/epg', lang: 'en' }, + { path: 'au/Sydney/epg', lang: 'en' } + ], + hgtvgo: [{ path: 'hgtv_go/epg', lang: 'en' }], + nz: [{ path: 'nz/epg', lang: 'en' }] + } + + const channels = [] + + const providerOptions = providers[provider] + for (const option of providerOptions) { + const xml = await axios + .get(`${API_ENDPOINT}/${option.path}.xml`) + .then(r => r.data) + .catch(console.error) + const data = parser.parse(xml) + + data.channels.forEach(item => { + channels.push({ + lang: option.lang, + site_id: `${option.path}#${item.id}`, + name: item.name[0].value + }) + }) + } + + return channels + } +} + +function mergeMovieParts(programs) { + const output = [] + + programs.forEach(prog => { + let prev = output[output.length - 1] + let found = + prev && + prog.categories.includes('Movie') && + prev.title === prog.title && + prev.description === prog.description + + if (found) { + prev.stop = prog.stop + } else { + output.push(prog) + } + }) + + return output +} + +function getTitle(item) { + return item.title.length ? item.title[0].value : null +} + +function getDescription(item) { + return item.desc.length ? item.desc[0].value : null +} + +function getCategories(item) { + return item.category.map(c => c.value) +} + +function getIcon(item) { + return item.icon && item.icon.length ? item.icon[0].src : null +} + +function parseItems(content, channel, date) { + try { + const curr_day = date + const next_day = date.add(1, 'd') + const [, site_id] = channel.site_id.split('#') + const data = parser.parse(content) + if (!data || !Array.isArray(data.programs)) return [] + + return data.programs + .filter( + p => + p.channel === site_id && dayjs(p.start, 'YYYYMMDDHHmmss ZZ').isBetween(curr_day, next_day) + ) + .map(p => { + if (Array.isArray(p.date) && p.date.length) { + p.date = p.date[0] + } + return p + }) + } catch { + return [] + } +} diff --git a/sites/i.mjh.nz/i.mjh.nz.test.js b/sites/i.mjh.nz/i.mjh.nz.test.js index d3d3f879..27fd8f4f 100644 --- a/sites/i.mjh.nz/i.mjh.nz.test.js +++ b/sites/i.mjh.nz/i.mjh.nz.test.js @@ -1,47 +1,47 @@ -const { parser, url } = require('./i.mjh.nz.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('2023-06-23', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'Plex/all#5e20b730f2f8d5003d739db7-5eea605674085f0040ddc7a6', - xmltv_id: 'DarkMatterTV.us', - lang: 'en' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe( - 'https://raw.githubusercontent.com/matthuisman/i.mjh.nz/master/Plex/all.xml' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml')) - const results = parser({ content, channel, date }) - - expect(results.length).toBe(11) - expect(results[0]).toMatchObject({ - start: '2023-06-23T07:14:32.000Z', - stop: '2023-06-23T09:09:36.000Z', - title: 'Killers Within', - date: '20180101', - description: - 'With her son being held captive by a criminal gang, police officer Amanda Doyle, together with her ex-husband and three unlikely allies, takes part in a desperate plot to hold a wealthy banker and his family to ransom. But this is no ordinary family.', - icon: 'https://provider-static.plex.tv/epg/images/thumbnails/darkmatter-tv-fallback.jpg', - categories: ['Movie'] - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '404: Not Found', - channel, - date - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./i.mjh.nz.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('2023-06-23', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'Plex/all#5e20b730f2f8d5003d739db7-5eea605674085f0040ddc7a6', + xmltv_id: 'DarkMatterTV.us', + lang: 'en' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe( + 'https://raw.githubusercontent.com/matthuisman/i.mjh.nz/master/Plex/all.xml' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml')) + const results = parser({ content, channel, date }) + + expect(results.length).toBe(11) + expect(results[0]).toMatchObject({ + start: '2023-06-23T07:14:32.000Z', + stop: '2023-06-23T09:09:36.000Z', + title: 'Killers Within', + date: '20180101', + description: + 'With her son being held captive by a criminal gang, police officer Amanda Doyle, together with her ex-husband and three unlikely allies, takes part in a desperate plot to hold a wealthy banker and his family to ransom. But this is no ordinary family.', + icon: 'https://provider-static.plex.tv/epg/images/thumbnails/darkmatter-tv-fallback.jpg', + categories: ['Movie'] + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '404: Not Found', + channel, + date + }) + expect(result).toMatchObject([]) +}) 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/i24news.tv/__data__/content.json b/sites/i24news.tv/__data__/content.json new file mode 100644 index 00000000..64b48624 --- /dev/null +++ b/sites/i24news.tv/__data__/content.json @@ -0,0 +1 @@ +[{"id":348995,"startHour":"22:30","endHour":"23:00","day":5,"firstDiffusion":false,"override":false,"show":{"parsedBody":[{"type":"text","text":"Special Edition"}],"id":131,"title":"تغطية خاصة","body":"Special Edition","slug":"Special-Edition-تغطية-خاصة","visible":true,"image":{"id":1142467,"credit":"","legend":"","href":"https://cdn.i24news.tv/uploads/a1/be/85/20/69/6f/32/1c/ed/b0/f8/5c/f6/1c/40/f9/a1be8520696f321cedb0f85cf61c40f9.png"}}},{"id":349023,"startHour":"15:00","endHour":"15:28","day":6,"firstDiffusion":false,"override":false,"show":{"parsedBody":[{"type":"text","text":"Special Edition"}],"id":131,"title":"تغطية خاصة","body":"Special Edition","slug":"Special-Edition-تغطية-خاصة","visible":true,"image":{"id":1142467,"credit":"","legend":"","href":"https://cdn.i24news.tv/uploads/a1/be/85/20/69/6f/32/1c/ed/b0/f8/5c/f6/1c/40/f9/a1be8520696f321cedb0f85cf61c40f9.png"}}}] \ No newline at end of file diff --git a/sites/i24news.tv/i24news.tv.config.js b/sites/i24news.tv/i24news.tv.config.js index b8539910..a0f9dc09 100644 --- a/sites/i24news.tv/i24news.tv.config.js +++ b/sites/i24news.tv/i24news.tv.config.js @@ -1,65 +1,65 @@ -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: 'i24news.tv', - days: 2, - url: function ({ channel }) { - return `https://api.i24news.tv/v2/${channel.site_id}/schedules` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content, date) - items.forEach(item => { - if (!item.show) return - programs.push({ - title: item.show.title, - description: item.show.body, - image: parseImage(item), - start: parseStart(item, date), - stop: parseStop(item, date) - }) - }) - - return programs - } -} - -function parseImage(item) { - return item.show.image ? item.show.image.href : null -} - -function parseStart(item, date) { - if (!item.startHour) return null - - return dayjs.tz( - `${date.format('YYYY-MM-DD')} ${item.startHour}`, - 'YYYY-MM-DD HH:mm', - 'Asia/Jerusalem' - ) -} - -function parseStop(item, date) { - if (!item.endHour) return null - - return dayjs.tz( - `${date.format('YYYY-MM-DD')} ${item.endHour}`, - 'YYYY-MM-DD HH:mm', - 'Asia/Jerusalem' - ) -} - -function parseItems(content, date) { - const data = JSON.parse(content) - if (!Array.isArray(data)) return [] - let day = date.day() - 1 - day = day < 0 ? 6 : day - - return data.filter(item => item.day === day) -} +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: 'i24news.tv', + days: 2, + url: function ({ channel }) { + return `https://api.i24news.tv/v2/${channel.site_id}/schedules` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content, date) + items.forEach(item => { + if (!item.show) return + programs.push({ + title: item.show.title, + description: item.show.body, + image: parseImage(item), + start: parseStart(item, date), + stop: parseStop(item, date) + }) + }) + + return programs + } +} + +function parseImage(item) { + return item.show.image ? item.show.image.href : null +} + +function parseStart(item, date) { + if (!item.startHour) return null + + return dayjs.tz( + `${date.format('YYYY-MM-DD')} ${item.startHour}`, + 'YYYY-MM-DD HH:mm', + 'Asia/Jerusalem' + ) +} + +function parseStop(item, date) { + if (!item.endHour) return null + + return dayjs.tz( + `${date.format('YYYY-MM-DD')} ${item.endHour}`, + 'YYYY-MM-DD HH:mm', + 'Asia/Jerusalem' + ) +} + +function parseItems(content, date) { + const data = JSON.parse(content) + if (!Array.isArray(data)) return [] + let day = date.day() - 1 + day = day < 0 ? 6 : day + + return data.filter(item => item.day === day) +} diff --git a/sites/i24news.tv/i24news.tv.test.js b/sites/i24news.tv/i24news.tv.test.js index 2525cdd8..2e48ebbd 100644 --- a/sites/i24news.tv/i24news.tv.test.js +++ b/sites/i24news.tv/i24news.tv.test.js @@ -1,45 +1,46 @@ -const { parser, url } = require('./i24news.tv.config.js') -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('2022-03-06', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'ar', - xmltv_id: 'I24NewsArabic.il' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://api.i24news.tv/v2/ar/schedules') -}) - -it('can parse response', () => { - const content = - '[{"id":348995,"startHour":"22:30","endHour":"23:00","day":5,"firstDiffusion":false,"override":false,"show":{"parsedBody":[{"type":"text","text":"Special Edition"}],"id":131,"title":"تغطية خاصة","body":"Special Edition","slug":"Special-Edition-تغطية-خاصة","visible":true,"image":{"id":1142467,"credit":"","legend":"","href":"https://cdn.i24news.tv/uploads/a1/be/85/20/69/6f/32/1c/ed/b0/f8/5c/f6/1c/40/f9/a1be8520696f321cedb0f85cf61c40f9.png"}}},{"id":349023,"startHour":"15:00","endHour":"15:28","day":6,"firstDiffusion":false,"override":false,"show":{"parsedBody":[{"type":"text","text":"Special Edition"}],"id":131,"title":"تغطية خاصة","body":"Special Edition","slug":"Special-Edition-تغطية-خاصة","visible":true,"image":{"id":1142467,"credit":"","legend":"","href":"https://cdn.i24news.tv/uploads/a1/be/85/20/69/6f/32/1c/ed/b0/f8/5c/f6/1c/40/f9/a1be8520696f321cedb0f85cf61c40f9.png"}}}]' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-06T13:00:00.000Z', - stop: '2022-03-06T13:28:00.000Z', - title: 'تغطية خاصة', - description: 'Special Edition', - image: - 'https://cdn.i24news.tv/uploads/a1/be/85/20/69/6f/32/1c/ed/b0/f8/5c/f6/1c/40/f9/a1be8520696f321cedb0f85cf61c40f9.png' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '[]', - date - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./i24news.tv.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('2022-03-06', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'ar', + xmltv_id: 'I24NewsArabic.il' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://api.i24news.tv/v2/ar/schedules') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-06T13:00:00.000Z', + stop: '2022-03-06T13:28:00.000Z', + title: 'تغطية خاصة', + description: 'Special Edition', + image: + 'https://cdn.i24news.tv/uploads/a1/be/85/20/69/6f/32/1c/ed/b0/f8/5c/f6/1c/40/f9/a1be8520696f321cedb0f85cf61c40f9.png' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '[]', + date + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/iltalehti.fi/iltalehti.fi.config.js b/sites/iltalehti.fi/iltalehti.fi.config.js index 4302794b..43ad5f8e 100644 --- a/sites/iltalehti.fi/iltalehti.fi.config.js +++ b/sites/iltalehti.fi/iltalehti.fi.config.js @@ -1,77 +1,77 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'iltalehti.fi', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: function ({ channel, date }) { - const [group] = channel.site_id.split('#') - - return `https://telkku.com/api/channel-groups/default_builtin_channelgroup${group}/offering?startTime=00%3A00%3A00.000&duration=PT24H&inclusionPolicy=IncludeOngoingAlso&limit=1000&tvDate=${date.format( - 'YYYY-MM-DD' - )}&view=PublicationDetails` - }, - parser: function ({ content, channel }) { - let programs = [] - const items = getItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.description, - image: getImage(item), - start: getStart(item), - stop: getStop(item) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://telkku.com/api/channel-groups') - .then(r => r.data) - .catch(console.log) - - let items = [] - data.response.forEach(group => { - group.channels.forEach(channel => { - items.push({ - lang: 'fi', - site_id: `${group.id}#${channel.id}`, - name: channel.name - }) - }) - }) - - return items - } -} - -function getImage(item) { - const image = item.images.find(i => i.type === 'default' && i.sizeTag === '1200x630') - - return image ? image.url : null -} - -function getStart(item) { - return dayjs(item.startTime) -} - -function getStop(item) { - return dayjs(item.endTime) -} - -function getItems(content, channel) { - const [, channelId] = channel.site_id.split('#') - const data = JSON.parse(content) - if (!data || !data.response || !Array.isArray(data.response.publicationsByChannel)) return [] - const channelData = data.response.publicationsByChannel.find(i => i.channel.id === channelId) - if (!channelData || !Array.isArray(channelData.publications)) return [] - - return channelData.publications -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'iltalehti.fi', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: function ({ channel, date }) { + const [group] = channel.site_id.split('#') + + return `https://telkku.com/api/channel-groups/default_builtin_channelgroup${group}/offering?startTime=00%3A00%3A00.000&duration=PT24H&inclusionPolicy=IncludeOngoingAlso&limit=1000&tvDate=${date.format( + 'YYYY-MM-DD' + )}&view=PublicationDetails` + }, + parser: function ({ content, channel }) { + let programs = [] + const items = getItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.description, + image: getImage(item), + start: getStart(item), + stop: getStop(item) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://telkku.com/api/channel-groups') + .then(r => r.data) + .catch(console.log) + + let items = [] + data.response.forEach(group => { + group.channels.forEach(channel => { + items.push({ + lang: 'fi', + site_id: `${group.id}#${channel.id}`, + name: channel.name + }) + }) + }) + + return items + } +} + +function getImage(item) { + const image = item.images.find(i => i.type === 'default' && i.sizeTag === '1200x630') + + return image ? image.url : null +} + +function getStart(item) { + return dayjs(item.startTime) +} + +function getStop(item) { + return dayjs(item.endTime) +} + +function getItems(content, channel) { + const [, channelId] = channel.site_id.split('#') + const data = JSON.parse(content) + if (!data || !data.response || !Array.isArray(data.response.publicationsByChannel)) return [] + const channelData = data.response.publicationsByChannel.find(i => i.channel.id === channelId) + if (!channelData || !Array.isArray(channelData.publications)) return [] + + return channelData.publications +} diff --git a/sites/iltalehti.fi/iltalehti.fi.test.js b/sites/iltalehti.fi/iltalehti.fi.test.js index 954eb422..14477a3c 100644 --- a/sites/iltalehti.fi/iltalehti.fi.test.js +++ b/sites/iltalehti.fi/iltalehti.fi.test.js @@ -1,47 +1,47 @@ -const { parser, url } = require('./iltalehti.fi.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('2022-10-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1#yle-tv1', - xmltv_id: 'YleTV1.fi' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://telkku.com/api/channel-groups/default_builtin_channelgroup1/offering?startTime=00%3A00%3A00.000&duration=PT24H&inclusionPolicy=IncludeOngoingAlso&limit=1000&tvDate=2022-10-29&view=PublicationDetails' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const results = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-28T20:50:00.000Z', - stop: '2022-10-28T21:20:00.000Z', - title: 'Puoli seitsemän', - description: - 'Vieraana näyttelijä Elias Salonen. Puoli seiskassa vietetään sekä halloweeniä että joulua, kun Olli-Pekka tapaa todellisen jouluttajan. Juontajina Anniina Valtonen, Tuulianna Tola ja Olli-Pekka Kursi.', - image: - 'https://thumbor.prod.telkku.com/YTglotoUl7aJtzPtYnvM9tH03sY=/1200x630/smart/filters:quality(86):format(jpeg)/img.prod.telkku.com/program-images/0f885238ac16ce167a9d80eace450254.jpg' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json')), - channel - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./iltalehti.fi.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('2022-10-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1#yle-tv1', + xmltv_id: 'YleTV1.fi' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://telkku.com/api/channel-groups/default_builtin_channelgroup1/offering?startTime=00%3A00%3A00.000&duration=PT24H&inclusionPolicy=IncludeOngoingAlso&limit=1000&tvDate=2022-10-29&view=PublicationDetails' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-28T20:50:00.000Z', + stop: '2022-10-28T21:20:00.000Z', + title: 'Puoli seitsemän', + description: + 'Vieraana näyttelijä Elias Salonen. Puoli seiskassa vietetään sekä halloweeniä että joulua, kun Olli-Pekka tapaa todellisen jouluttajan. Juontajina Anniina Valtonen, Tuulianna Tola ja Olli-Pekka Kursi.', + image: + 'https://thumbor.prod.telkku.com/YTglotoUl7aJtzPtYnvM9tH03sY=/1200x630/smart/filters:quality(86):format(jpeg)/img.prod.telkku.com/program-images/0f885238ac16ce167a9d80eace450254.jpg' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json')), + channel + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/indihometv.com/__data__/content.html b/sites/indihometv.com/__data__/content.html new file mode 100644 index 00000000..9feda34d --- /dev/null +++ b/sites/indihometv.com/__data__/content.html @@ -0,0 +1 @@ +
    \ No newline at end of file diff --git a/sites/indihometv.com/__data__/no_content.html b/sites/indihometv.com/__data__/no_content.html new file mode 100644 index 00000000..6fedfd4c --- /dev/null +++ b/sites/indihometv.com/__data__/no_content.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sites/indihometv.com/indihometv.com.config.js b/sites/indihometv.com/indihometv.com.config.js index cf943af8..48e58165 100644 --- a/sites/indihometv.com/indihometv.com.config.js +++ b/sites/indihometv.com/indihometv.com.config.js @@ -1,92 +1,92 @@ -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) - -const tz = 'Asia/Jakarta' - -module.exports = { - site: 'indihometv.com', - days: 2, - url({ channel }) { - return `https://www.indihometv.com/livetv/${channel.site_id}` - }, - parser({ content, date }) { - const programs = [] - const [$, items] = parseItems(content, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = $(item) - let start = parseStart($item, date) - if (prev && start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - let stop = parseStop($item, date) - if (stop.isBefore(start)) { - stop = stop.add(1, 'd') - date = date.add(1, 'd') - } - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - const cheerio = require('cheerio') - const data = await axios - .get('https://www.indihometv.com/tv/live') - .then(response => response.data) - .catch(console.error) - - const $ = cheerio.load(data) - const items = $('#channelContainer a.channel-item').toArray() - const channels = items.map(item => { - const $item = $(item) - - return { - lang: 'id', - site_id: $item.data('url').substr($item.data('url').lastIndexOf('/') + 1), - name: $item.data('name') - } - }) - - return channels - } -} - -function parseStart($item, date) { - const timeString = $item.find('p').text() - const [, start] = timeString.match(/(\d{2}:\d{2}) -/) || [null, null] - const dateString = `${date.format('YYYY-MM-DD')} ${start}` - - return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', tz) -} - -function parseStop($item, date) { - const timeString = $item.find('p').text() - const [, stop] = timeString.match(/- (\d{2}:\d{2})/) || [null, null] - const dateString = `${date.format('YYYY-MM-DD')} ${stop}` - - return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', tz) -} - -function parseTitle($item) { - return $item.find('b').text() -} - -function parseItems(content, date) { - const $ = cheerio.load(content) - - return [$, $(`#pills-${date.format('YYYY-MM-DD')} .schedule-item`).toArray()] -} +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) + +const tz = 'Asia/Jakarta' + +module.exports = { + site: 'indihometv.com', + days: 2, + url({ channel }) { + return `https://www.indihometv.com/livetv/${channel.site_id}` + }, + parser({ content, date }) { + const programs = [] + const [$, items] = parseItems(content, date) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = $(item) + let start = parseStart($item, date) + if (prev && start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + let stop = parseStop($item, date) + if (stop.isBefore(start)) { + stop = stop.add(1, 'd') + date = date.add(1, 'd') + } + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const axios = require('axios') + const cheerio = require('cheerio') + const data = await axios + .get('https://www.indihometv.com/tv/live') + .then(response => response.data) + .catch(console.error) + + const $ = cheerio.load(data) + const items = $('#channelContainer a.channel-item').toArray() + const channels = items.map(item => { + const $item = $(item) + + return { + lang: 'id', + site_id: $item.data('url').substr($item.data('url').lastIndexOf('/') + 1), + name: $item.data('name') + } + }) + + return channels + } +} + +function parseStart($item, date) { + const timeString = $item.find('p').text() + const [, start] = timeString.match(/(\d{2}:\d{2}) -/) || [null, null] + const dateString = `${date.format('YYYY-MM-DD')} ${start}` + + return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', tz) +} + +function parseStop($item, date) { + const timeString = $item.find('p').text() + const [, stop] = timeString.match(/- (\d{2}:\d{2})/) || [null, null] + const dateString = `${date.format('YYYY-MM-DD')} ${stop}` + + return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', tz) +} + +function parseTitle($item) { + return $item.find('b').text() +} + +function parseItems(content, date) { + const $ = cheerio.load(content) + + return [$, $(`#pills-${date.format('YYYY-MM-DD')} .schedule-item`).toArray()] +} diff --git a/sites/indihometv.com/indihometv.com.test.js b/sites/indihometv.com/indihometv.com.test.js index 8230a917..7ff6a62d 100644 --- a/sites/indihometv.com/indihometv.com.test.js +++ b/sites/indihometv.com/indihometv.com.test.js @@ -1,56 +1,57 @@ -const { parser, url } = require('./indihometv.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2022-08-08').startOf('d') -const channel = { - site_id: 'metrotv', - xmltv_id: 'MetroTV.id' -} -const content = - '
    ' - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://www.indihometv.com/livetv/metrotv') -}) - -it('can parse response', () => { - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - title: 'Headline News', - start: '2022-08-08T00:00:00.000Z', - stop: '2022-08-08T00:05:00.000Z' - }, - { - title: 'Editorial Media Indonesia', - start: '2022-08-08T00:05:00.000Z', - stop: '2022-08-08T00:30:00.000Z' - }, - { - title: 'Editorial Media Indonesia', - start: '2022-08-08T00:30:00.000Z', - stop: '2022-08-08T00:45:00.000Z' - }, - { - title: 'Editorial Media Indonesia', - start: '2022-08-08T00:45:00.000Z', - stop: '2022-08-08T01:00:00.000Z' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./indihometv.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2022-08-08').startOf('d') +const channel = { + site_id: 'metrotv', + xmltv_id: 'MetroTV.id' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://www.indihometv.com/livetv/metrotv') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + title: 'Headline News', + start: '2022-08-08T00:00:00.000Z', + stop: '2022-08-08T00:05:00.000Z' + }, + { + title: 'Editorial Media Indonesia', + start: '2022-08-08T00:05:00.000Z', + stop: '2022-08-08T00:30:00.000Z' + }, + { + title: 'Editorial Media Indonesia', + start: '2022-08-08T00:30:00.000Z', + stop: '2022-08-08T00:45:00.000Z' + }, + { + title: 'Editorial Media Indonesia', + start: '2022-08-08T00:45:00.000Z', + stop: '2022-08-08T01:00:00.000Z' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/ionplustv.com/ionplustv.com.config.js b/sites/ionplustv.com/ionplustv.com.config.js index 77073521..231bea59 100644 --- a/sites/ionplustv.com/ionplustv.com.config.js +++ b/sites/ionplustv.com/ionplustv.com.config.js @@ -1,107 +1,107 @@ -const dayjs = require('dayjs') -const cheerio = require('cheerio') -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: 'ionplustv.com', - days: 2, - url({ date }) { - return `https://ionplustv.com/schedule/${date.format('YYYY-MM-DD')}` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - for (let item of items) { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const duration = parseDuration($item) - let stop = start.add(duration, 'm') - - programs.push({ - title: parseTitle($item), - sub_title: parseSubTitle($item), - description: parseDescription($item), - image: parseImage($item), - rating: parseRating($item), - start, - stop - }) - } - - return programs - } -} - -function parseDescription($item) { - return $item('.panel-body > div > div > div > p:nth-child(2)').text().trim() -} - -function parseImage($item) { - return $item('.video-thumbnail img').attr('src') -} - -function parseTitle($item) { - return $item('.show-title').text().trim() -} - -function parseSubTitle($item) { - return $item('.panel-title > div > div > div > div:nth-child(2) > p') - .text() - .trim() - .replace(/\s\s+/g, ' ') -} - -function parseRating($item) { - const [, rating] = $item('.tv-rating') - .text() - .match(/([^(]+)/) || [null, null] - - return rating - ? { - system: 'MPA', - value: rating.trim() - } - : null -} - -function parseStart($item, date) { - let time = $item('.panel-title h2').clone().children().remove().end().text().trim() - time = time.includes(':') ? time : time + ':00' - const meridiem = $item('.panel-title h2 > .meridiem').text().trim() - - return dayjs.tz( - `${date.format('YYYY-MM-DD')} ${time} ${meridiem}`, - 'YYYY-MM-DD H:mm A', - 'America/New_York' - ) -} - -function parseDuration($item) { - const [, duration] = $item('.tv-rating') - .text() - .trim() - .match(/\((\d+)/) || [null, null] - - return parseInt(duration) -} - -function parseItems(content) { - if (!content) return [] - const $ = cheerio.load(content) - - return $('#accordion > div').toArray() -} +const dayjs = require('dayjs') +const cheerio = require('cheerio') +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: 'ionplustv.com', + days: 2, + url({ date }) { + return `https://ionplustv.com/schedule/${date.format('YYYY-MM-DD')}` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + for (let item of items) { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const duration = parseDuration($item) + let stop = start.add(duration, 'm') + + programs.push({ + title: parseTitle($item), + sub_title: parseSubTitle($item), + description: parseDescription($item), + image: parseImage($item), + rating: parseRating($item), + start, + stop + }) + } + + return programs + } +} + +function parseDescription($item) { + return $item('.panel-body > div > div > div > p:nth-child(2)').text().trim() +} + +function parseImage($item) { + return $item('.video-thumbnail img').attr('src') +} + +function parseTitle($item) { + return $item('.show-title').text().trim() +} + +function parseSubTitle($item) { + return $item('.panel-title > div > div > div > div:nth-child(2) > p') + .text() + .trim() + .replace(/\s\s+/g, ' ') +} + +function parseRating($item) { + const [, rating] = $item('.tv-rating') + .text() + .match(/([^(]+)/) || [null, null] + + return rating + ? { + system: 'MPA', + value: rating.trim() + } + : null +} + +function parseStart($item, date) { + let time = $item('.panel-title h2').clone().children().remove().end().text().trim() + time = time.includes(':') ? time : time + ':00' + const meridiem = $item('.panel-title h2 > .meridiem').text().trim() + + return dayjs.tz( + `${date.format('YYYY-MM-DD')} ${time} ${meridiem}`, + 'YYYY-MM-DD H:mm A', + 'America/New_York' + ) +} + +function parseDuration($item) { + const [, duration] = $item('.tv-rating') + .text() + .trim() + .match(/\((\d+)/) || [null, null] + + return parseInt(duration) +} + +function parseItems(content) { + if (!content) return [] + const $ = cheerio.load(content) + + return $('#accordion > div').toArray() +} diff --git a/sites/ionplustv.com/ionplustv.com.test.js b/sites/ionplustv.com/ionplustv.com.test.js index 1a76e1f4..21ce2fd3 100644 --- a/sites/ionplustv.com/ionplustv.com.test.js +++ b/sites/ionplustv.com/ionplustv.com.test.js @@ -1,49 +1,49 @@ -const { parser, url } = require('./ionplustv.com.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('2022-11-08', 'YYYY-MM-DD').startOf('d') - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://ionplustv.com/schedule/2022-11-08') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - - 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: '2022-11-08T10:00:00.000Z', - stop: '2022-11-08T11:00:00.000Z', - title: 'All For Nothing?', - sub_title: '226 : Randy & Sarita Vs. Jean-marcel & Melodie', - image: - 'https://ionplustv.com/static/programs/shows/all-for-nothing/show-banner-all-for-nothing-5ab162f2d8ee6-897aca6d7d9a7d4e2026ca3b592d8b2a047238fa.png', - rating: { - system: 'MPA', - value: 'TV-PG+L' - }, - description: - "Randy and Sarita want to take their relationship to the next level and move-in together. Blending their families will require space for seven so they must sell Randy's dated bungalow for top dollar. Paul and Penny have differing opinions on the best plan for this house, but they do agree that all the wallpaper boarders must go! Having struggled to get the demolition started, Randy and Sarita turn up the reno pace in the second week which includes gambling on a poker night fundraiser. In preparation for retirement, Jean-Marcel and Melodie are ready to downsize. Having been out of the real estate market for ages, they have no idea how to ˜wow' the buyers of today. Armed with Paul and Penny's job list to bring their house into the now, they make major progress on day one. Flu, leaks, and a free shower insert that won't fit into their bathroom slow down their pace giving the competition a chance to overtake their early lead." - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html')), - date - }) - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./ionplustv.com.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('2022-11-08', 'YYYY-MM-DD').startOf('d') + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://ionplustv.com/schedule/2022-11-08') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + + 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: '2022-11-08T10:00:00.000Z', + stop: '2022-11-08T11:00:00.000Z', + title: 'All For Nothing?', + sub_title: '226 : Randy & Sarita Vs. Jean-marcel & Melodie', + image: + 'https://ionplustv.com/static/programs/shows/all-for-nothing/show-banner-all-for-nothing-5ab162f2d8ee6-897aca6d7d9a7d4e2026ca3b592d8b2a047238fa.png', + rating: { + system: 'MPA', + value: 'TV-PG+L' + }, + description: + "Randy and Sarita want to take their relationship to the next level and move-in together. Blending their families will require space for seven so they must sell Randy's dated bungalow for top dollar. Paul and Penny have differing opinions on the best plan for this house, but they do agree that all the wallpaper boarders must go! Having struggled to get the demolition started, Randy and Sarita turn up the reno pace in the second week which includes gambling on a poker night fundraiser. In preparation for retirement, Jean-Marcel and Melodie are ready to downsize. Having been out of the real estate market for ages, they have no idea how to ˜wow' the buyers of today. Armed with Paul and Penny's job list to bring their house into the now, they make major progress on day one. Flu, leaks, and a free shower insert that won't fit into their bathroom slow down their pace giving the competition a chance to overtake their early lead." + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html')), + date + }) + expect(results).toMatchObject([]) +}) diff --git a/sites/ipko.tv/__data__/content.json b/sites/ipko.tv/__data__/content.json new file mode 100644 index 00000000..fff5a347 --- /dev/null +++ b/sites/ipko.tv/__data__/content.json @@ -0,0 +1,58 @@ +{ + "shows": [ + { + "title": "IPKO Promo", + "show_start": 1735012800, + "show_end": 1735020000, + "timestamp": "5:00 - 7:00", + "show_id": "EPG_TvProfil_IPKOPROMO_296105567", + "thumbnail": "https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg", + "is_adult": false, + "friendly_id": "ipko_promo_4cf3", + "pg": "", + "genres": [], + "year": 0, + "summary": "", + "categories": "Other", + "stb_only": false, + "is_live": false, + "original_title": "IPKO Promo" + }, + { + "title": "IPKO Promo", + "show_start": 1735020000, + "show_end": 1735027200, + "timestamp": "7:00 - 9:00", + "show_id": "EPG_TvProfil_IPKOPROMO_296105568", + "thumbnail": "https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg", + "is_adult": false, + "friendly_id": "ipko_promo_416b", + "pg": "", + "genres": [], + "year": 0, + "summary": "", + "categories": "Other", + "stb_only": false, + "is_live": false, + "original_title": "IPKO Promo" + }, + { + "title": "IPKO Promo", + "show_start": 1735027200, + "show_end": 1735034400, + "timestamp": "9:00 - 11:00", + "show_id": "EPG_TvProfil_IPKOPROMO_296105569", + "thumbnail": "https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg", + "is_adult": false, + "friendly_id": "ipko_promo_2e23", + "pg": "", + "genres": [], + "year": 0, + "summary": "", + "categories": "Other", + "stb_only": false, + "is_live": false, + "original_title": "IPKO Promo" + } + ] + } \ No newline at end of file diff --git a/sites/ipko.tv/__data__/no_content.json b/sites/ipko.tv/__data__/no_content.json new file mode 100644 index 00000000..78743050 --- /dev/null +++ b/sites/ipko.tv/__data__/no_content.json @@ -0,0 +1 @@ +{"shows":[]} \ No newline at end of file diff --git a/sites/ipko.tv/ipko.tv.config.js b/sites/ipko.tv/ipko.tv.config.js index 55803f1e..3b5fd411 100644 --- a/sites/ipko.tv/ipko.tv.config.js +++ b/sites/ipko.tv/ipko.tv.config.js @@ -1,80 +1,80 @@ -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') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'ipko.tv', - timezone: 'Europe/Belgrade', - days: 5, - url() { - return 'https://stargate.ipko.tv/api/titan.tv.WebEpg/GetWebEpgData' - }, - request: { - method: 'POST', - headers: { - Host: 'stargate.ipko.tv', - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0', - Accept: 'application/json, text/plain, */*', - 'Accept-Language': 'nl,en-US;q=0.7,en;q=0.3', - 'Content-Type': 'application/json', - 'X-AppLayout': '1', - 'x-language': 'sq', - Origin: 'https://ipko.tv', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'cross-site', - 'Sec-GPC': '1', - Connection: 'keep-alive' - }, - data({ channel, date }) { - const todayEpoch = date.startOf('day').unix() - const nextDayEpoch = date.add(1, 'day').startOf('day').unix() - return JSON.stringify({ - ch_ext_id: channel.site_id, - from: todayEpoch, - to: nextDayEpoch - }) - } - }, - parser: function ({ content }) { - const programs = [] - const data = JSON.parse(content) - data.shows.forEach(show => { - const start = dayjs.unix(show.show_start).utc() - const stop = dayjs.unix(show.show_end).utc() - const programData = { - title: show.title, - description: show.summary || 'No description available', - start: start.toISOString(), - stop: stop.toISOString(), - thumbnail: show.thumbnail - } - programs.push(programData) - }) - return programs - }, - async channels() { - const response = await axios.post( - 'https://stargate.ipko.tv/api/titan.tv.WebEpg/ZapList', - JSON.stringify({ includeRadioStations: true }), - { - headers: this.request.headers - } - ) - - const data = response.data.data - return data.map(item => ({ - lang: 'sq', - name: String(item.channel.title), - site_id: String(item.channel.id) - //logo: String(item.channel.logo) - })) - } -} +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') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'ipko.tv', + timezone: 'Europe/Belgrade', + days: 5, + url() { + return 'https://stargate.ipko.tv/api/titan.tv.WebEpg/GetWebEpgData' + }, + request: { + method: 'POST', + headers: { + Host: 'stargate.ipko.tv', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0', + Accept: 'application/json, text/plain, */*', + 'Accept-Language': 'nl,en-US;q=0.7,en;q=0.3', + 'Content-Type': 'application/json', + 'X-AppLayout': '1', + 'x-language': 'sq', + Origin: 'https://ipko.tv', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'cross-site', + 'Sec-GPC': '1', + Connection: 'keep-alive' + }, + data({ channel, date }) { + const todayEpoch = date.startOf('day').unix() + const nextDayEpoch = date.add(1, 'day').startOf('day').unix() + return JSON.stringify({ + ch_ext_id: channel.site_id, + from: todayEpoch, + to: nextDayEpoch + }) + } + }, + parser: function ({ content }) { + const programs = [] + const data = JSON.parse(content) + data.shows.forEach(show => { + const start = dayjs.unix(show.show_start).utc() + const stop = dayjs.unix(show.show_end).utc() + const programData = { + title: show.title, + description: show.summary || 'No description available', + start: start.toISOString(), + stop: stop.toISOString(), + thumbnail: show.thumbnail + } + programs.push(programData) + }) + return programs + }, + async channels() { + const response = await axios.post( + 'https://stargate.ipko.tv/api/titan.tv.WebEpg/ZapList', + JSON.stringify({ includeRadioStations: true }), + { + headers: this.request.headers + } + ) + + const data = response.data.data + return data.map(item => ({ + lang: 'sq', + name: String(item.channel.title), + site_id: String(item.channel.id) + //logo: String(item.channel.logo) + })) + } +} diff --git a/sites/ipko.tv/ipko.tv.test.js b/sites/ipko.tv/ipko.tv.test.js index 19a83e86..a7e456f0 100644 --- a/sites/ipko.tv/ipko.tv.test.js +++ b/sites/ipko.tv/ipko.tv.test.js @@ -1,111 +1,54 @@ -const { parser, url } = require('./ipko.tv.config.js') -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('2024-12-24', 'YYYY-MM-DD').startOf('day') -const channel = { - site_id: 'ipko-promo', - xmltv_id: 'IPKOPROMO' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe('https://stargate.ipko.tv/api/titan.tv.WebEpg/GetWebEpgData') -}) - -it('can parse response', () => { - const content = ` - { - "shows": [ - { - "title": "IPKO Promo", - "show_start": 1735012800, - "show_end": 1735020000, - "timestamp": "5:00 - 7:00", - "show_id": "EPG_TvProfil_IPKOPROMO_296105567", - "thumbnail": "https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg", - "is_adult": false, - "friendly_id": "ipko_promo_4cf3", - "pg": "", - "genres": [], - "year": 0, - "summary": "", - "categories": "Other", - "stb_only": false, - "is_live": false, - "original_title": "IPKO Promo" - }, - { - "title": "IPKO Promo", - "show_start": 1735020000, - "show_end": 1735027200, - "timestamp": "7:00 - 9:00", - "show_id": "EPG_TvProfil_IPKOPROMO_296105568", - "thumbnail": "https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg", - "is_adult": false, - "friendly_id": "ipko_promo_416b", - "pg": "", - "genres": [], - "year": 0, - "summary": "", - "categories": "Other", - "stb_only": false, - "is_live": false, - "original_title": "IPKO Promo" - }, - { - "title": "IPKO Promo", - "show_start": 1735027200, - "show_end": 1735034400, - "timestamp": "9:00 - 11:00", - "show_id": "EPG_TvProfil_IPKOPROMO_296105569", - "thumbnail": "https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg", - "is_adult": false, - "friendly_id": "ipko_promo_2e23", - "pg": "", - "genres": [], - "year": 0, - "summary": "", - "categories": "Other", - "stb_only": false, - "is_live": false, - "original_title": "IPKO Promo" - } - ] - }` - - const result = parser({ content, channel }) - - expect(result).toMatchObject([ - { - title: 'IPKO Promo', - description: 'No description available', - start: '2024-12-24T04:00:00.000Z', - stop: '2024-12-24T06:00:00.000Z', - thumbnail: 'https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg' - }, - { - title: 'IPKO Promo', - description: 'No description available', - start: '2024-12-24T06:00:00.000Z', - stop: '2024-12-24T08:00:00.000Z', - thumbnail: 'https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg' - }, - { - title: 'IPKO Promo', - description: 'No description available', - start: '2024-12-24T08:00:00.000Z', - stop: '2024-12-24T10:00:00.000Z', - thumbnail: 'https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '{"shows":[]}' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./ipko.tv.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('2024-12-24', 'YYYY-MM-DD').startOf('day') +const channel = { + site_id: 'ipko-promo', + xmltv_id: 'IPKOPROMO' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe('https://stargate.ipko.tv/api/titan.tv.WebEpg/GetWebEpgData') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content, channel }) + + expect(result).toMatchObject([ + { + title: 'IPKO Promo', + description: 'No description available', + start: '2024-12-24T04:00:00.000Z', + stop: '2024-12-24T06:00:00.000Z', + thumbnail: 'https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg' + }, + { + title: 'IPKO Promo', + description: 'No description available', + start: '2024-12-24T06:00:00.000Z', + stop: '2024-12-24T08:00:00.000Z', + thumbnail: 'https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg' + }, + { + title: 'IPKO Promo', + description: 'No description available', + start: '2024-12-24T08:00:00.000Z', + stop: '2024-12-24T10:00:00.000Z', + thumbnail: 'https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/jiotv.com/jiotv.com.config.js b/sites/jiotv.com/jiotv.com.config.js index ed30001c..3a753505 100644 --- a/sites/jiotv.com/jiotv.com.config.js +++ b/sites/jiotv.com/jiotv.com.config.js @@ -1,87 +1,87 @@ -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') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'jiotv.com', - days: 2, - url({ date, channel }) { - const offset = date.diff(dayjs.utc().startOf('d'), 'd') - - return `https://jiotvapi.cdn.jio.com/apis/v1.3/getepg/get?channel_id=${channel.site_id}&offset=${offset}` - }, - parser({ content }) { - let programs = [] - let items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.showname, - description: item.episode_desc || item.description, - directors: parseList(item.director), - actors: parseList(item.starCast), - categories: item.showGenre, - episode: parseEpisode(item), - keywords: item.keywords, - icon: parseIcon(item), - image: parseImage(item), - start: dayjs(item.startEpoch), - stop: dayjs(item.endEpoch) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get( - 'https://jiotvapi.cdn.jio.com/apis/v3.0/getMobileChannelList/get/?langId=6&devicetype=phone&os=android&usertype=JIO&version=343' - ) - .then(r => r.data) - .catch(console.error) - - return data.result.map(c => { - return { - lang: 'en', - site_id: c.channel_id, - name: c.channel_name - } - }) - } -} - -function parseEpisode(item) { - return item.episode_num > 0 ? item.episode_num : null -} - -function parseList(string) { - return string.split(', ').filter(Boolean) -} - -function parseIcon(item) { - return item.episodeThumbnail - ? `https://jiotvimages.cdn.jio.com/dare_images/shows/700/-/${item.episodeThumbnail}` - : null -} - -function parseImage(item) { - return item.episodePoster - ? `https://jiotvimages.cdn.jio.com/dare_images/shows/700/-/${item.episodePoster}` - : null -} - -function parseItems(content) { - try { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.epg)) return [] - - return data.epg - } catch { - return [] - } -} +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') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'jiotv.com', + days: 2, + url({ date, channel }) { + const offset = date.diff(dayjs.utc().startOf('d'), 'd') + + return `https://jiotvapi.cdn.jio.com/apis/v1.3/getepg/get?channel_id=${channel.site_id}&offset=${offset}` + }, + parser({ content }) { + let programs = [] + let items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.showname, + description: item.episode_desc || item.description, + directors: parseList(item.director), + actors: parseList(item.starCast), + categories: item.showGenre, + episode: parseEpisode(item), + keywords: item.keywords, + icon: parseIcon(item), + image: parseImage(item), + start: dayjs(item.startEpoch), + stop: dayjs(item.endEpoch) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get( + 'https://jiotvapi.cdn.jio.com/apis/v3.0/getMobileChannelList/get/?langId=6&devicetype=phone&os=android&usertype=JIO&version=343' + ) + .then(r => r.data) + .catch(console.error) + + return data.result.map(c => { + return { + lang: 'en', + site_id: c.channel_id, + name: c.channel_name + } + }) + } +} + +function parseEpisode(item) { + return item.episode_num > 0 ? item.episode_num : null +} + +function parseList(string) { + return string.split(', ').filter(Boolean) +} + +function parseIcon(item) { + return item.episodeThumbnail + ? `https://jiotvimages.cdn.jio.com/dare_images/shows/700/-/${item.episodeThumbnail}` + : null +} + +function parseImage(item) { + return item.episodePoster + ? `https://jiotvimages.cdn.jio.com/dare_images/shows/700/-/${item.episodePoster}` + : null +} + +function parseItems(content) { + try { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.epg)) return [] + + return data.epg + } catch { + return [] + } +} diff --git a/sites/jiotv.com/jiotv.com.test.js b/sites/jiotv.com/jiotv.com.test.js index 5738c930..c2531817 100644 --- a/sites/jiotv.com/jiotv.com.test.js +++ b/sites/jiotv.com/jiotv.com.test.js @@ -1,86 +1,86 @@ -const { parser, url } = require('./jiotv.com.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) - -jest.useFakeTimers().setSystemTime(new Date('2025-01-15')) - -const date = dayjs.utc('2025-01-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '146', - xmltv_id: 'HistoryTV18HD.in' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://jiotvapi.cdn.jio.com/apis/v1.3/getepg/get?channel_id=146&offset=2' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') - let results = parser({ content }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(46) - expect(results[1]).toMatchObject({ - start: '2025-01-16T19:13:00.000Z', - stop: '2025-01-16T19:57:00.000Z', - title: "History's Greatest Heists With Pierce Brosnan", - description: - "Daring criminals burrow beneath London's streets to infiltrate a Lloyds Bank vault. However, their heist takes an unexpected turn when a radio hobbyist stumbles upon their communications.", - categories: ['History'], - directors: ['Brendan G Murphy'], - actors: ['Brent Picha', 'William Sibley', 'Bobby Williams'], - episode: 3, - keywords: [ - 'Heist', - 'Criminal mastermind', - 'Consequences', - 'Historical account', - 'Historical significance', - 'Criminal offence' - ], - icon: 'https://jiotvimages.cdn.jio.com/dare_images/shows/700/-/2025-01-17/250117146001_s.jpg', - image: 'https://jiotvimages.cdn.jio.com/dare_images/shows/700/-/2025-01-17/250117146001.jpg' - }) - expect(results[45]).toMatchObject({ - start: '2025-01-17T18:29:00.000Z', - stop: '2025-01-17T18:29:59.000Z', - title: "History's Greatest Escapes with Morgan Freeman", - description: - 'In French Guiana, when petty thief Rene Belbenoit faces harsh imprisonment, determined to break free, he endures years of gruelling conditions and attempts numerous daring escapes.', - categories: ['Crime'], - directors: ['Mitch Marcus'], - actors: [], - episode: 5, - keywords: [ - 'Imprisoned', - 'Prison', - 'Prison Break', - 'Prison film', - 'Set in a prison', - 'Escape', - 'Survival', - 'Survival Instinct' - ], - icon: 'https://jiotvimages.cdn.jio.com/dare_images/shows/700/-/2025-01-17/250117146045_s.jpg', - image: 'https://jiotvimages.cdn.jio.com/dare_images/shows/700/-/2025-01-17/250117146045.jpg' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: '' - }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./jiotv.com.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) + +jest.useFakeTimers().setSystemTime(new Date('2025-01-15')) + +const date = dayjs.utc('2025-01-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '146', + xmltv_id: 'HistoryTV18HD.in' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://jiotvapi.cdn.jio.com/apis/v1.3/getepg/get?channel_id=146&offset=2' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') + let results = parser({ content }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(46) + expect(results[1]).toMatchObject({ + start: '2025-01-16T19:13:00.000Z', + stop: '2025-01-16T19:57:00.000Z', + title: "History's Greatest Heists With Pierce Brosnan", + description: + "Daring criminals burrow beneath London's streets to infiltrate a Lloyds Bank vault. However, their heist takes an unexpected turn when a radio hobbyist stumbles upon their communications.", + categories: ['History'], + directors: ['Brendan G Murphy'], + actors: ['Brent Picha', 'William Sibley', 'Bobby Williams'], + episode: 3, + keywords: [ + 'Heist', + 'Criminal mastermind', + 'Consequences', + 'Historical account', + 'Historical significance', + 'Criminal offence' + ], + icon: 'https://jiotvimages.cdn.jio.com/dare_images/shows/700/-/2025-01-17/250117146001_s.jpg', + image: 'https://jiotvimages.cdn.jio.com/dare_images/shows/700/-/2025-01-17/250117146001.jpg' + }) + expect(results[45]).toMatchObject({ + start: '2025-01-17T18:29:00.000Z', + stop: '2025-01-17T18:29:59.000Z', + title: "History's Greatest Escapes with Morgan Freeman", + description: + 'In French Guiana, when petty thief Rene Belbenoit faces harsh imprisonment, determined to break free, he endures years of gruelling conditions and attempts numerous daring escapes.', + categories: ['Crime'], + directors: ['Mitch Marcus'], + actors: [], + episode: 5, + keywords: [ + 'Imprisoned', + 'Prison', + 'Prison Break', + 'Prison film', + 'Set in a prison', + 'Escape', + 'Survival', + 'Survival Instinct' + ], + icon: 'https://jiotvimages.cdn.jio.com/dare_images/shows/700/-/2025-01-17/250117146045_s.jpg', + image: 'https://jiotvimages.cdn.jio.com/dare_images/shows/700/-/2025-01-17/250117146045.jpg' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: '' + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/kan.org.il/__data__/content.json b/sites/kan.org.il/__data__/content.json new file mode 100644 index 00000000..0037cfd4 --- /dev/null +++ b/sites/kan.org.il/__data__/content.json @@ -0,0 +1 @@ +[{"title":"ארץ מולדת - בין תורכיה לבריטניה","start_time":"2022-03-06T00:05:37","end_time":"2022-03-06T00:27:12","id":"2598","age_category_desc":"0","epg_name":"ארץ מולדת","title1":"ארץ מולדת - בין תורכיה לבריטניה","chapter_number":"9","live_desc":"קבוצת תלמידים מתארגנת בפרוץ מלחמת העולם הראשונה להגיש עזרה לישוב. באמצעות התלמידים לומד הצופה על בעיותיו של הישוב בתקופת המלחמה, והתלבטותו בין נאמנות לשלטון העות'מאני לבין תקוותיו מהבריטים הכובשים.","Station_Radio":"0","Station_Id":"20","stationUrlScheme":"kan11://plugin/?type=player&plugin_identifier=kan_player&ds=general-provider%3A%2F%2FfetchData%3Ftype%3DFEED_JSON%26url%3DaHR0cHM6Ly93d3cua2FuLm9yZy5pbC9hcHBLYW4vbGl2ZVN0YXRpb25zLmFzaHg%3D&id=4","program_code":"3671","picture_code":"https://kanweb.blob.core.windows.net/download/pictures/2021/1/20/imgid=45847_Z.jpeg","program_image":"","station_image":"Logo_Image_Logo20_img__8.jpg","program_id":"","timezone":"2"}] \ No newline at end of file diff --git a/sites/kan.org.il/kan.org.il.config.js b/sites/kan.org.il/kan.org.il.config.js index 01aaec09..999e155c 100644 --- a/sites/kan.org.il/kan.org.il.config.js +++ b/sites/kan.org.il/kan.org.il.config.js @@ -1,52 +1,52 @@ -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: 'kan.org.il', - days: 2, - url: function ({ channel, date }) { - return `https://www.kan.org.il/tv-guide/tv_guidePrograms.ashx?stationID=${ - channel.site_id - }&day=${date.format('DD/MM/YYYY')}` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.live_desc, - image: item.picture_code, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseStart(item) { - if (!item.start_time) return null - - return dayjs.tz(item.start_time, 'YYYY-MM-DDTHH:mm:ss', 'Asia/Jerusalem') -} - -function parseStop(item) { - if (!item.end_time) return null - - return dayjs.tz(item.end_time, 'YYYY-MM-DDTHH:mm:ss', 'Asia/Jerusalem') -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!Array.isArray(data)) return [] - - return data -} +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: 'kan.org.il', + days: 2, + url: function ({ channel, date }) { + return `https://www.kan.org.il/tv-guide/tv_guidePrograms.ashx?stationID=${ + channel.site_id + }&day=${date.format('DD/MM/YYYY')}` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.live_desc, + image: item.picture_code, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseStart(item) { + if (!item.start_time) return null + + return dayjs.tz(item.start_time, 'YYYY-MM-DDTHH:mm:ss', 'Asia/Jerusalem') +} + +function parseStop(item) { + if (!item.end_time) return null + + return dayjs.tz(item.end_time, 'YYYY-MM-DDTHH:mm:ss', 'Asia/Jerusalem') +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!Array.isArray(data)) return [] + + return data +} diff --git a/sites/kan.org.il/kan.org.il.test.js b/sites/kan.org.il/kan.org.il.test.js index 4dfd2575..8b7c270f 100644 --- a/sites/kan.org.il/kan.org.il.test.js +++ b/sites/kan.org.il/kan.org.il.test.js @@ -1,46 +1,47 @@ -const { parser, url } = require('./kan.org.il.config.js') -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('2022-03-06', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '19', - xmltv_id: 'KANEducational.il' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.kan.org.il/tv-guide/tv_guidePrograms.ashx?stationID=19&day=06/03/2022' - ) -}) - -it('can parse response', () => { - const content = - '[{"title":"ארץ מולדת - בין תורכיה לבריטניה","start_time":"2022-03-06T00:05:37","end_time":"2022-03-06T00:27:12","id":"2598","age_category_desc":"0","epg_name":"ארץ מולדת","title1":"ארץ מולדת - בין תורכיה לבריטניה","chapter_number":"9","live_desc":"קבוצת תלמידים מתארגנת בפרוץ מלחמת העולם הראשונה להגיש עזרה לישוב. באמצעות התלמידים לומד הצופה על בעיותיו של הישוב בתקופת המלחמה, והתלבטותו בין נאמנות לשלטון העות\'מאני לבין תקוותיו מהבריטים הכובשים.","Station_Radio":"0","Station_Id":"20","stationUrlScheme":"kan11://plugin/?type=player&plugin_identifier=kan_player&ds=general-provider%3A%2F%2FfetchData%3Ftype%3DFEED_JSON%26url%3DaHR0cHM6Ly93d3cua2FuLm9yZy5pbC9hcHBLYW4vbGl2ZVN0YXRpb25zLmFzaHg%3D&id=4","program_code":"3671","picture_code":"https://kanweb.blob.core.windows.net/download/pictures/2021/1/20/imgid=45847_Z.jpeg","program_image":"","station_image":"Logo_Image_Logo20_img__8.jpg","program_id":"","timezone":"2"}]' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-05T22:05:37.000Z', - stop: '2022-03-05T22:27:12.000Z', - title: 'ארץ מולדת - בין תורכיה לבריטניה', - description: - "קבוצת תלמידים מתארגנת בפרוץ מלחמת העולם הראשונה להגיש עזרה לישוב. באמצעות התלמידים לומד הצופה על בעיותיו של הישוב בתקופת המלחמה, והתלבטותו בין נאמנות לשלטון העות'מאני לבין תקוותיו מהבריטים הכובשים.", - image: 'https://kanweb.blob.core.windows.net/download/pictures/2021/1/20/imgid=45847_Z.jpeg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '[]' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./kan.org.il.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('2022-03-06', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '19', + xmltv_id: 'KANEducational.il' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.kan.org.il/tv-guide/tv_guidePrograms.ashx?stationID=19&day=06/03/2022' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-05T22:05:37.000Z', + stop: '2022-03-05T22:27:12.000Z', + title: 'ארץ מולדת - בין תורכיה לבריטניה', + description: + "קבוצת תלמידים מתארגנת בפרוץ מלחמת העולם הראשונה להגיש עזרה לישוב. באמצעות התלמידים לומד הצופה על בעיותיו של הישוב בתקופת המלחמה, והתלבטותו בין נאמנות לשלטון העות'מאני לבין תקוותיו מהבריטים הכובשים.", + image: 'https://kanweb.blob.core.windows.net/download/pictures/2021/1/20/imgid=45847_Z.jpeg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '[]' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/knr.gl/knr.gl.config.js b/sites/knr.gl/knr.gl.config.js index 1091e2eb..e8b7dbed 100644 --- a/sites/knr.gl/knr.gl.config.js +++ b/sites/knr.gl/knr.gl.config.js @@ -1,79 +1,79 @@ -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: 'knr.gl', - days: 2, - url: 'https://knr.gl/kl/tv/aallakaatitassat?ajax_form=1', - request: { - method: 'POST', - headers: { - 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8' - }, - data({ date }) { - const params = new URLSearchParams() - params.append('list_date', date.format('YYYY-MM-DD')) - params.append('form_id', 'knr_radio_tv_program_overview_form') - params.append('_triggering_element_name', 'list_date') - - return params - } - }, - parser({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const start = parseStart(item, date) - const stop = start.add(1, 'h') - if (prev) prev.stop = start - programs.push({ - title: item.title, - description: item.description, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item, date) { - const time = `${date.format('YYYY-MM-DD')} ${item.time}` - - return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'America/Nuuk') -} - -function parseItems(content) { - const data = JSON.parse(content) - if (data.length !== 1 || !data[0].data) return [] - const $ = cheerio.load(data[0].data) - - const items = [] - $('.overview-program__list__item').each((i, el) => { - const title = $(el).find('.overview-program__text').text().trim() - const description = $(el) - .find('.overview-program__sublist__item') - .first() - .text() - .trim() - .replace(/(\r\n|\n|\r)/gm, ' ') - const time = $(el).find('.overview-program__time').text().trim() - - items.push({ - title, - description, - time - }) - }) - - return items -} +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: 'knr.gl', + days: 2, + url: 'https://knr.gl/kl/tv/aallakaatitassat?ajax_form=1', + request: { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8' + }, + data({ date }) { + const params = new URLSearchParams() + params.append('list_date', date.format('YYYY-MM-DD')) + params.append('form_id', 'knr_radio_tv_program_overview_form') + params.append('_triggering_element_name', 'list_date') + + return params + } + }, + parser({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const start = parseStart(item, date) + const stop = start.add(1, 'h') + if (prev) prev.stop = start + programs.push({ + title: item.title, + description: item.description, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item, date) { + const time = `${date.format('YYYY-MM-DD')} ${item.time}` + + return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'America/Nuuk') +} + +function parseItems(content) { + const data = JSON.parse(content) + if (data.length !== 1 || !data[0].data) return [] + const $ = cheerio.load(data[0].data) + + const items = [] + $('.overview-program__list__item').each((i, el) => { + const title = $(el).find('.overview-program__text').text().trim() + const description = $(el) + .find('.overview-program__sublist__item') + .first() + .text() + .trim() + .replace(/(\r\n|\n|\r)/gm, ' ') + const time = $(el).find('.overview-program__time').text().trim() + + items.push({ + title, + description, + time + }) + }) + + return items +} diff --git a/sites/knr.gl/knr.gl.test.js b/sites/knr.gl/knr.gl.test.js index aa1c8cd1..99f0a90a 100644 --- a/sites/knr.gl/knr.gl.test.js +++ b/sites/knr.gl/knr.gl.test.js @@ -1,67 +1,67 @@ -const { parser, url, request } = require('./knr.gl.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('2021-11-22', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '#', - xmltv_id: 'KNR.gl' -} - -it('can generate valid url', () => { - expect(url).toBe('https://knr.gl/kl/tv/aallakaatitassat?ajax_form=1') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({}) -}) - -it('can generate valid request data', () => { - const params = request.data({ date }) - - expect(params.get('list_date')).toBe('2021-11-22') - expect(params.get('form_id')).toBe('knr_radio_tv_program_overview_form') - expect(params.get('_triggering_element_name')).toBe('list_date') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2021-11-22T11:00:00.000Z', - stop: '2021-11-22T11:30:00.000Z', - title: 'Issittormiuaqqat' - }) - - expect(results[4]).toMatchObject({ - start: '2021-11-22T13:00:00.000Z', - stop: '2021-11-22T13:30:00.000Z', - title: 'KNR2: Tusagassiortunik katersortitsineq - Erik Jensen', - description: - 'Naalakkersuisoq Erik Jensen tusagassiortunut 21.november nal. 10.00-11.00 katersortitsissaaq attassisinnaanermut siuariartornermullu pilersaarut pillugu (holdbarheds- og vækstplan).' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const result = parser({ - date, - channel, - content - }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./knr.gl.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('2021-11-22', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '#', + xmltv_id: 'KNR.gl' +} + +it('can generate valid url', () => { + expect(url).toBe('https://knr.gl/kl/tv/aallakaatitassat?ajax_form=1') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({}) +}) + +it('can generate valid request data', () => { + const params = request.data({ date }) + + expect(params.get('list_date')).toBe('2021-11-22') + expect(params.get('form_id')).toBe('knr_radio_tv_program_overview_form') + expect(params.get('_triggering_element_name')).toBe('list_date') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2021-11-22T11:00:00.000Z', + stop: '2021-11-22T11:30:00.000Z', + title: 'Issittormiuaqqat' + }) + + expect(results[4]).toMatchObject({ + start: '2021-11-22T13:00:00.000Z', + stop: '2021-11-22T13:30:00.000Z', + title: 'KNR2: Tusagassiortunik katersortitsineq - Erik Jensen', + description: + 'Naalakkersuisoq Erik Jensen tusagassiortunut 21.november nal. 10.00-11.00 katersortitsissaaq attassisinnaanermut siuariartornermullu pilersaarut pillugu (holdbarheds- og vækstplan).' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const result = parser({ + date, + channel, + content + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/kvf.fo/kvf.fo.config.js b/sites/kvf.fo/kvf.fo.config.js index f5381412..f95a27b9 100644 --- a/sites/kvf.fo/kvf.fo.config.js +++ b/sites/kvf.fo/kvf.fo.config.js @@ -1,71 +1,71 @@ -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: 'kvf.fo', - days: 2, - url({ date }) { - return `https://kvf.fo/nskra/sv?date=${date.format('YYYY-MM-DD')}` - }, - parser({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (!start) return - if (prev && start.isBefore(prev.stop)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - let stop = parseStop($item, date) - if (stop.isBefore(start)) { - stop = stop.add(1, 'd') - date = date.add(1, 'd') - } - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart($item, date) { - const string = $item('.s-normal > .s-time1').text().trim() - let [time] = string.match(/^(\d{2}:\d{2})/g) || [null] - if (!time) return null - time = `${date.format('YYYY-MM-DD')} ${time}` - - return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'Atlantic/Faroe') -} - -function parseStop($item, date) { - const string = $item('.s-normal > .s-time1').text().trim() - let [time] = string.match(/(\d{2}:\d{2})$/g) || [null] - if (!time) return null - time = `${date.format('YYYY-MM-DD')} ${time}` - - return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'Atlantic/Faroe') -} - -function parseTitle($item) { - return $item('.s-normal > .s-heiti').text() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.view > .view-content > div.views-row').toArray() -} +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: 'kvf.fo', + days: 2, + url({ date }) { + return `https://kvf.fo/nskra/sv?date=${date.format('YYYY-MM-DD')}` + }, + parser({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (!start) return + if (prev && start.isBefore(prev.stop)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + let stop = parseStop($item, date) + if (stop.isBefore(start)) { + stop = stop.add(1, 'd') + date = date.add(1, 'd') + } + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart($item, date) { + const string = $item('.s-normal > .s-time1').text().trim() + let [time] = string.match(/^(\d{2}:\d{2})/g) || [null] + if (!time) return null + time = `${date.format('YYYY-MM-DD')} ${time}` + + return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'Atlantic/Faroe') +} + +function parseStop($item, date) { + const string = $item('.s-normal > .s-time1').text().trim() + let [time] = string.match(/(\d{2}:\d{2})$/g) || [null] + if (!time) return null + time = `${date.format('YYYY-MM-DD')} ${time}` + + return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'Atlantic/Faroe') +} + +function parseTitle($item) { + return $item('.s-normal > .s-heiti').text() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.view > .view-content > div.views-row').toArray() +} diff --git a/sites/kvf.fo/kvf.fo.test.js b/sites/kvf.fo/kvf.fo.test.js index 70b839f1..db4a087e 100644 --- a/sites/kvf.fo/kvf.fo.test.js +++ b/sites/kvf.fo/kvf.fo.test.js @@ -1,42 +1,42 @@ -const { parser, url } = require('./kvf.fo.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('2021-11-21', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '#', - xmltv_id: 'KVFSjonvarp.fo' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://kvf.fo/nskra/sv?date=2021-11-21') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, './__data__/example.html')) - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result[2]).toMatchObject({ - start: '2021-11-21T18:05:00.000Z', - stop: '2021-11-21T18:30:00.000Z', - title: 'Letibygd 13' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: ' ' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./kvf.fo.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('2021-11-21', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '#', + xmltv_id: 'KVFSjonvarp.fo' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://kvf.fo/nskra/sv?date=2021-11-21') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, './__data__/example.html')) + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result[2]).toMatchObject({ + start: '2021-11-21T18:05:00.000Z', + stop: '2021-11-21T18:30:00.000Z', + title: 'Letibygd 13' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: ' ' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/m.tv.sms.cz/m.tv.sms.cz.config.js b/sites/m.tv.sms.cz/m.tv.sms.cz.config.js index 92861212..f1e67326 100644 --- a/sites/m.tv.sms.cz/m.tv.sms.cz.config.js +++ b/sites/m.tv.sms.cz/m.tv.sms.cz.config.js @@ -1,84 +1,84 @@ -const cheerio = require('cheerio') -const iconv = require('iconv-lite') -const { DateTime } = require('luxon') - -module.exports = { - site: 'm.tv.sms.cz', - days: 2, - url: function ({ date, channel }) { - return `https://m.tv.sms.cz/index.php?stanice=${channel.site_id}&cas=0&den=${date.format( - 'YYYY-MM-DD' - )}` - }, - parser: function ({ buffer, date }) { - const programs = [] - const items = parseItems(buffer) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ hours: 1 }) - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - const data = await axios - .get('https://m.tv.sms.cz/?zmen_stanice=true') - .then(r => r.data) - .catch(console.log) - - let channels = [] - const $ = cheerio.load(data) - $('.stanice').each((i, el) => { - const name = $(el).attr('title') - const site_id = $(el).find('input').attr('value').replace(/\|/g, '') - - if (!name) return - - channels.push({ - lang: 'cs', - site_id, - name - }) - }) - - return channels - } -} - -function parseStart($item, date) { - const timeString = $item('div > span').text().trim() - const dateString = `${date.format('MM/DD/YYYY')} ${timeString}` - - return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH.mm', { zone: 'Europe/Prague' }).toUTC() -} - -function parseDescription($item) { - return $item('a.nazev > div.detail').text().trim() -} - -function parseTitle($item) { - return $item('a.nazev > div:nth-child(1)').text().trim() -} - -function parseItems(buffer) { - const string = iconv.decode(buffer, 'win1250') - const $ = cheerio.load(string) - - return $('#obsah > div > div.porady > div.porad').toArray() -} +const cheerio = require('cheerio') +const iconv = require('iconv-lite') +const { DateTime } = require('luxon') + +module.exports = { + site: 'm.tv.sms.cz', + days: 2, + url: function ({ date, channel }) { + return `https://m.tv.sms.cz/index.php?stanice=${channel.site_id}&cas=0&den=${date.format( + 'YYYY-MM-DD' + )}` + }, + parser: function ({ buffer, date }) { + const programs = [] + const items = parseItems(buffer) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ hours: 1 }) + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const axios = require('axios') + const data = await axios + .get('https://m.tv.sms.cz/?zmen_stanice=true') + .then(r => r.data) + .catch(console.log) + + let channels = [] + const $ = cheerio.load(data) + $('.stanice').each((i, el) => { + const name = $(el).attr('title') + const site_id = $(el).find('input').attr('value').replace(/\|/g, '') + + if (!name) return + + channels.push({ + lang: 'cs', + site_id, + name + }) + }) + + return channels + } +} + +function parseStart($item, date) { + const timeString = $item('div > span').text().trim() + const dateString = `${date.format('MM/DD/YYYY')} ${timeString}` + + return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH.mm', { zone: 'Europe/Prague' }).toUTC() +} + +function parseDescription($item) { + return $item('a.nazev > div.detail').text().trim() +} + +function parseTitle($item) { + return $item('a.nazev > div:nth-child(1)').text().trim() +} + +function parseItems(buffer) { + const string = iconv.decode(buffer, 'win1250') + const $ = cheerio.load(string) + + return $('#obsah > div > div.porady > div.porad').toArray() +} diff --git a/sites/m.tv.sms.cz/m.tv.sms.cz.test.js b/sites/m.tv.sms.cz/m.tv.sms.cz.test.js index ac5a3216..9c5806fe 100644 --- a/sites/m.tv.sms.cz/m.tv.sms.cz.test.js +++ b/sites/m.tv.sms.cz/m.tv.sms.cz.test.js @@ -1,57 +1,57 @@ -const { parser, url } = require('./m.tv.sms.cz.config.js') -const iconv = require('iconv-lite') -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('2023-06-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'Cero', - xmltv_id: '0.es' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://m.tv.sms.cz/index.php?stanice=Cero&cas=0&den=2023-06-11' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const buffer = iconv.encode(content, 'win1250') - const results = parser({ buffer, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-06-11T03:21:00.000Z', - stop: '2023-06-11T04:08:00.000Z', - title: 'Conspiraciones al descubierto: La bomba atómica alemana y el hundimiento del Titanic', - description: 'Documentales' - }) - - expect(results[25]).toMatchObject({ - start: '2023-06-12T02:23:00.000Z', - stop: '2023-06-12T03:23:00.000Z', - title: 'Rapa I (6)', - description: 'Series' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - buffer: iconv.encode( - Buffer.from( - '' - ), - 'win1250' - ) - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./m.tv.sms.cz.config.js') +const iconv = require('iconv-lite') +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('2023-06-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'Cero', + xmltv_id: '0.es' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://m.tv.sms.cz/index.php?stanice=Cero&cas=0&den=2023-06-11' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const buffer = iconv.encode(content, 'win1250') + const results = parser({ buffer, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-06-11T03:21:00.000Z', + stop: '2023-06-11T04:08:00.000Z', + title: 'Conspiraciones al descubierto: La bomba atómica alemana y el hundimiento del Titanic', + description: 'Documentales' + }) + + expect(results[25]).toMatchObject({ + start: '2023-06-12T02:23:00.000Z', + stop: '2023-06-12T03:23:00.000Z', + title: 'Rapa I (6)', + description: 'Series' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + buffer: iconv.encode( + Buffer.from( + '' + ), + 'win1250' + ) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/m.tving.com/m.tving.com.config.js b/sites/m.tving.com/m.tving.com.config.js index 77ef4ea2..8b3a750d 100644 --- a/sites/m.tving.com/m.tving.com.config.js +++ b/sites/m.tving.com/m.tving.com.config.js @@ -1,94 +1,94 @@ -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') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'm.tving.com', - days: 2, - url: function ({ channel, date }) { - return `https://api.tving.com/v2/media/schedules/${channel.site_id}/${date.format( - 'YYYYMMDD' - )}?callback=cb&pageNo=1&pageSize=500&screenCode=CSSD0200&networkCode=CSND0900&osCode=CSOD0900&teleCode=CSCD0900&apiKey=4263d7d76161f4a19a9efe9ca7903ec4` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.program.name.ko, - description: item.program.synopsis.ko, - categories: parseCategories(item), - date: item.program.product_year, - directors: item.program.director, - actors: item.program.actor, - start: parseStart(item), - stop: parseStop(item), - image: parseImage(item) - }) - }) - - return programs - }, - async channels() { - let items = await axios - .get('https://m.tving.com/guide/schedule.tving') - .then(r => r.data) - .then(html => { - let $ = cheerio.load(html) - - return $('ul.cb > li').toArray() - }) - .catch(console.log) - - return items.map(item => { - let $item = cheerio.load(item) - let [, site_id] = $item('a') - .attr('href') - .match(/\?id=(.*)/) || [null, null] - let name = $item('img').attr('alt') - - return { - lang: 'ko', - site_id, - name - } - }) - } -} - -function parseImage(item) { - return item.program.image.length ? `https://image.tving.com${item.program.image[0].url}` : null -} - -function parseStart(item) { - return dayjs.tz(item.broadcast_start_time.toString(), 'YYYYMMDDHHmmss', 'Asia/Seoul') -} - -function parseStop(item) { - return dayjs.tz(item.broadcast_end_time.toString(), 'YYYYMMDDHHmmss', 'Asia/Seoul') -} - -function parseCategories(item) { - const categories = [] - - if (item.category1_name) categories.push(item.category1_name.ko) - if (item.category2_name) categories.push(item.category2_name.ko) - - return categories.filter(Boolean) -} - -function parseItems(content) { - let data = (content.match(/cb\((.*)\)/) || [null, null])[1] - if (!data) return [] - let json = JSON.parse(data) - if (!json || !json.body || !Array.isArray(json.body.result)) return [] - - return json.body.result -} +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') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'm.tving.com', + days: 2, + url: function ({ channel, date }) { + return `https://api.tving.com/v2/media/schedules/${channel.site_id}/${date.format( + 'YYYYMMDD' + )}?callback=cb&pageNo=1&pageSize=500&screenCode=CSSD0200&networkCode=CSND0900&osCode=CSOD0900&teleCode=CSCD0900&apiKey=4263d7d76161f4a19a9efe9ca7903ec4` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.program.name.ko, + description: item.program.synopsis.ko, + categories: parseCategories(item), + date: item.program.product_year, + directors: item.program.director, + actors: item.program.actor, + start: parseStart(item), + stop: parseStop(item), + image: parseImage(item) + }) + }) + + return programs + }, + async channels() { + let items = await axios + .get('https://m.tving.com/guide/schedule.tving') + .then(r => r.data) + .then(html => { + let $ = cheerio.load(html) + + return $('ul.cb > li').toArray() + }) + .catch(console.log) + + return items.map(item => { + let $item = cheerio.load(item) + let [, site_id] = $item('a') + .attr('href') + .match(/\?id=(.*)/) || [null, null] + let name = $item('img').attr('alt') + + return { + lang: 'ko', + site_id, + name + } + }) + } +} + +function parseImage(item) { + return item.program.image.length ? `https://image.tving.com${item.program.image[0].url}` : null +} + +function parseStart(item) { + return dayjs.tz(item.broadcast_start_time.toString(), 'YYYYMMDDHHmmss', 'Asia/Seoul') +} + +function parseStop(item) { + return dayjs.tz(item.broadcast_end_time.toString(), 'YYYYMMDDHHmmss', 'Asia/Seoul') +} + +function parseCategories(item) { + const categories = [] + + if (item.category1_name) categories.push(item.category1_name.ko) + if (item.category2_name) categories.push(item.category2_name.ko) + + return categories.filter(Boolean) +} + +function parseItems(content) { + let data = (content.match(/cb\((.*)\)/) || [null, null])[1] + if (!data) return [] + let json = JSON.parse(data) + if (!json || !json.body || !Array.isArray(json.body.result)) return [] + + return json.body.result +} diff --git a/sites/m.tving.com/m.tving.com.test.js b/sites/m.tving.com/m.tving.com.test.js index afb7a07f..3f5e764b 100644 --- a/sites/m.tving.com/m.tving.com.test.js +++ b/sites/m.tving.com/m.tving.com.test.js @@ -1,47 +1,47 @@ -const { parser, url } = require('./m.tving.com.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('2023-01-23', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'C00551', - xmltv_id: 'tvN.kr' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://api.tving.com/v2/media/schedules/C00551/20230123?callback=cb&pageNo=1&pageSize=500&screenCode=CSSD0200&networkCode=CSND0900&osCode=CSOD0900&teleCode=CSCD0900&apiKey=4263d7d76161f4a19a9efe9ca7903ec4' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.txt'), 'utf8') - const results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - title: '외계+인 1부', - description: '외계+인 1부', - image: 'https://image.tving.com/upload/cms/caip/CAIP0200/P001661154.jpg', - date: 2022, - categories: [], - directors: ['최동훈'], - actors: ['김우빈', '류준열'], - start: '2023-01-22T13:40:00.000Z', - stop: '2023-01-22T15:00:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.txt'), 'utf8') - - expect(parser({ content })).toMatchObject([]) -}) +const { parser, url } = require('./m.tving.com.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('2023-01-23', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'C00551', + xmltv_id: 'tvN.kr' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://api.tving.com/v2/media/schedules/C00551/20230123?callback=cb&pageNo=1&pageSize=500&screenCode=CSSD0200&networkCode=CSND0900&osCode=CSOD0900&teleCode=CSCD0900&apiKey=4263d7d76161f4a19a9efe9ca7903ec4' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.txt'), 'utf8') + const results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + title: '외계+인 1부', + description: '외계+인 1부', + image: 'https://image.tving.com/upload/cms/caip/CAIP0200/P001661154.jpg', + date: 2022, + categories: [], + directors: ['최동훈'], + actors: ['김우빈', '류준열'], + start: '2023-01-22T13:40:00.000Z', + stop: '2023-01-22T15:00:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.txt'), 'utf8') + + expect(parser({ content })).toMatchObject([]) +}) diff --git a/sites/magticom.ge/__data__/content.json b/sites/magticom.ge/__data__/content.json new file mode 100644 index 00000000..08956956 --- /dev/null +++ b/sites/magticom.ge/__data__/content.json @@ -0,0 +1 @@ +[{"id":2313254118,"channelId":260,"startTimestamp":"2021-11-22T07:00:00","endTimestamp":"2021-11-22T09:00:00","duration":null,"title":"\u0425\/\u0444 \"\u041d\u0435\u0440\u0430\u0432\u043d\u044b\u0439 \u0431\u0440\u0430\u043a\".","subTitle":"\u0425\/\u0444 \"\u041d\u0435\u0440\u0430\u0432\u043d\u044b\u0439 \u0431\u0440\u0430\u043a\".","info":"\u0413\u0443\u0434\u0436\u0430\u0440\u0430\u0442\u0435\u0446 \u0425\u0430\u0441\u043c\u0443\u043a\u0445 \u041f\u0430\u0442\u0435\u043b \u043f\u043e\u0441\u0441\u043e\u0440\u0438\u043b\u0441\u044f \u0441 \u043d\u043e\u0432\u044b\u043c \u0441\u043e\u0441\u0435\u0434\u043e\u043c \u0413\u0443\u0433\u0433\u0438 \u0422\u0430\u043d\u0434\u043e\u043d\u043e\u043c. \u041d\u043e \u0438\u043c \u043f\u0440\u0438\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u043f\u043e\u043c\u0438\u0440\u0438\u0442\u044c\u0441\u044f, \u043a\u043e\u0433\u0434\u0430 \u0438\u0445 \u0434\u0435\u0442\u0438 \u0432\u043b\u044e\u0431\u043b\u044f\u044e\u0442\u0441\u044f \u0434\u0440\u0443\u0433 \u0432 \u0434\u0440\u0443\u0433\u0430. \u0420\u0435\u0436\u0438\u0441\u0441\u0435\u0440: \u0421\u0430\u043d\u0434\u0436\u0430\u0439 \u0427\u0445\u0435\u043b. \u0410\u043a\u0442\u0435\u0440\u044b: \u0420\u0438\u0448\u0438 \u041a\u0430\u043f\u0443\u0440, \u041f\u0430\u0440\u0435\u0448 \u0420\u0430\u0432\u0430\u043b, \u0412\u0438\u0440 \u0414\u0430\u0441. 2017 \u0433\u043e\u0434.","pg":null,"year":null,"country":null,"imageUrl":null,"createdBy":-200,"creationTimestamp":"2021-11-21T18:04:52","epgSourceId":8,"startDateStr":"20211122070000","genreByGenreId":null,"languageByLanguageId":{"id":3,"name":"\u10e0\u10e3\u10e1\u10e3\u10da\u10d8","orderIndex":3,"nameShort":"ru"},"externalId":"2021460000084132","programHumanById":[],"date":null,"time":null,"startDate":null,"endDate":null,"longInfo":"\u0413\u0443\u0434\u0436\u0430\u0440\u0430\u0442\u0435\u0446 \u0425\u0430\u0441\u043c\u0443\u043a\u0445 \u041f\u0430\u0442\u0435\u043b \u043f\u043e\u0441\u0441\u043e\u0440\u0438\u043b\u0441\u044f \u0441 \u043d\u043e\u0432\u044b\u043c \u0441\u043e\u0441\u0435\u0434\u043e\u043c \u0413\u0443\u0433\u0433\u0438 \u0422\u0430\u043d\u0434\u043e\u043d\u043e\u043c. \u041d\u043e \u0438\u043c \u043f\u0440\u0438\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u043f\u043e\u043c\u0438\u0440\u0438\u0442\u044c\u0441\u044f, \u043a\u043e\u0433\u0434\u0430 \u0438\u0445 \u0434\u0435\u0442\u0438 \u0432\u043b\u044e\u0431\u043b\u044f\u044e\u0442\u0441\u044f \u0434\u0440\u0443\u0433 \u0432 \u0434\u0440\u0443\u0433\u0430. \u0420\u0435\u0436\u0438\u0441\u0441\u0435\u0440: \u0421\u0430\u043d\u0434\u0436\u0430\u0439 \u0427\u0445\u0435\u043b. \u0410\u043a\u0442\u0435\u0440\u044b: \u0420\u0438\u0448\u0438 \u041a\u0430\u043f\u0443\u0440, \u041f\u0430\u0440\u0435\u0448 \u0420\u0430\u0432\u0430\u043b, \u0412\u0438\u0440 \u0414\u0430\u0441. 2017 \u0433\u043e\u0434."}] \ No newline at end of file diff --git a/sites/magticom.ge/magticom.ge.config.js b/sites/magticom.ge/magticom.ge.config.js index 2a936ba1..3611e078 100644 --- a/sites/magticom.ge/magticom.ge.config.js +++ b/sites/magticom.ge/magticom.ge.config.js @@ -1,86 +1,86 @@ -const cheerio = require('cheerio') -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') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'magticom.ge', - days: 2, - url: 'https://www.magticom.ge/request/channel-program.php', - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - Referer: 'https://www.magticom.ge/en/tv/tv-services/tv-guide' - }, - data({ channel, date }) { - const params = new URLSearchParams() - params.append('channelId', channel.site_id) - params.append('start', date.unix()) - params.append('end', date.add(1, 'd').unix()) - - return params - } - }, - parser({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.info, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const html = await axios - .get('https://www.magticom.ge/en/tv/tv-services/tv-guide') - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(html) - const channels = $( - '#article > article > div > div > div.tv-guide > div.tv-guide-channels > div.tv-guide-channel' - ).toArray() - - return channels.map(item => { - const $item = cheerio.load(item) - const channelId = $item('*').data('id') - return { - lang: 'ka', - site_id: channelId, - name: $item('.tv-guide-channel-title > div > div').text() - } - }) - } -} - -function parseStart(item) { - return dayjs.tz(item.startTimestamp, 'YYYY-MM-DDTHH:mm:ss', 'Asia/Tbilisi') -} - -function parseStop(item) { - return dayjs.tz(item.endTimestamp, 'YYYY-MM-DDTHH:mm:ss', 'Asia/Tbilisi') -} - -function parseItems(content) { - let data - try { - data = JSON.parse(content) - } catch { - return [] - } - if (!data || !Array.isArray(data)) return [] - - return data -} +const cheerio = require('cheerio') +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') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'magticom.ge', + days: 2, + url: 'https://www.magticom.ge/request/channel-program.php', + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + Referer: 'https://www.magticom.ge/en/tv/tv-services/tv-guide' + }, + data({ channel, date }) { + const params = new URLSearchParams() + params.append('channelId', channel.site_id) + params.append('start', date.unix()) + params.append('end', date.add(1, 'd').unix()) + + return params + } + }, + parser({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.info, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const html = await axios + .get('https://www.magticom.ge/en/tv/tv-services/tv-guide') + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(html) + const channels = $( + '#article > article > div > div > div.tv-guide > div.tv-guide-channels > div.tv-guide-channel' + ).toArray() + + return channels.map(item => { + const $item = cheerio.load(item) + const channelId = $item('*').data('id') + return { + lang: 'ka', + site_id: channelId, + name: $item('.tv-guide-channel-title > div > div').text() + } + }) + } +} + +function parseStart(item) { + return dayjs.tz(item.startTimestamp, 'YYYY-MM-DDTHH:mm:ss', 'Asia/Tbilisi') +} + +function parseStop(item) { + return dayjs.tz(item.endTimestamp, 'YYYY-MM-DDTHH:mm:ss', 'Asia/Tbilisi') +} + +function parseItems(content) { + let data + try { + data = JSON.parse(content) + } catch { + return [] + } + if (!data || !Array.isArray(data)) return [] + + return data +} diff --git a/sites/magticom.ge/magticom.ge.test.js b/sites/magticom.ge/magticom.ge.test.js index 59d4dee0..ce78191a 100644 --- a/sites/magticom.ge/magticom.ge.test.js +++ b/sites/magticom.ge/magticom.ge.test.js @@ -1,63 +1,64 @@ -const { parser, url, request } = require('./magticom.ge.config.js') -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('2021-11-22', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '260', - xmltv_id: 'BollywoodHDRussia.ru' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.magticom.ge/request/channel-program.php') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - Referer: 'https://www.magticom.ge/en/tv/tv-services/tv-guide' - }) -}) - -it('can generate valid request data', () => { - const result = request.data({ channel, date }) - expect(result.has('channelId')).toBe(true) - expect(result.has('start')).toBe(true) - expect(result.has('end')).toBe(true) -}) - -it('can parse response', () => { - const content = - '[{"id":2313254118,"channelId":260,"startTimestamp":"2021-11-22T07:00:00","endTimestamp":"2021-11-22T09:00:00","duration":null,"title":"\\u0425\\/\\u0444 \\"\\u041d\\u0435\\u0440\\u0430\\u0432\\u043d\\u044b\\u0439 \\u0431\\u0440\\u0430\\u043a\\".","subTitle":"\\u0425\\/\\u0444 \\"\\u041d\\u0435\\u0440\\u0430\\u0432\\u043d\\u044b\\u0439 \\u0431\\u0440\\u0430\\u043a\\".","info":"\\u0413\\u0443\\u0434\\u0436\\u0430\\u0440\\u0430\\u0442\\u0435\\u0446 \\u0425\\u0430\\u0441\\u043c\\u0443\\u043a\\u0445 \\u041f\\u0430\\u0442\\u0435\\u043b \\u043f\\u043e\\u0441\\u0441\\u043e\\u0440\\u0438\\u043b\\u0441\\u044f \\u0441 \\u043d\\u043e\\u0432\\u044b\\u043c \\u0441\\u043e\\u0441\\u0435\\u0434\\u043e\\u043c \\u0413\\u0443\\u0433\\u0433\\u0438 \\u0422\\u0430\\u043d\\u0434\\u043e\\u043d\\u043e\\u043c. \\u041d\\u043e \\u0438\\u043c \\u043f\\u0440\\u0438\\u0445\\u043e\\u0434\\u0438\\u0442\\u0441\\u044f \\u043f\\u043e\\u043c\\u0438\\u0440\\u0438\\u0442\\u044c\\u0441\\u044f, \\u043a\\u043e\\u0433\\u0434\\u0430 \\u0438\\u0445 \\u0434\\u0435\\u0442\\u0438 \\u0432\\u043b\\u044e\\u0431\\u043b\\u044f\\u044e\\u0442\\u0441\\u044f \\u0434\\u0440\\u0443\\u0433 \\u0432 \\u0434\\u0440\\u0443\\u0433\\u0430. \\u0420\\u0435\\u0436\\u0438\\u0441\\u0441\\u0435\\u0440: \\u0421\\u0430\\u043d\\u0434\\u0436\\u0430\\u0439 \\u0427\\u0445\\u0435\\u043b. \\u0410\\u043a\\u0442\\u0435\\u0440\\u044b: \\u0420\\u0438\\u0448\\u0438 \\u041a\\u0430\\u043f\\u0443\\u0440, \\u041f\\u0430\\u0440\\u0435\\u0448 \\u0420\\u0430\\u0432\\u0430\\u043b, \\u0412\\u0438\\u0440 \\u0414\\u0430\\u0441. 2017 \\u0433\\u043e\\u0434.","pg":null,"year":null,"country":null,"imageUrl":null,"createdBy":-200,"creationTimestamp":"2021-11-21T18:04:52","epgSourceId":8,"startDateStr":"20211122070000","genreByGenreId":null,"languageByLanguageId":{"id":3,"name":"\\u10e0\\u10e3\\u10e1\\u10e3\\u10da\\u10d8","orderIndex":3,"nameShort":"ru"},"externalId":"2021460000084132","programHumanById":[],"date":null,"time":null,"startDate":null,"endDate":null,"longInfo":"\\u0413\\u0443\\u0434\\u0436\\u0430\\u0440\\u0430\\u0442\\u0435\\u0446 \\u0425\\u0430\\u0441\\u043c\\u0443\\u043a\\u0445 \\u041f\\u0430\\u0442\\u0435\\u043b \\u043f\\u043e\\u0441\\u0441\\u043e\\u0440\\u0438\\u043b\\u0441\\u044f \\u0441 \\u043d\\u043e\\u0432\\u044b\\u043c \\u0441\\u043e\\u0441\\u0435\\u0434\\u043e\\u043c \\u0413\\u0443\\u0433\\u0433\\u0438 \\u0422\\u0430\\u043d\\u0434\\u043e\\u043d\\u043e\\u043c. \\u041d\\u043e \\u0438\\u043c \\u043f\\u0440\\u0438\\u0445\\u043e\\u0434\\u0438\\u0442\\u0441\\u044f \\u043f\\u043e\\u043c\\u0438\\u0440\\u0438\\u0442\\u044c\\u0441\\u044f, \\u043a\\u043e\\u0433\\u0434\\u0430 \\u0438\\u0445 \\u0434\\u0435\\u0442\\u0438 \\u0432\\u043b\\u044e\\u0431\\u043b\\u044f\\u044e\\u0442\\u0441\\u044f \\u0434\\u0440\\u0443\\u0433 \\u0432 \\u0434\\u0440\\u0443\\u0433\\u0430. \\u0420\\u0435\\u0436\\u0438\\u0441\\u0441\\u0435\\u0440: \\u0421\\u0430\\u043d\\u0434\\u0436\\u0430\\u0439 \\u0427\\u0445\\u0435\\u043b. \\u0410\\u043a\\u0442\\u0435\\u0440\\u044b: \\u0420\\u0438\\u0448\\u0438 \\u041a\\u0430\\u043f\\u0443\\u0440, \\u041f\\u0430\\u0440\\u0435\\u0448 \\u0420\\u0430\\u0432\\u0430\\u043b, \\u0412\\u0438\\u0440 \\u0414\\u0430\\u0441. 2017 \\u0433\\u043e\\u0434."}]' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-22T03:00:00.000Z', - stop: '2021-11-22T05:00:00.000Z', - title: 'Х/ф "Неравный брак".', - description: - 'Гуджаратец Хасмукх Пател поссорился с новым соседом Гугги Тандоном. Но им приходится помириться, когда их дети влюбляются друг в друга. Режиссер: Санджай Чхел. Актеры: Риши Капур, Пареш Равал, Вир Дас. 2017 год.' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '[]' - }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./magticom.ge.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('2021-11-22', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '260', + xmltv_id: 'BollywoodHDRussia.ru' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.magticom.ge/request/channel-program.php') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + Referer: 'https://www.magticom.ge/en/tv/tv-services/tv-guide' + }) +}) + +it('can generate valid request data', () => { + const result = request.data({ channel, date }) + expect(result.has('channelId')).toBe(true) + expect(result.has('start')).toBe(true) + expect(result.has('end')).toBe(true) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-22T03:00:00.000Z', + stop: '2021-11-22T05:00:00.000Z', + title: 'Х/ф "Неравный брак".', + description: + 'Гуджаратец Хасмукх Пател поссорился с новым соседом Гугги Тандоном. Но им приходится помириться, когда их дети влюбляются друг в друга. Режиссер: Санджай Чхел. Актеры: Риши Капур, Пареш Равал, Вир Дас. 2017 год.' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '[]' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mako.co.il/__data__/content.json b/sites/mako.co.il/__data__/content.json new file mode 100644 index 00000000..f48d561b --- /dev/null +++ b/sites/mako.co.il/__data__/content.json @@ -0,0 +1 @@ +{"programs":[{"DisplayEndTime":"06:15","MakoTVURL":"","HouseNumber":"L17165475","StartTimeUTC":1646539200000,"DurationMs":900000,"DisplayStartTime":"06:00","MobilePicture":"https://img.mako.co.il/2017/01/01/placeHolder.jpg","StartTime":"06/03/2022 06:00:00","RerunBroadcast":false,"Duration":"00:15","ProgramName":"כותרות הבוקר","Date":"06/03/2022 06:00:00","MakoProgramsURL":"","LiveBroadcast":true,"ProgramCode":134987,"Episode":"","Picture":"https://img.mako.co.il//2021/08/04/hadshot_haboker_im_niv_raskin.jpg","MakoShortName":"","hebrewDate":"6 במרץ","Season":"","day":"הערב","EventDescription":"","EnglishName":"cotrot,EP 46"},{"DisplayEndTime":"02:39","MakoTVURL":"","HouseNumber":"A168960","StartTimeUTC":1646613480000,"DurationMs":60000,"DisplayStartTime":"02:38","MobilePicture":"https://img.mako.co.il/2017/01/01/placeHolder.jpg","StartTime":"07/03/2022 02:38:00","RerunBroadcast":true,"Duration":"00:01","ProgramName":"רוקדים עם כוכבים - בר זומר","Date":"07/03/2022 02:38:00","MakoProgramsURL":"","LiveBroadcast":false,"ProgramCode":135029,"Episode":"","Picture":"https://img.mako.co.il/2022/02/13/DancingWithStars2022_EPG.jpg","MakoShortName":"","hebrewDate":"7 במרץ","Season":"","day":"מחר","EventDescription":"מהדורת החדשות המרכזית של הבוקר, האנשים הפרשנויות והכותרות שיעשו את היום.","EnglishName":"rokdim,EP 10"}]} \ No newline at end of file diff --git a/sites/mako.co.il/mako.co.il.config.js b/sites/mako.co.il/mako.co.il.config.js index 0a6a0b19..08ff9d71 100644 --- a/sites/mako.co.il/mako.co.il.config.js +++ b/sites/mako.co.il/mako.co.il.config.js @@ -1,45 +1,45 @@ -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: 'mako.co.il', - days: 2, - url: 'https://www.mako.co.il/AjaxPage?jspName=EPGResponse.jsp', - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const start = parseStart(item) - const stop = start.add(item.DurationMs, 'ms') - programs.push({ - title: item.ProgramName, - description: item.EventDescription, - image: item.Picture, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item) { - if (!item.StartTimeUTC) return null - - return dayjs(item.StartTimeUTC) -} - -function parseItems(content, date) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.programs)) return [] - const d = date.format('DD/MM/YYYY') - - return data.programs.filter(item => item.Date.startsWith(d)) -} +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: 'mako.co.il', + days: 2, + url: 'https://www.mako.co.il/AjaxPage?jspName=EPGResponse.jsp', + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const start = parseStart(item) + const stop = start.add(item.DurationMs, 'ms') + programs.push({ + title: item.ProgramName, + description: item.EventDescription, + image: item.Picture, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item) { + if (!item.StartTimeUTC) return null + + return dayjs(item.StartTimeUTC) +} + +function parseItems(content, date) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.programs)) return [] + const d = date.format('DD/MM/YYYY') + + return data.programs.filter(item => item.Date.startsWith(d)) +} diff --git a/sites/mako.co.il/mako.co.il.test.js b/sites/mako.co.il/mako.co.il.test.js index 70f1586e..bba70e6a 100644 --- a/sites/mako.co.il/mako.co.il.test.js +++ b/sites/mako.co.il/mako.co.il.test.js @@ -1,40 +1,41 @@ -const { parser, url } = require('./mako.co.il.config.js') -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('2022-03-07', 'YYYY-MM-DD').startOf('d') - -it('can generate valid url', () => { - expect(url).toBe('https://www.mako.co.il/AjaxPage?jspName=EPGResponse.jsp') -}) - -it('can parse response', () => { - const content = - '{"programs":[{"DisplayEndTime":"06:15","MakoTVURL":"","HouseNumber":"L17165475","StartTimeUTC":1646539200000,"DurationMs":900000,"DisplayStartTime":"06:00","MobilePicture":"https://img.mako.co.il/2017/01/01/placeHolder.jpg","StartTime":"06/03/2022 06:00:00","RerunBroadcast":false,"Duration":"00:15","ProgramName":"כותרות הבוקר","Date":"06/03/2022 06:00:00","MakoProgramsURL":"","LiveBroadcast":true,"ProgramCode":134987,"Episode":"","Picture":"https://img.mako.co.il//2021/08/04/hadshot_haboker_im_niv_raskin.jpg","MakoShortName":"","hebrewDate":"6 במרץ","Season":"","day":"הערב","EventDescription":"","EnglishName":"cotrot,EP 46"},{"DisplayEndTime":"02:39","MakoTVURL":"","HouseNumber":"A168960","StartTimeUTC":1646613480000,"DurationMs":60000,"DisplayStartTime":"02:38","MobilePicture":"https://img.mako.co.il/2017/01/01/placeHolder.jpg","StartTime":"07/03/2022 02:38:00","RerunBroadcast":true,"Duration":"00:01","ProgramName":"רוקדים עם כוכבים - בר זומר","Date":"07/03/2022 02:38:00","MakoProgramsURL":"","LiveBroadcast":false,"ProgramCode":135029,"Episode":"","Picture":"https://img.mako.co.il/2022/02/13/DancingWithStars2022_EPG.jpg","MakoShortName":"","hebrewDate":"7 במרץ","Season":"","day":"מחר","EventDescription":"מהדורת החדשות המרכזית של הבוקר, האנשים הפרשנויות והכותרות שיעשו את היום.","EnglishName":"rokdim,EP 10"}]}' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-07T00:38:00.000Z', - stop: '2022-03-07T00:39:00.000Z', - title: 'רוקדים עם כוכבים - בר זומר', - description: 'מהדורת החדשות המרכזית של הבוקר, האנשים הפרשנויות והכותרות שיעשו את היום.', - image: 'https://img.mako.co.il/2022/02/13/DancingWithStars2022_EPG.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '[]', - date - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./mako.co.il.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('2022-03-07', 'YYYY-MM-DD').startOf('d') + +it('can generate valid url', () => { + expect(url).toBe('https://www.mako.co.il/AjaxPage?jspName=EPGResponse.jsp') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-07T00:38:00.000Z', + stop: '2022-03-07T00:39:00.000Z', + title: 'רוקדים עם כוכבים - בר זומר', + description: 'מהדורת החדשות המרכזית של הבוקר, האנשים הפרשנויות והכותרות שיעשו את היום.', + image: 'https://img.mako.co.il/2022/02/13/DancingWithStars2022_EPG.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '[]', + date + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/makrodigitaltelevision.com/makrodigitaltelevision.com.config.js b/sites/makrodigitaltelevision.com/makrodigitaltelevision.com.config.js index 8a35a105..7a358823 100644 --- a/sites/makrodigitaltelevision.com/makrodigitaltelevision.com.config.js +++ b/sites/makrodigitaltelevision.com/makrodigitaltelevision.com.config.js @@ -1,26 +1,26 @@ -const parser = require('epg-parser') - -module.exports = { - site: 'makrodigitaltelevision.com', - days: 3, - url: 'https://makrodigitaltelevision.com/epg.xml', - parser({ content, date, channel }) { - let programs = [] - const items = parseItems(content, channel, date) - items.forEach(item => { - programs.push({ - title: item.title?.[0]?.value, - start: item.start, - stop: item.stop - }) - }) - - return programs - } -} - -function parseItems(content, channel, date) { - const { programs } = parser.parse(content) - - return programs.filter(p => p.channel === channel.site_id && date.isSame(p.start, 'day')) -} +const parser = require('epg-parser') + +module.exports = { + site: 'makrodigitaltelevision.com', + days: 3, + url: 'https://makrodigitaltelevision.com/epg.xml', + parser({ content, date, channel }) { + let programs = [] + const items = parseItems(content, channel, date) + items.forEach(item => { + programs.push({ + title: item.title?.[0]?.value, + start: item.start, + stop: item.stop + }) + }) + + return programs + } +} + +function parseItems(content, channel, date) { + const { programs } = parser.parse(content) + + return programs.filter(p => p.channel === channel.site_id && date.isSame(p.start, 'day')) +} diff --git a/sites/makrodigitaltelevision.com/makrodigitaltelevision.com.test.js b/sites/makrodigitaltelevision.com/makrodigitaltelevision.com.test.js index 56de5851..4c559b08 100644 --- a/sites/makrodigitaltelevision.com/makrodigitaltelevision.com.test.js +++ b/sites/makrodigitaltelevision.com/makrodigitaltelevision.com.test.js @@ -1,39 +1,39 @@ -const { parser, url } = require('./makrodigitaltelevision.com.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-02-16', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: '17' } - -it('can generate valid url', () => { - expect(url).toBe('https://makrodigitaltelevision.com/epg.xml') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml')) - - const results = parser({ content, channel, date }) - - expect(results.length).toBe(8) - expect(results[0]).toMatchObject({ - title: 'Programación Infantil', - start: '2025-02-16T13:00:00.000Z', - stop: '2025-02-16T17:00:00.000Z' - }) - expect(results[7]).toMatchObject({ - title: 'Comunicación Cristiana', - start: '2025-02-16T23:30:00.000Z', - stop: '2025-02-17T00:00:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ content: '' }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./makrodigitaltelevision.com.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-02-16', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: '17' } + +it('can generate valid url', () => { + expect(url).toBe('https://makrodigitaltelevision.com/epg.xml') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml')) + + const results = parser({ content, channel, date }) + + expect(results.length).toBe(8) + expect(results[0]).toMatchObject({ + title: 'Programación Infantil', + start: '2025-02-16T13:00:00.000Z', + stop: '2025-02-16T17:00:00.000Z' + }) + expect(results[7]).toMatchObject({ + title: 'Comunicación Cristiana', + start: '2025-02-16T23:30:00.000Z', + stop: '2025-02-17T00:00:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ content: '' }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/maxtvgo.mk/__data__/content.json b/sites/maxtvgo.mk/__data__/content.json new file mode 100644 index 00000000..9c159231 --- /dev/null +++ b/sites/maxtvgo.mk/__data__/content.json @@ -0,0 +1 @@ +{"programme":[{"@attributes":{"channel":"105","id":"21949063","start":"20211116231000 +0100","stop":"20211117010000 +0100","disable_catchup":"0","is_adult":"0"},"title":"Палмето - игран филм","original-title":{"@attributes":{"lang":""}},"sub-title":{"@attributes":{"lang":""}},"category_id":"11","category":"Останато","desc":"Екстремниот рибар, Џереми Вејд, е во потрага по слатководни риби кои јадат човечко месо. Со форензички методи, Џереми им илустрира на гледачите како овие нови чудовишта се создадени да убиваат.","icon":{"@attributes":{"src":"https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1"}},"episode_num":{},"date":"0","star-rating":{"value":{}},"rating":{"@attributes":{"system":""},"value":"0+"},"linear_channel_rating":"0+","genres":{},"credits":{}}]} \ No newline at end of file diff --git a/sites/maxtvgo.mk/__data__/content_no_description.json b/sites/maxtvgo.mk/__data__/content_no_description.json new file mode 100644 index 00000000..ed5f4999 --- /dev/null +++ b/sites/maxtvgo.mk/__data__/content_no_description.json @@ -0,0 +1 @@ +{"programme":[{"@attributes":{"channel":"105","id":"21949063","start":"20211116231000 +0100","stop":"20211117010000 +0100","disable_catchup":"0","is_adult":"0"},"title":"Палмето - игран филм","original-title":{"@attributes":{"lang":""}},"sub-title":{"@attributes":{"lang":""}},"category_id":"11","category":"Останато","desc":{},"icon":{"@attributes":{"src":"https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1"}},"episode_num":{},"date":"0","star-rating":{"value":{}},"rating":{"@attributes":{"system":""},"value":"0+"},"linear_channel_rating":"0+","genres":{},"credits":{}}]} \ No newline at end of file diff --git a/sites/maxtvgo.mk/__data__/no_content.json b/sites/maxtvgo.mk/__data__/no_content.json new file mode 100644 index 00000000..d90445ea --- /dev/null +++ b/sites/maxtvgo.mk/__data__/no_content.json @@ -0,0 +1 @@ +{"@attributes":{"source-info-name":"maxtvgo.mk","generator-info-name":"spectar_epg"}} \ No newline at end of file diff --git a/sites/maxtvgo.mk/maxtvgo.mk.config.js b/sites/maxtvgo.mk/maxtvgo.mk.config.js index 9573d184..b553f728 100644 --- a/sites/maxtvgo.mk/maxtvgo.mk.config.js +++ b/sites/maxtvgo.mk/maxtvgo.mk.config.js @@ -1,72 +1,72 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(customParseFormat) - -module.exports = { - site: 'maxtvgo.mk', - days: 2, - url: function ({ channel, date }) { - return `https://prd-static-mkt.spectar.tv/rev-1636968171/client_api.php/epg/list/instance_id/1/language/mk/channel_id/${ - channel.site_id - }/start/${date.format('YYYYMMDDHHmmss')}/stop/${date - .add(1, 'd') - .format('YYYYMMDDHHmmss')}/include_current/true/format/json` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - category: item.category, - description: parseDescription(item), - image: parseImage(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const channels = await axios - .get( - 'https://prd-static-mkt.spectar.tv/rev-1636968171/client_api.php/channel/all/application_id/deep_blue/device_configuration/2/instance_id/1/language/mk/http_proto/https/format/json' - ) - .then(r => r.data) - .catch(console.log) - - return channels.map(item => { - return { - lang: 'mk', - site_id: item.id, - name: item.name - } - }) - } -} - -function parseStart(item) { - return dayjs(item['@attributes'].start, 'YYYYMMDDHHmmss ZZ') -} - -function parseStop(item) { - return dayjs(item['@attributes'].stop, 'YYYYMMDDHHmmss ZZ') -} - -function parseDescription(item) { - return typeof item.desc === 'string' ? item.desc : null -} - -function parseImage(item) { - return item.icon['@attributes'].src -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.programme)) return [] - - return data.programme -} +const axios = require('axios') +const dayjs = require('dayjs') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(customParseFormat) + +module.exports = { + site: 'maxtvgo.mk', + days: 2, + url: function ({ channel, date }) { + return `https://prd-static-mkt.spectar.tv/rev-1636968171/client_api.php/epg/list/instance_id/1/language/mk/channel_id/${ + channel.site_id + }/start/${date.format('YYYYMMDDHHmmss')}/stop/${date + .add(1, 'd') + .format('YYYYMMDDHHmmss')}/include_current/true/format/json` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + category: item.category, + description: parseDescription(item), + image: parseImage(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const channels = await axios + .get( + 'https://prd-static-mkt.spectar.tv/rev-1636968171/client_api.php/channel/all/application_id/deep_blue/device_configuration/2/instance_id/1/language/mk/http_proto/https/format/json' + ) + .then(r => r.data) + .catch(console.log) + + return channels.map(item => { + return { + lang: 'mk', + site_id: item.id, + name: item.name + } + }) + } +} + +function parseStart(item) { + return dayjs(item['@attributes'].start, 'YYYYMMDDHHmmss ZZ') +} + +function parseStop(item) { + return dayjs(item['@attributes'].stop, 'YYYYMMDDHHmmss ZZ') +} + +function parseDescription(item) { + return typeof item.desc === 'string' ? item.desc : null +} + +function parseImage(item) { + return item.icon['@attributes'].src +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.programme)) return [] + + return data.programme +} diff --git a/sites/maxtvgo.mk/maxtvgo.mk.test.js b/sites/maxtvgo.mk/maxtvgo.mk.test.js index f218c3ba..bb813b28 100644 --- a/sites/maxtvgo.mk/maxtvgo.mk.test.js +++ b/sites/maxtvgo.mk/maxtvgo.mk.test.js @@ -1,72 +1,72 @@ -const { parser, url } = require('./maxtvgo.mk.config.js') -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('2021-11-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '105', - xmltv_id: 'MRT1.mk' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://prd-static-mkt.spectar.tv/rev-1636968171/client_api.php/epg/list/instance_id/1/language/mk/channel_id/105/start/20211117000000/stop/20211118000000/include_current/true/format/json' - ) -}) - -it('can parse response', () => { - const content = - '{"programme":[{"@attributes":{"channel":"105","id":"21949063","start":"20211116231000 +0100","stop":"20211117010000 +0100","disable_catchup":"0","is_adult":"0"},"title":"Палмето - игран филм","original-title":{"@attributes":{"lang":""}},"sub-title":{"@attributes":{"lang":""}},"category_id":"11","category":"Останато","desc":"Екстремниот рибар, Џереми Вејд, е во потрага по слатководни риби кои јадат човечко месо. Со форензички методи, Џереми им илустрира на гледачите како овие нови чудовишта се создадени да убиваат.","icon":{"@attributes":{"src":"https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1"}},"episode_num":{},"date":"0","star-rating":{"value":{}},"rating":{"@attributes":{"system":""},"value":"0+"},"linear_channel_rating":"0+","genres":{},"credits":{}}]}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-16T22:10:00.000Z', - stop: '2021-11-17T00:00:00.000Z', - title: 'Палмето - игран филм', - category: 'Останато', - description: - 'Екстремниот рибар, Џереми Вејд, е во потрага по слатководни риби кои јадат човечко месо. Со форензички методи, Џереми им илустрира на гледачите како овие нови чудовишта се создадени да убиваат.', - image: - 'https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1' - } - ]) -}) - -it('can parse response with no description', () => { - const content = - '{"programme":[{"@attributes":{"channel":"105","id":"21949063","start":"20211116231000 +0100","stop":"20211117010000 +0100","disable_catchup":"0","is_adult":"0"},"title":"Палмето - игран филм","original-title":{"@attributes":{"lang":""}},"sub-title":{"@attributes":{"lang":""}},"category_id":"11","category":"Останато","desc":{},"icon":{"@attributes":{"src":"https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1"}},"episode_num":{},"date":"0","star-rating":{"value":{}},"rating":{"@attributes":{"system":""},"value":"0+"},"linear_channel_rating":"0+","genres":{},"credits":{}}]}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-16T22:10:00.000Z', - stop: '2021-11-17T00:00:00.000Z', - title: 'Палмето - игран филм', - category: 'Останато', - description: null, - image: - 'https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"@attributes":{"source-info-name":"maxtvgo.mk","generator-info-name":"spectar_epg"}}' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./maxtvgo.mk.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('2021-11-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '105', + xmltv_id: 'MRT1.mk' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://prd-static-mkt.spectar.tv/rev-1636968171/client_api.php/epg/list/instance_id/1/language/mk/channel_id/105/start/20211117000000/stop/20211118000000/include_current/true/format/json' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-16T22:10:00.000Z', + stop: '2021-11-17T00:00:00.000Z', + title: 'Палмето - игран филм', + category: 'Останато', + description: + 'Екстремниот рибар, Џереми Вејд, е во потрага по слатководни риби кои јадат човечко месо. Со форензички методи, Џереми им илустрира на гледачите како овие нови чудовишта се создадени да убиваат.', + image: + 'https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1' + } + ]) +}) + +it('can parse response with no description', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_no_description.json')) + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-16T22:10:00.000Z', + stop: '2021-11-17T00:00:00.000Z', + title: 'Палмето - игран филм', + category: 'Останато', + description: null, + image: + 'https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mediagenie.co.kr/mediagenie.co.kr.config.js b/sites/mediagenie.co.kr/mediagenie.co.kr.config.js index 35fcb007..32f85cbe 100644 --- a/sites/mediagenie.co.kr/mediagenie.co.kr.config.js +++ b/sites/mediagenie.co.kr/mediagenie.co.kr.config.js @@ -1,77 +1,77 @@ -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: 'mediagenie.co.kr', - days: 1, - url({ channel, date }) { - return `https://mediagenie.co.kr/${channel.site_id}/?qd=${date.format('YYYYMMDD')}` - }, - request: { - headers: { - cookie: 'CUPID=d5ed6b77012aef2b4d4365ffd3a1a3a4' - } - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (!start) return - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - rating: parseRating($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('.col2').clone().children().remove().end().text().trim() -} - -function parseRating($item) { - const rating = $item('.col6').text().trim() - - return rating - ? { - system: 'KMRB', - value: rating - } - : null -} - -function parseStart($item, date) { - const time = $item('.col1').text().trim() - - if (!/^\d{2}:\d{2}$/.test(time)) return null - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Seoul') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.tbl > tbody > tr').toArray() -} +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: 'mediagenie.co.kr', + days: 1, + url({ channel, date }) { + return `https://mediagenie.co.kr/${channel.site_id}/?qd=${date.format('YYYYMMDD')}` + }, + request: { + headers: { + cookie: 'CUPID=d5ed6b77012aef2b4d4365ffd3a1a3a4' + } + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (!start) return + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + rating: parseRating($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('.col2').clone().children().remove().end().text().trim() +} + +function parseRating($item) { + const rating = $item('.col6').text().trim() + + return rating + ? { + system: 'KMRB', + value: rating + } + : null +} + +function parseStart($item, date) { + const time = $item('.col1').text().trim() + + if (!/^\d{2}:\d{2}$/.test(time)) return null + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Seoul') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.tbl > tbody > tr').toArray() +} diff --git a/sites/mediagenie.co.kr/mediagenie.co.kr.test.js b/sites/mediagenie.co.kr/mediagenie.co.kr.test.js index 05315da5..44f4f9e8 100644 --- a/sites/mediagenie.co.kr/mediagenie.co.kr.test.js +++ b/sites/mediagenie.co.kr/mediagenie.co.kr.test.js @@ -1,63 +1,63 @@ -const { parser, url, request } = require('./mediagenie.co.kr.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('2023-01-25', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'ENA_DRAMA', - xmltv_id: 'ENADRAMA.kr' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://mediagenie.co.kr/ENA_DRAMA/?qd=20230125') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - cookie: 'CUPID=d5ed6b77012aef2b4d4365ffd3a1a3a4' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - 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: '2023-01-24T15:20:00.000Z', - stop: '2023-01-24T16:34:00.000Z', - title: '대행사', - rating: { - system: 'KMRB', - value: '15' - } - }) - - expect(results[16]).toMatchObject({ - start: '2023-01-25T14:27:00.000Z', - stop: '2023-01-25T14:57:00.000Z', - title: '법쩐', - rating: { - system: 'KMRB', - value: '15' - } - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - }) - - expect(results).toMatchObject([]) -}) +const { parser, url, request } = require('./mediagenie.co.kr.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('2023-01-25', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'ENA_DRAMA', + xmltv_id: 'ENADRAMA.kr' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://mediagenie.co.kr/ENA_DRAMA/?qd=20230125') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + cookie: 'CUPID=d5ed6b77012aef2b4d4365ffd3a1a3a4' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + 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: '2023-01-24T15:20:00.000Z', + stop: '2023-01-24T16:34:00.000Z', + title: '대행사', + rating: { + system: 'KMRB', + value: '15' + } + }) + + expect(results[16]).toMatchObject({ + start: '2023-01-25T14:27:00.000Z', + stop: '2023-01-25T14:57:00.000Z', + title: '법쩐', + rating: { + system: 'KMRB', + value: '15' + } + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/mediaklikk.hu/mediaklikk.hu.config.js b/sites/mediaklikk.hu/mediaklikk.hu.config.js index 673fb4b3..1a6bf2b6 100644 --- a/sites/mediaklikk.hu/mediaklikk.hu.config.js +++ b/sites/mediaklikk.hu/mediaklikk.hu.config.js @@ -1,85 +1,85 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'mediaklikk.hu', - days: 2, - url: 'https://mediaklikk.hu/wp-content/plugins/hms-global-widgets/widgets/programGuide/programGuideInterface.php', - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - data: function ({ date, channel }) { - const params = new URLSearchParams() - params.append('ChannelIds', `${channel.site_id},`) - params.append('Date', date.format('YYYY-MM-DD')) - - return params - } - }, - parser: function ({ content }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const start = parseStart($item) - let stop = parseStop($item) - if (!stop) stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - image: parseImage($item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart($item) { - const timeString = $item('*').data('from') - - return dayjs(timeString, 'YYYY-MM-DD HH:mm:ssZZ') -} - -function parseStop($item) { - const timeString = $item('*').data('till') - if (!timeString || /^\+/.test(timeString)) return null - - try { - return dayjs(timeString, 'YYYY-MM-DD HH:mm:ssZZ') - } catch { - return null - } -} - -function parseTitle($item) { - return $item('.program_info > h1').text().trim() -} - -function parseDescription($item) { - return $item('.program_about > .program_description > p').text().trim() -} - -function parseImage($item) { - const backgroundImage = $item('.program_about > .program_photo').css('background-image') - if (!backgroundImage) return null - const [, imageUrl] = backgroundImage.match(/url\('(.*)'\)/) || [null, null] - if (!imageUrl) return null - - return `https:${imageUrl}` -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('li.program_body').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'mediaklikk.hu', + days: 2, + url: 'https://mediaklikk.hu/wp-content/plugins/hms-global-widgets/widgets/programGuide/programGuideInterface.php', + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + data: function ({ date, channel }) { + const params = new URLSearchParams() + params.append('ChannelIds', `${channel.site_id},`) + params.append('Date', date.format('YYYY-MM-DD')) + + return params + } + }, + parser: function ({ content }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const start = parseStart($item) + let stop = parseStop($item) + if (!stop) stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + image: parseImage($item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart($item) { + const timeString = $item('*').data('from') + + return dayjs(timeString, 'YYYY-MM-DD HH:mm:ssZZ') +} + +function parseStop($item) { + const timeString = $item('*').data('till') + if (!timeString || /^\+/.test(timeString)) return null + + try { + return dayjs(timeString, 'YYYY-MM-DD HH:mm:ssZZ') + } catch { + return null + } +} + +function parseTitle($item) { + return $item('.program_info > h1').text().trim() +} + +function parseDescription($item) { + return $item('.program_about > .program_description > p').text().trim() +} + +function parseImage($item) { + const backgroundImage = $item('.program_about > .program_photo').css('background-image') + if (!backgroundImage) return null + const [, imageUrl] = backgroundImage.match(/url\('(.*)'\)/) || [null, null] + if (!imageUrl) return null + + return `https:${imageUrl}` +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('li.program_body').toArray() +} diff --git a/sites/mediaklikk.hu/mediaklikk.hu.test.js b/sites/mediaklikk.hu/mediaklikk.hu.test.js index 7ab04fbe..f4dd06a1 100644 --- a/sites/mediaklikk.hu/mediaklikk.hu.test.js +++ b/sites/mediaklikk.hu/mediaklikk.hu.test.js @@ -1,72 +1,72 @@ -const { parser, url, request } = require('./mediaklikk.hu.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('2022-03-10', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '3', - xmltv_id: 'DuneTV.hu' -} - -it('can generate valid url', () => { - expect(url).toBe( - 'https://mediaklikk.hu/wp-content/plugins/hms-global-widgets/widgets/programGuide/programGuideInterface.php' - ) -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded' - }) -}) - -it('can generate valid request data', () => { - const result = request.data({ date, channel }) - expect(result.get('ChannelIds')).toBe('3,') - expect(result.get('Date')).toBe('2022-03-10') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-27T22:00:46.000Z', - stop: '2022-10-27T22:54:00.000Z', - title: 'A hegyi doktor - I. évad', - description: - 'Maxl iskolatársának, Vroninak az anyja egy autóbalesetben meghal. A 20 éves testvér, Vinzenz magához szeretné venni a lányt, ám a gyámüggyel problémái akadnak, ezért megpróbálja elszöktetni.(Eredeti hang digitálisan.)', - image: - 'https://mediaklikk.hu/wp-content/uploads/sites/4/2019/10/A-hegyi-doktor-I-évad-e1571318391226-150x150.jpg' - }) - - expect(results[56]).toMatchObject({ - start: '2022-10-28T20:35:05.000Z', - stop: '2022-10-28T21:05:05.000Z', - title: 'Szemtől szemben (1967)', - description: - 'Brad Fletcher bostoni történelemtanár, aki a délnyugati határvidéken kúrálja tüdőbetegségét, egy véletlen folytán összeakad Beauregard Bennett körözött útonállóval, akit végül maga segít a menekülésben. A tanárt lenyűgözi a törvényen kívüliek világa és felismeri, hogy értelmi felsőbbrendűségével bámulatosan tudja irányítani az embereket. Bennett csakhamar azt veszi észre, hogy a peremre szorult saját bandájában. Eközben a Pinkerton ügynökség beépített embere is csapdába igyekszik csalni mindnyájukat.' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./mediaklikk.hu.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('2022-03-10', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '3', + xmltv_id: 'DuneTV.hu' +} + +it('can generate valid url', () => { + expect(url).toBe( + 'https://mediaklikk.hu/wp-content/plugins/hms-global-widgets/widgets/programGuide/programGuideInterface.php' + ) +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded' + }) +}) + +it('can generate valid request data', () => { + const result = request.data({ date, channel }) + expect(result.get('ChannelIds')).toBe('3,') + expect(result.get('Date')).toBe('2022-03-10') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-27T22:00:46.000Z', + stop: '2022-10-27T22:54:00.000Z', + title: 'A hegyi doktor - I. évad', + description: + 'Maxl iskolatársának, Vroninak az anyja egy autóbalesetben meghal. A 20 éves testvér, Vinzenz magához szeretné venni a lányt, ám a gyámüggyel problémái akadnak, ezért megpróbálja elszöktetni.(Eredeti hang digitálisan.)', + image: + 'https://mediaklikk.hu/wp-content/uploads/sites/4/2019/10/A-hegyi-doktor-I-évad-e1571318391226-150x150.jpg' + }) + + expect(results[56]).toMatchObject({ + start: '2022-10-28T20:35:05.000Z', + stop: '2022-10-28T21:05:05.000Z', + title: 'Szemtől szemben (1967)', + description: + 'Brad Fletcher bostoni történelemtanár, aki a délnyugati határvidéken kúrálja tüdőbetegségét, egy véletlen folytán összeakad Beauregard Bennett körözött útonállóval, akit végül maga segít a menekülésben. A tanárt lenyűgözi a törvényen kívüliek világa és felismeri, hogy értelmi felsőbbrendűségével bámulatosan tudja irányítani az embereket. Bennett csakhamar azt veszi észre, hogy a peremre szorult saját bandájában. Eközben a Pinkerton ügynökség beépített embere is csapdába igyekszik csalni mindnyájukat.' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mediasetinfinity.mediaset.it/mediasetinfinity.mediaset.it.config.js b/sites/mediasetinfinity.mediaset.it/mediasetinfinity.mediaset.it.config.js index 3a168953..bcbda09a 100644 --- a/sites/mediasetinfinity.mediaset.it/mediasetinfinity.mediaset.it.config.js +++ b/sites/mediasetinfinity.mediaset.it/mediasetinfinity.mediaset.it.config.js @@ -1,97 +1,97 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) -dayjs.extend(timezone) - -module.exports = { - site: 'mediasetinfinity.mediaset.it', - days: 2, - url: function ({ date, channel }) { - // Get the epoch timestamp - const todayEpoch = date.startOf('day').utc().valueOf() - // Get the epoch timestamp for the next day - const nextDayEpoch = date.add(1, 'day').startOf('day').utc().valueOf() - return `https://api-ott-prod-fe.mediaset.net/PROD/play/feed/allListingFeedEpg/v2.0?byListingTime=${todayEpoch}~${nextDayEpoch}&byCallSign=${channel.site_id}` - }, - parser: function ({ content }) { - const programs = [] - const data = JSON.parse(content) - - if ( - !data.response || - !data.response.entries || - !data.response.entries[0] || - !data.response.entries[0].listings - ) { - // If the structure is not as expected, return an empty array - return programs - } - - const listings = data.response.entries[0].listings - - listings.forEach(listing => { - const title = listing.mediasetlisting$epgTitle - const subTitle = listing.program.title - const season = parseSeason(listing) - const episode = parseEpisode(listing) - - if (listing.program.title && listing.startTime && listing.endTime) { - programs.push({ - title: title || subTitle, - sub_title: title && title != subTitle ? subTitle : null, - description: listing.program.description || null, - category: listing.program.mediasetprogram$skyGenre || null, - season: episode && !season ? '0' : season, - episode: episode, - start: parseTime(listing.startTime), - stop: parseTime(listing.endTime), - image: getMaxResolutionThumbnails(listing) - }) - } - }) - - return programs - } -} - -function parseTime(timestamp) { - return dayjs(timestamp).utc().format('YYYY-MM-DD HH:mm') -} - -function parseSeason(item) { - if (!item.mediasetlisting$shortDescription) return null - const season = item.mediasetlisting$shortDescription.match(/S(\d+)\s/) - return season ? season[1] : null -} - -function parseEpisode(item) { - if (!item.mediasetlisting$shortDescription) return null - const episode = item.mediasetlisting$shortDescription.match(/Ep(\d+)\s/) - return episode ? episode[1] : null -} - -function getMaxResolutionThumbnails(item) { - const thumbnails = item.program.thumbnails || null - const maxResolutionThumbnails = {} - - for (const key in thumbnails) { - const type = key.split('-')[0] // Estrarre il tipo di thumbnail - const { width, height, url, title } = thumbnails[key] - - if ( - !maxResolutionThumbnails[type] || - width * height > maxResolutionThumbnails[type].width * maxResolutionThumbnails[type].height - ) { - maxResolutionThumbnails[type] = { width, height, url, title } - } - } - if (maxResolutionThumbnails.image_keyframe_poster) - return maxResolutionThumbnails.image_keyframe_poster.url - else if (maxResolutionThumbnails.image_header_poster) - return maxResolutionThumbnails.image_header_poster.url - else return null -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) +dayjs.extend(timezone) + +module.exports = { + site: 'mediasetinfinity.mediaset.it', + days: 2, + url: function ({ date, channel }) { + // Get the epoch timestamp + const todayEpoch = date.startOf('day').utc().valueOf() + // Get the epoch timestamp for the next day + const nextDayEpoch = date.add(1, 'day').startOf('day').utc().valueOf() + return `https://api-ott-prod-fe.mediaset.net/PROD/play/feed/allListingFeedEpg/v2.0?byListingTime=${todayEpoch}~${nextDayEpoch}&byCallSign=${channel.site_id}` + }, + parser: function ({ content }) { + const programs = [] + const data = JSON.parse(content) + + if ( + !data.response || + !data.response.entries || + !data.response.entries[0] || + !data.response.entries[0].listings + ) { + // If the structure is not as expected, return an empty array + return programs + } + + const listings = data.response.entries[0].listings + + listings.forEach(listing => { + const title = listing.mediasetlisting$epgTitle + const subTitle = listing.program.title + const season = parseSeason(listing) + const episode = parseEpisode(listing) + + if (listing.program.title && listing.startTime && listing.endTime) { + programs.push({ + title: title || subTitle, + sub_title: title && title != subTitle ? subTitle : null, + description: listing.program.description || null, + category: listing.program.mediasetprogram$skyGenre || null, + season: episode && !season ? '0' : season, + episode: episode, + start: parseTime(listing.startTime), + stop: parseTime(listing.endTime), + image: getMaxResolutionThumbnails(listing) + }) + } + }) + + return programs + } +} + +function parseTime(timestamp) { + return dayjs(timestamp).utc().format('YYYY-MM-DD HH:mm') +} + +function parseSeason(item) { + if (!item.mediasetlisting$shortDescription) return null + const season = item.mediasetlisting$shortDescription.match(/S(\d+)\s/) + return season ? season[1] : null +} + +function parseEpisode(item) { + if (!item.mediasetlisting$shortDescription) return null + const episode = item.mediasetlisting$shortDescription.match(/Ep(\d+)\s/) + return episode ? episode[1] : null +} + +function getMaxResolutionThumbnails(item) { + const thumbnails = item.program.thumbnails || null + const maxResolutionThumbnails = {} + + for (const key in thumbnails) { + const type = key.split('-')[0] // Estrarre il tipo di thumbnail + const { width, height, url, title } = thumbnails[key] + + if ( + !maxResolutionThumbnails[type] || + width * height > maxResolutionThumbnails[type].width * maxResolutionThumbnails[type].height + ) { + maxResolutionThumbnails[type] = { width, height, url, title } + } + } + if (maxResolutionThumbnails.image_keyframe_poster) + return maxResolutionThumbnails.image_keyframe_poster.url + else if (maxResolutionThumbnails.image_header_poster) + return maxResolutionThumbnails.image_header_poster.url + else return null +} diff --git a/sites/mediasetinfinity.mediaset.it/mediasetinfinity.mediaset.it.test.js b/sites/mediasetinfinity.mediaset.it/mediasetinfinity.mediaset.it.test.js index c1c8ce11..40963167 100644 --- a/sites/mediasetinfinity.mediaset.it/mediasetinfinity.mediaset.it.test.js +++ b/sites/mediasetinfinity.mediaset.it/mediasetinfinity.mediaset.it.test.js @@ -1,53 +1,53 @@ -const { parser, url } = require('./mediasetinfinity.mediaset.it.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('2024-01-20', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'LB', - xmltv_id: '20.it' -} - -it('can generate valid url', () => { - expect( - url({ - channel, - date - }) - ).toBe( - 'https://api-ott-prod-fe.mediaset.net/PROD/play/feed/allListingFeedEpg/v2.0?byListingTime=1705708800000~1705795200000&byCallSign=LB' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') - const results = parser({ content, date }).map(p => { - return p - }) - - expect(results[3]).toMatchObject({ - start: '2024-01-20 02:14', - stop: '2024-01-20 02:54', - title: 'Chicago Fire', - sub_title: 'Ep. 22 - Io non ti lascio', - description: - 'Severide e Kidd continuano a indagare su un vecchio caso doloso di Benny. Notizie inaspettate portano Brett a meditare su una grande decisione.', - category: 'Intrattenimento', - season: '7', - episode: '22', - image: - 'https://static2.mediasetplay.mediaset.it/Mediaset_Italia_Production_-_Main/F309370301002204/media/0/0/1ef76b73-3173-43bd-9c16-73986a0ec131/46896726-11e7-4438-b947-d2ae53f58c0b.jpg' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '[]' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./mediasetinfinity.mediaset.it.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('2024-01-20', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'LB', + xmltv_id: '20.it' +} + +it('can generate valid url', () => { + expect( + url({ + channel, + date + }) + ).toBe( + 'https://api-ott-prod-fe.mediaset.net/PROD/play/feed/allListingFeedEpg/v2.0?byListingTime=1705708800000~1705795200000&byCallSign=LB' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') + const results = parser({ content, date }).map(p => { + return p + }) + + expect(results[3]).toMatchObject({ + start: '2024-01-20 02:14', + stop: '2024-01-20 02:54', + title: 'Chicago Fire', + sub_title: 'Ep. 22 - Io non ti lascio', + description: + 'Severide e Kidd continuano a indagare su un vecchio caso doloso di Benny. Notizie inaspettate portano Brett a meditare su una grande decisione.', + category: 'Intrattenimento', + season: '7', + episode: '22', + image: + 'https://static2.mediasetplay.mediaset.it/Mediaset_Italia_Production_-_Main/F309370301002204/media/0/0/1ef76b73-3173-43bd-9c16-73986a0ec131/46896726-11e7-4438-b947-d2ae53f58c0b.jpg' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '[]' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/melita.com/__data__/content.json b/sites/melita.com/__data__/content.json new file mode 100644 index 00000000..4d7529fd --- /dev/null +++ b/sites/melita.com/__data__/content.json @@ -0,0 +1 @@ +{"schedules":[{"id":"138dabff-131a-42a0-9373-203545933dd0","published":{"start":"2022-04-20T06:25:00Z","end":"2022-04-20T06:45:00Z"},"program":"ae52299a-3c99-4d34-9932-e21d383f9800","live":false,"blackouts":[]}],"programs":[{"id":"ae52299a-3c99-4d34-9932-e21d383f9800","title":"How I Met Your Mother","shortSynopsis":"Symphony of Illumination - Robin gets some bad news and decides to keep it to herself. Marshall decorates the house.","posterImage":"https://androme.melitacable.com/media/images/epg/bc/07/p8953134_e_h10_ad.jpg","episode":12,"episodeTitle":"Symphony of Illumination","season":"fdd6e42c-97f9-4d7a-aaca-78b53378f960","genres":["3.5.7.3"],"tags":["comedy"],"adult":false}],"seasons":[{"id":"fdd6e42c-97f9-4d7a-aaca-78b53378f960","title":"How I Met Your Mother","adult":false,"season":7,"series":"858c535a-abbb-451b-807a-94196997ea2d"}],"series":[{"id":"858c535a-abbb-451b-807a-94196997ea2d","title":"How I Met Your Mother","adult":false}]} \ No newline at end of file diff --git a/sites/melita.com/melita.com.config.js b/sites/melita.com/melita.com.config.js index d344170c..93386c7a 100644 --- a/sites/melita.com/melita.com.config.js +++ b/sites/melita.com/melita.com.config.js @@ -1,89 +1,89 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'melita.com', - days: 2, - url: function ({ channel, date }) { - return `https://androme.melitacable.com/api/epg/v1/schedule/channel/${ - channel.site_id - }/from/${date.format('YYYY-MM-DDTHH:mmZ')}/until/${date - .add(1, 'd') - .format('YYYY-MM-DDTHH:mmZ')}` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.shortSynopsis, - image: parseImage(item), - category: item.tags, - season: item.season, - episode: item.episode, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const channels = await axios - .get('https://androme.melitacable.com/api/epg/v2/channel') - .then(r => r.data) - .catch(console.log) - - return channels - .filter(i => !i.audioOnly && i.enabled) - .map(i => { - return { - lang: 'en', - name: i.name, - site_id: i.id - } - }) - } -} - -function parseStart(item) { - if (!item.published || !item.published.start) return null - - return dayjs(item.published.start) -} - -function parseStop(item) { - if (!item.published || !item.published.end) return null - - return dayjs(item.published.end) -} - -function parseImage(item) { - return item.posterImage ? item.posterImage + '?form=epg-card-6' : null -} - -function parseItems(content) { - const data = JSON.parse(content) - if ( - !data || - !data.schedules || - !data.programs || - !data.seasons || - !data.series || - !Array.isArray(data.schedules) - ) - return [] - - return data.schedules - .map(i => { - const program = data.programs.find(p => p.id === i.program) || {} - if (!program.season) return null - const season = data.seasons.find(s => s.id === program.season) || {} - if (!season.series) return null - const series = data.series.find(s => s.id === season.series) - - return { ...i, ...program, ...season, ...series } - }) - .filter(i => i) -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'melita.com', + days: 2, + url: function ({ channel, date }) { + return `https://androme.melitacable.com/api/epg/v1/schedule/channel/${ + channel.site_id + }/from/${date.format('YYYY-MM-DDTHH:mmZ')}/until/${date + .add(1, 'd') + .format('YYYY-MM-DDTHH:mmZ')}` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.shortSynopsis, + image: parseImage(item), + category: item.tags, + season: item.season, + episode: item.episode, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const channels = await axios + .get('https://androme.melitacable.com/api/epg/v2/channel') + .then(r => r.data) + .catch(console.log) + + return channels + .filter(i => !i.audioOnly && i.enabled) + .map(i => { + return { + lang: 'en', + name: i.name, + site_id: i.id + } + }) + } +} + +function parseStart(item) { + if (!item.published || !item.published.start) return null + + return dayjs(item.published.start) +} + +function parseStop(item) { + if (!item.published || !item.published.end) return null + + return dayjs(item.published.end) +} + +function parseImage(item) { + return item.posterImage ? item.posterImage + '?form=epg-card-6' : null +} + +function parseItems(content) { + const data = JSON.parse(content) + if ( + !data || + !data.schedules || + !data.programs || + !data.seasons || + !data.series || + !Array.isArray(data.schedules) + ) + return [] + + return data.schedules + .map(i => { + const program = data.programs.find(p => p.id === i.program) || {} + if (!program.season) return null + const season = data.seasons.find(s => s.id === program.season) || {} + if (!season.series) return null + const series = data.series.find(s => s.id === season.series) + + return { ...i, ...program, ...season, ...series } + }) + .filter(i => i) +} diff --git a/sites/melita.com/melita.com.test.js b/sites/melita.com/melita.com.test.js index 2cedf24e..1781cbb2 100644 --- a/sites/melita.com/melita.com.test.js +++ b/sites/melita.com/melita.com.test.js @@ -1,50 +1,51 @@ -const { parser, url } = require('./melita.com.config.js') -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('2022-04-20', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '4d40a9f9-12fd-4f03-8072-61c637ff6995', - xmltv_id: 'TVM.mt' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://androme.melitacable.com/api/epg/v1/schedule/channel/4d40a9f9-12fd-4f03-8072-61c637ff6995/from/2022-04-20T00:00+00:00/until/2022-04-21T00:00+00:00' - ) -}) - -it('can parse response', () => { - const content = - '{"schedules":[{"id":"138dabff-131a-42a0-9373-203545933dd0","published":{"start":"2022-04-20T06:25:00Z","end":"2022-04-20T06:45:00Z"},"program":"ae52299a-3c99-4d34-9932-e21d383f9800","live":false,"blackouts":[]}],"programs":[{"id":"ae52299a-3c99-4d34-9932-e21d383f9800","title":"How I Met Your Mother","shortSynopsis":"Symphony of Illumination - Robin gets some bad news and decides to keep it to herself. Marshall decorates the house.","posterImage":"https://androme.melitacable.com/media/images/epg/bc/07/p8953134_e_h10_ad.jpg","episode":12,"episodeTitle":"Symphony of Illumination","season":"fdd6e42c-97f9-4d7a-aaca-78b53378f960","genres":["3.5.7.3"],"tags":["comedy"],"adult":false}],"seasons":[{"id":"fdd6e42c-97f9-4d7a-aaca-78b53378f960","title":"How I Met Your Mother","adult":false,"season":7,"series":"858c535a-abbb-451b-807a-94196997ea2d"}],"series":[{"id":"858c535a-abbb-451b-807a-94196997ea2d","title":"How I Met Your Mother","adult":false}]}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-04-20T06:25:00.000Z', - stop: '2022-04-20T06:45:00.000Z', - title: 'How I Met Your Mother', - description: - 'Symphony of Illumination - Robin gets some bad news and decides to keep it to herself. Marshall decorates the house.', - season: 7, - episode: 12, - image: - 'https://androme.melitacable.com/media/images/epg/bc/07/p8953134_e_h10_ad.jpg?form=epg-card-6', - category: ['comedy'] - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '{}' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./melita.com.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('2022-04-20', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '4d40a9f9-12fd-4f03-8072-61c637ff6995', + xmltv_id: 'TVM.mt' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://androme.melitacable.com/api/epg/v1/schedule/channel/4d40a9f9-12fd-4f03-8072-61c637ff6995/from/2022-04-20T00:00+00:00/until/2022-04-21T00:00+00:00' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-04-20T06:25:00.000Z', + stop: '2022-04-20T06:45:00.000Z', + title: 'How I Met Your Mother', + description: + 'Symphony of Illumination - Robin gets some bad news and decides to keep it to herself. Marshall decorates the house.', + season: 7, + episode: 12, + image: + 'https://androme.melitacable.com/media/images/epg/bc/07/p8953134_e_h10_ad.jpg?form=epg-card-6', + category: ['comedy'] + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '{}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/meo.pt/meo.pt.config.js b/sites/meo.pt/meo.pt.config.js index c8054d69..9d93ab20 100644 --- a/sites/meo.pt/meo.pt.config.js +++ b/sites/meo.pt/meo.pt.config.js @@ -1,82 +1,82 @@ -const { DateTime } = require('luxon') - -module.exports = { - site: 'meo.pt', - days: 2, - url: 'https://authservice.apps.meo.pt/Services/GridTv/GridTvMng.svc/getProgramsFromChannels', - request: { - method: 'POST', - headers: { - Origin: 'https://www.meo.pt', - 'User-Agent': 'Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; en-US Trident/4.0)' - }, - data: function ({ channel, date }) { - return { - service: 'channelsguide', - channels: [channel.site_id], - dateStart: date.format('YYYY-MM-DDT00:00:00-00:00'), - dateEnd: date.add(1, 'd').format('YYYY-MM-DDT00:00:00-00:00'), - accountID: '' - } - } - }, - parser({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const start = parseStart(item) - let stop = parseStop(item) - if (stop < start) { - stop = stop.plus({ days: 1 }) - } - programs.push({ - title: item.name, - start, - stop - }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - const data = await axios - .post('https://authservice.apps.meo.pt/Services/GridTv/GridTvMng.svc/getGridAnon', null, { - headers: { - Origin: 'https://www.meo.pt' - } - }) - .then(r => r.data) - .catch(console.log) - - return data.d.channels - .map(item => { - return { - lang: 'pt', - site_id: item.sigla, - name: item.name - } - }) - .filter(channel => channel.site_id) - } -} - -function parseStart(item) { - return DateTime.fromFormat(`${item.date} ${item.timeIni}`, 'd-M-yyyy HH:mm', { - zone: 'Europe/Lisbon' - }).toUTC() -} - -function parseStop(item) { - return DateTime.fromFormat(`${item.date} ${item.timeEnd}`, 'd-M-yyyy HH:mm', { - zone: 'Europe/Lisbon' - }).toUTC() -} - -function parseItems(content) { - if (!content) return [] - const data = JSON.parse(content) - const programs = data?.d?.channels?.[0]?.programs - - return Array.isArray(programs) ? programs : [] -} +const { DateTime } = require('luxon') + +module.exports = { + site: 'meo.pt', + days: 2, + url: 'https://authservice.apps.meo.pt/Services/GridTv/GridTvMng.svc/getProgramsFromChannels', + request: { + method: 'POST', + headers: { + Origin: 'https://www.meo.pt', + 'User-Agent': 'Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; en-US Trident/4.0)' + }, + data: function ({ channel, date }) { + return { + service: 'channelsguide', + channels: [channel.site_id], + dateStart: date.format('YYYY-MM-DDT00:00:00-00:00'), + dateEnd: date.add(1, 'd').format('YYYY-MM-DDT00:00:00-00:00'), + accountID: '' + } + } + }, + parser({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const start = parseStart(item) + let stop = parseStop(item) + if (stop < start) { + stop = stop.plus({ days: 1 }) + } + programs.push({ + title: item.name, + start, + stop + }) + }) + + return programs + }, + async channels() { + const axios = require('axios') + const data = await axios + .post('https://authservice.apps.meo.pt/Services/GridTv/GridTvMng.svc/getGridAnon', null, { + headers: { + Origin: 'https://www.meo.pt' + } + }) + .then(r => r.data) + .catch(console.log) + + return data.d.channels + .map(item => { + return { + lang: 'pt', + site_id: item.sigla, + name: item.name + } + }) + .filter(channel => channel.site_id) + } +} + +function parseStart(item) { + return DateTime.fromFormat(`${item.date} ${item.timeIni}`, 'd-M-yyyy HH:mm', { + zone: 'Europe/Lisbon' + }).toUTC() +} + +function parseStop(item) { + return DateTime.fromFormat(`${item.date} ${item.timeEnd}`, 'd-M-yyyy HH:mm', { + zone: 'Europe/Lisbon' + }).toUTC() +} + +function parseItems(content) { + if (!content) return [] + const data = JSON.parse(content) + const programs = data?.d?.channels?.[0]?.programs + + return Array.isArray(programs) ? programs : [] +} diff --git a/sites/meo.pt/meo.pt.test.js b/sites/meo.pt/meo.pt.test.js index 7decee75..c1b455cd 100644 --- a/sites/meo.pt/meo.pt.test.js +++ b/sites/meo.pt/meo.pt.test.js @@ -1,60 +1,60 @@ -const { parser, url, request } = require('./meo.pt.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('2022-12-02', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'RTPM', - xmltv_id: 'RTPMadeira.pt' -} - -it('can generate valid url', () => { - expect(url).toBe( - 'https://authservice.apps.meo.pt/Services/GridTv/GridTvMng.svc/getProgramsFromChannels' - ) -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - Origin: 'https://www.meo.pt' - }) -}) - -it('can generate valid request method', () => { - expect(request.data({ channel, date })).toMatchObject({ - service: 'channelsguide', - channels: ['RTPM'], - dateStart: '2022-12-02T00:00:00-00:00', - dateEnd: '2022-12-03T00:00:00-00:00', - accountID: '' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-12-01T23:35:00.000Z', - stop: '2022-12-02T00:17:00.000Z', - title: 'Walker, O Ranger Do Texas T6 - Ep. 14' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ content: '', channel, date }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./meo.pt.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('2022-12-02', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'RTPM', + xmltv_id: 'RTPMadeira.pt' +} + +it('can generate valid url', () => { + expect(url).toBe( + 'https://authservice.apps.meo.pt/Services/GridTv/GridTvMng.svc/getProgramsFromChannels' + ) +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + Origin: 'https://www.meo.pt' + }) +}) + +it('can generate valid request method', () => { + expect(request.data({ channel, date })).toMatchObject({ + service: 'channelsguide', + channels: ['RTPM'], + dateStart: '2022-12-02T00:00:00-00:00', + dateEnd: '2022-12-03T00:00:00-00:00', + accountID: '' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-12-01T23:35:00.000Z', + stop: '2022-12-02T00:17:00.000Z', + title: 'Walker, O Ranger Do Texas T6 - Ep. 14' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ content: '', channel, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/meuguia.tv/meuguia.tv.config.js b/sites/meuguia.tv/meuguia.tv.config.js index e86f2bfa..1fdd3a08 100644 --- a/sites/meuguia.tv/meuguia.tv.config.js +++ b/sites/meuguia.tv/meuguia.tv.config.js @@ -1,105 +1,105 @@ -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: 'meuguia.tv', - days: 2, - url({ channel }) { - return `https://meuguia.tv/programacao/canal/${channel.site_id}` - }, - parser({ content, date }) { - const programs = [] - parseItems(content, date).forEach(item => { - if (dayjs.utc(item.start).isSame(date, 'day')) { - programs.push(item) - } - }) - - return programs - }, - async channels() { - const channels = [] - const axios = require('axios') - const baseUrl = 'https://meuguia.tv' - - let seq = 0 - const queues = [baseUrl] - while (true) { - if (!queues.length) { - break - } - const url = queues.shift() - const content = await axios - .get(url) - .then(response => response.data) - .catch(console.error) - - if (content) { - const [$, items] = getItems(content) - if (seq === 0) { - queues.push(...items.map(category => baseUrl + $(category).attr('href'))) - } else { - items.forEach(item => { - const href = $(item).attr('href') - channels.push({ - lang: 'pt', - site_id: href.substr(href.lastIndexOf('/') + 1), - name: $(item).find('.licontent h2').text().trim() - }) - }) - } - } - seq++ - } - - return channels - } -} - -function getItems(content) { - const $ = cheerio.load(content) - return [$, $('div.mw ul li a').toArray()] -} - -function parseItems(content, date) { - const result = [] - const $ = cheerio.load(content) - - let lastDate - for (const item of $('ul.mw li').toArray()) { - const $item = $(item) - if ($item.hasClass('subheader')) { - lastDate = `${$item.text().split(', ')[1]}/${date.format('YYYY')}` - } else if ($item.hasClass('divider')) { - // ignore - } else if (lastDate) { - const data = { title: $item.find('a').attr('title').trim() } - const ep = data.title.match(/T(\d+) EP(\d+)/) - if (ep) { - data.season = parseInt(ep[1]) - data.episode = parseInt(ep[2]) - } - data.start = dayjs.tz( - `${lastDate} ${$item.find('.time').text()}`, - 'DD/MM/YYYY HH:mm', - 'America/Sao_Paulo' - ) - result.push(data) - } - } - // use stop time from next item - if (result.length > 1) { - for (let i = 0; i < result.length - 1; i++) { - result[i].stop = result[i + 1].start - } - } - - return result -} +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: 'meuguia.tv', + days: 2, + url({ channel }) { + return `https://meuguia.tv/programacao/canal/${channel.site_id}` + }, + parser({ content, date }) { + const programs = [] + parseItems(content, date).forEach(item => { + if (dayjs.utc(item.start).isSame(date, 'day')) { + programs.push(item) + } + }) + + return programs + }, + async channels() { + const channels = [] + const axios = require('axios') + const baseUrl = 'https://meuguia.tv' + + let seq = 0 + const queues = [baseUrl] + while (true) { + if (!queues.length) { + break + } + const url = queues.shift() + const content = await axios + .get(url) + .then(response => response.data) + .catch(console.error) + + if (content) { + const [$, items] = getItems(content) + if (seq === 0) { + queues.push(...items.map(category => baseUrl + $(category).attr('href'))) + } else { + items.forEach(item => { + const href = $(item).attr('href') + channels.push({ + lang: 'pt', + site_id: href.substr(href.lastIndexOf('/') + 1), + name: $(item).find('.licontent h2').text().trim() + }) + }) + } + } + seq++ + } + + return channels + } +} + +function getItems(content) { + const $ = cheerio.load(content) + return [$, $('div.mw ul li a').toArray()] +} + +function parseItems(content, date) { + const result = [] + const $ = cheerio.load(content) + + let lastDate + for (const item of $('ul.mw li').toArray()) { + const $item = $(item) + if ($item.hasClass('subheader')) { + lastDate = `${$item.text().split(', ')[1]}/${date.format('YYYY')}` + } else if ($item.hasClass('divider')) { + // ignore + } else if (lastDate) { + const data = { title: $item.find('a').attr('title').trim() } + const ep = data.title.match(/T(\d+) EP(\d+)/) + if (ep) { + data.season = parseInt(ep[1]) + data.episode = parseInt(ep[2]) + } + data.start = dayjs.tz( + `${lastDate} ${$item.find('.time').text()}`, + 'DD/MM/YYYY HH:mm', + 'America/Sao_Paulo' + ) + result.push(data) + } + } + // use stop time from next item + if (result.length > 1) { + for (let i = 0; i < result.length - 1; i++) { + result[i].stop = result[i + 1].start + } + } + + return result +} diff --git a/sites/meuguia.tv/meuguia.tv.test.js b/sites/meuguia.tv/meuguia.tv.test.js index 3fe2ce94..c121039c 100644 --- a/sites/meuguia.tv/meuguia.tv.test.js +++ b/sites/meuguia.tv/meuguia.tv.test.js @@ -1,60 +1,60 @@ -const { parser, url } = require('./meuguia.tv.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -const date = dayjs.utc('2023-11-21').startOf('d') -const channel = { - site_id: 'AXN', - xmltv_id: 'AXN.id' -} -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://meuguia.tv/programacao/canal/AXN') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - if (p.stop) { - p.stop = p.stop.toJSON() - } - return p - }) - - expect(result).toMatchObject([ - { - title: 'Hawaii Five-0 : T10 EP4 - Tiny Is the Flower, Yet It Scents the Grasses Around It', - start: '2023-11-21T21:20:00.000Z', - stop: '2023-11-21T22:15:00.000Z', - season: 10, - episode: 4 - }, - { - title: - "Hawaii Five-0 : T10 EP5 - Don't Blame Ghosts and Spirits for One's Troubles; A Human Is Responsible", - start: '2023-11-21T22:15:00.000Z', - stop: '2023-11-21T23:10:00.000Z', - season: 10, - episode: 5 - }, - { - title: 'NCIS : T5 EP15 - In the Zone', - start: '2023-11-21T23:10:00.000Z', - season: 5, - episode: 15 - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./meuguia.tv.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +const date = dayjs.utc('2023-11-21').startOf('d') +const channel = { + site_id: 'AXN', + xmltv_id: 'AXN.id' +} +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://meuguia.tv/programacao/canal/AXN') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + if (p.stop) { + p.stop = p.stop.toJSON() + } + return p + }) + + expect(result).toMatchObject([ + { + title: 'Hawaii Five-0 : T10 EP4 - Tiny Is the Flower, Yet It Scents the Grasses Around It', + start: '2023-11-21T21:20:00.000Z', + stop: '2023-11-21T22:15:00.000Z', + season: 10, + episode: 4 + }, + { + title: + "Hawaii Five-0 : T10 EP5 - Don't Blame Ghosts and Spirits for One's Troubles; A Human Is Responsible", + start: '2023-11-21T22:15:00.000Z', + stop: '2023-11-21T23:10:00.000Z', + season: 10, + episode: 5 + }, + { + title: 'NCIS : T5 EP15 - In the Zone', + start: '2023-11-21T23:10:00.000Z', + season: 5, + episode: 15 + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mewatch.sg/__data__/content.json b/sites/mewatch.sg/__data__/content.json new file mode 100644 index 00000000..81c223b3 --- /dev/null +++ b/sites/mewatch.sg/__data__/content.json @@ -0,0 +1 @@ +[{"channelId":"97098","startDate":"2022-06-11T21:00:00.000Z","endDate":"2022-06-12T21:00:00.000Z","schedules":[{"channelId":"97098","customId":"37040748","endDate":"2022-06-11T21:30:00Z","id":"788a7dd","live":false,"startDate":"2022-06-11T21:00:00Z","isGap":false,"InteractiveType":"0","item":{"type":"episode","title":"Open Homes S3 - EP 2","blackoutMessage":"Programme is not available for live streaming.","description":"Mike heads down to the Sydney beaches to visit a beachside renovation with all the bells and whistles, we see a kitchen tip and recipe anyone can do at home. We finish up in the prestigious Byron bay to visit a multi million dollar award winning home.","classification":{"code":"IMDA-G (Violence)","name":"G (Violence)"},"episodeNumber":2,"episodeTitle":"Collaroy, Sydney","seasonNumber":3,"images":{"wallpaper":"https://production.togglestatic.com/shain/v1/dataservice/ResizeImage/$value?Format='jpg'&Quality=85&ImageId='4853691'&EntityType='LinearSchedule'&EntityId='788a7dd9-9b12-446f-91b4-c8ac9fec95e5'&Width=1280&Height=720&device=web_browser&subscriptions=Anonymous&segmentationTags=all","tile":"https://production.togglestatic.com/shain/v1/dataservice/ResizeImage/$value?Format='jpg'&Quality=85&ImageId='4853697'&EntityType='LinearSchedule'&EntityId='788a7dd9-9b12-446f-91b4-c8ac9fec95e5'&Width=1280&Height=720&device=web_browser&subscriptions=Anonymous&segmentationTags=all"},"enableCatchUp":true,"enableStartOver":false,"enableSeeking":false,"programSource":"ACQUIRED","simulcast":"LOCAL","masterReferenceKey":"0CH50CH5A0105567800020A0000000000P3254400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}}]}] \ No newline at end of file diff --git a/sites/mewatch.sg/__data__/no_content.json b/sites/mewatch.sg/__data__/no_content.json new file mode 100644 index 00000000..72d02266 --- /dev/null +++ b/sites/mewatch.sg/__data__/no_content.json @@ -0,0 +1 @@ +[{"channelId":"9798","startDate":"2022-06-11T21:00:00.000Z","endDate":"2022-06-12T21:00:00.000Z","schedules":[]}] \ No newline at end of file diff --git a/sites/mewatch.sg/mewatch.sg.config.js b/sites/mewatch.sg/mewatch.sg.config.js index 1b793c83..0d77c665 100644 --- a/sites/mewatch.sg/mewatch.sg.config.js +++ b/sites/mewatch.sg/mewatch.sg.config.js @@ -1,100 +1,100 @@ -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: 'mewatch.sg', - days: 2, - url: function ({ channel, date }) { - const utcDate = date.isUTC() ? date.tz(dayjs.tz.guess(), true).utc() : date.utc() - - return `https://cdn.mewatch.sg/api/schedules?channels=${channel.site_id}&date=${utcDate.format( - 'YYYY-MM-DD' - )}&duration=24&ff=idp,ldp,rpt,cd&hour=${utcDate.format( - 'HH' - )}&intersect=true&lang=en&segments=all` - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const info = item.item - programs.push({ - title: info.title, - description: info.description, - image: info.images.tile, - episode: info.episodeNumber, - season: info.seasonNumber, - start: parseStart(item), - stop: parseStop(item), - rating: parseRating(info) - }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - const cheerio = require('cheerio') - const data = await axios - .get('https://www.mewatch.sg/channel-guide') - .then(r => r.data) - .catch(console.log) - - let channels = [] - const $ = cheerio.load(data) - $('#side-nav > div > div > div > nav:nth-child(1) > ul > li > ul > li').each((i, el) => { - const name = $(el).find('a > span').text() - const url = $(el).find('a').attr('href') - const [, site_id = null] = url.match(/\/(\d+)\?player-fullscreen/) ?? [] - - if (!site_id) { - return - } - - channels.push({ - lang: 'en', - name, - site_id - }) - }) - - return channels - } -} - -function parseStart(item) { - return dayjs(item.startDate) -} - -function parseStop(item) { - return dayjs(item.endDate) -} - -function parseRating(info) { - const classification = info.classification - if (classification && classification.code) { - const [, system, value] = classification.code.match(/^([A-Z]+)-([A-Z0-9]+)/) || [ - null, - null, - null - ] - - return { system, value } - } - - return null -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data)) return [] - const channelData = data.find(i => i.channelId === channel.site_id) - - return channelData && Array.isArray(channelData.schedules) ? channelData.schedules : [] -} +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: 'mewatch.sg', + days: 2, + url: function ({ channel, date }) { + const utcDate = date.isUTC() ? date.tz(dayjs.tz.guess(), true).utc() : date.utc() + + return `https://cdn.mewatch.sg/api/schedules?channels=${channel.site_id}&date=${utcDate.format( + 'YYYY-MM-DD' + )}&duration=24&ff=idp,ldp,rpt,cd&hour=${utcDate.format( + 'HH' + )}&intersect=true&lang=en&segments=all` + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const info = item.item + programs.push({ + title: info.title, + description: info.description, + image: info.images.tile, + episode: info.episodeNumber, + season: info.seasonNumber, + start: parseStart(item), + stop: parseStop(item), + rating: parseRating(info) + }) + }) + + return programs + }, + async channels() { + const axios = require('axios') + const cheerio = require('cheerio') + const data = await axios + .get('https://www.mewatch.sg/channel-guide') + .then(r => r.data) + .catch(console.log) + + let channels = [] + const $ = cheerio.load(data) + $('#side-nav > div > div > div > nav:nth-child(1) > ul > li > ul > li').each((i, el) => { + const name = $(el).find('a > span').text() + const url = $(el).find('a').attr('href') + const [, site_id = null] = url.match(/\/(\d+)\?player-fullscreen/) ?? [] + + if (!site_id) { + return + } + + channels.push({ + lang: 'en', + name, + site_id + }) + }) + + return channels + } +} + +function parseStart(item) { + return dayjs(item.startDate) +} + +function parseStop(item) { + return dayjs(item.endDate) +} + +function parseRating(info) { + const classification = info.classification + if (classification && classification.code) { + const [, system, value] = classification.code.match(/^([A-Z]+)-([A-Z0-9]+)/) || [ + null, + null, + null + ] + + return { system, value } + } + + return null +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data)) return [] + const channelData = data.find(i => i.channelId === channel.site_id) + + return channelData && Array.isArray(channelData.schedules) ? channelData.schedules : [] +} diff --git a/sites/mewatch.sg/mewatch.sg.test.js b/sites/mewatch.sg/mewatch.sg.test.js index 6a949443..0eb0565e 100644 --- a/sites/mewatch.sg/mewatch.sg.test.js +++ b/sites/mewatch.sg/mewatch.sg.test.js @@ -1,55 +1,56 @@ -const { parser, url } = require('./mewatch.sg.config.js') -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('2022-06-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '97098', - xmltv_id: 'Channel5Singapore.sg' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://cdn.mewatch.sg/api/schedules?channels=97098&date=2022-06-10&duration=24&ff=idp,ldp,rpt,cd&hour=12&intersect=true&lang=en&segments=all' - ) -}) - -it('can parse response', () => { - const content = - '[{"channelId":"97098","startDate":"2022-06-11T21:00:00.000Z","endDate":"2022-06-12T21:00:00.000Z","schedules":[{"channelId":"97098","customId":"37040748","endDate":"2022-06-11T21:30:00Z","id":"788a7dd","live":false,"startDate":"2022-06-11T21:00:00Z","isGap":false,"InteractiveType":"0","item":{"type":"episode","title":"Open Homes S3 - EP 2","blackoutMessage":"Programme is not available for live streaming.","description":"Mike heads down to the Sydney beaches to visit a beachside renovation with all the bells and whistles, we see a kitchen tip and recipe anyone can do at home. We finish up in the prestigious Byron bay to visit a multi million dollar award winning home.","classification":{"code":"IMDA-G (Violence)","name":"G (Violence)"},"episodeNumber":2,"episodeTitle":"Collaroy, Sydney","seasonNumber":3,"images":{"wallpaper":"https://production.togglestatic.com/shain/v1/dataservice/ResizeImage/$value?Format=\'jpg\'&Quality=85&ImageId=\'4853691\'&EntityType=\'LinearSchedule\'&EntityId=\'788a7dd9-9b12-446f-91b4-c8ac9fec95e5\'&Width=1280&Height=720&device=web_browser&subscriptions=Anonymous&segmentationTags=all","tile":"https://production.togglestatic.com/shain/v1/dataservice/ResizeImage/$value?Format=\'jpg\'&Quality=85&ImageId=\'4853697\'&EntityType=\'LinearSchedule\'&EntityId=\'788a7dd9-9b12-446f-91b4-c8ac9fec95e5\'&Width=1280&Height=720&device=web_browser&subscriptions=Anonymous&segmentationTags=all"},"enableCatchUp":true,"enableStartOver":false,"enableSeeking":false,"programSource":"ACQUIRED","simulcast":"LOCAL","masterReferenceKey":"0CH50CH5A0105567800020A0000000000P3254400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}}]}]' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-06-11T21:00:00.000Z', - stop: '2022-06-11T21:30:00.000Z', - title: 'Open Homes S3 - EP 2', - description: - 'Mike heads down to the Sydney beaches to visit a beachside renovation with all the bells and whistles, we see a kitchen tip and recipe anyone can do at home. We finish up in the prestigious Byron bay to visit a multi million dollar award winning home.', - image: - "https://production.togglestatic.com/shain/v1/dataservice/ResizeImage/$value?Format='jpg'&Quality=85&ImageId='4853697'&EntityType='LinearSchedule'&EntityId='788a7dd9-9b12-446f-91b4-c8ac9fec95e5'&Width=1280&Height=720&device=web_browser&subscriptions=Anonymous&segmentationTags=all", - episode: 2, - season: 3, - rating: { - system: 'IMDA', - value: 'G' - } - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: - '[{"channelId":"9798","startDate":"2022-06-11T21:00:00.000Z","endDate":"2022-06-12T21:00:00.000Z","schedules":[]}]', - channel - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./mewatch.sg.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('2022-06-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '97098', + xmltv_id: 'Channel5Singapore.sg' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://cdn.mewatch.sg/api/schedules?channels=97098&date=2022-06-10&duration=24&ff=idp,ldp,rpt,cd&hour=12&intersect=true&lang=en&segments=all' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-06-11T21:00:00.000Z', + stop: '2022-06-11T21:30:00.000Z', + title: 'Open Homes S3 - EP 2', + description: + 'Mike heads down to the Sydney beaches to visit a beachside renovation with all the bells and whistles, we see a kitchen tip and recipe anyone can do at home. We finish up in the prestigious Byron bay to visit a multi million dollar award winning home.', + image: + "https://production.togglestatic.com/shain/v1/dataservice/ResizeImage/$value?Format='jpg'&Quality=85&ImageId='4853697'&EntityType='LinearSchedule'&EntityId='788a7dd9-9b12-446f-91b4-c8ac9fec95e5'&Width=1280&Height=720&device=web_browser&subscriptions=Anonymous&segmentationTags=all", + episode: 2, + season: 3, + rating: { + system: 'IMDA', + value: 'G' + } + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: + fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')), + channel + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mi.tv/__data__/content.html b/sites/mi.tv/__data__/content.html new file mode 100644 index 00000000..920a8d23 --- /dev/null +++ b/sites/mi.tv/__data__/content.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sites/mi.tv/__data__/no_content.html b/sites/mi.tv/__data__/no_content.html new file mode 100644 index 00000000..6fedfd4c --- /dev/null +++ b/sites/mi.tv/__data__/no_content.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sites/mi.tv/mi.tv.config.js b/sites/mi.tv/mi.tv.config.js index fb8ec605..fe7df4aa 100644 --- a/sites/mi.tv/mi.tv.config.js +++ b/sites/mi.tv/mi.tv.config.js @@ -1,116 +1,144 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -const headers = { - "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", - "accept-language": "en", - "sec-fetch-site": "same-origin", - "sec-fetch-user": "?1", - "upgrade-insecure-requests": "1", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36" -} - -module.exports = { - site: 'mi.tv', - days: 2, - request: { headers }, - url({ date, channel }) { - const [country, id] = channel.site_id.split('#') - return `https://mi.tv/${country}/async/channel/${id}/${date.format('YYYY-MM-DD')}/0` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (!start) return - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(1, 'h') - programs.push({ - title: parseTitle($item), - category: parseCategory($item), - description: parseDescription($item), - image: parseImage($item), - start, - stop - }) - }) - - return programs - }, - async channels({ country }) { - let lang = 'es' - if (country === 'br') lang = 'pt' - - const axios = require('axios') - const data = await axios - .get(`https://mi.tv/${country}/sitemap`) - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(data) - - let channels = [] - $(`#page-contents a[href*="${country}/canales"], a[href*="${country}/canais"]`).each( - (i, el) => { - const name = $(el).text() - const url = $(el).attr('href') - const [, , , channelId] = url.split('/') - - channels.push({ - lang, - name, - site_id: `${country}#${channelId}` - }) - } - ) - - return channels - } -} - -function parseStart($item, date) { - const timeString = $item('a > div.content > span.time').text() - if (!timeString) return null - const dateString = `${date.format('MM/DD/YYYY')} ${timeString}` - - return dayjs.utc(dateString, 'MM/DD/YYYY HH:mm') -} - -function parseTitle($item) { - return $item('a > div.content > h2').text().trim() -} - -function parseCategory($item) { - return $item('a > div.content > span.sub-title').text().trim() -} - -function parseDescription($item) { - return $item('a > div.content > p.synopsis').text().trim() -} - -function parseImage($item) { - const backgroundImage = $item('a > div.image-parent > div.image').css('background-image') - const [, image] = backgroundImage.match(/url\('(.*)'\)/) || [null, null] - - return image -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#listings > ul > li').toArray() +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +const headers = { + 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'accept-language': 'en', + 'sec-fetch-site': 'same-origin', + 'sec-fetch-user': '?1', + 'upgrade-insecure-requests': '1', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36' +} + +module.exports = { + site: 'mi.tv', + days: 2, + request: { headers }, + url({ date, channel }) { + const [country, id] = channel.site_id.split('#') + return `https://mi.tv/${country}/async/channel/${id}/${date.format('YYYY-MM-DD')}/0` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (!start) return + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(1, 'h') + programs.push({ + title: parseTitle($item), + category: parseCategory($item), + description: parseDescription($item), + image: parseImage($item), + start, + stop + }) + }) + + return programs + }, + async channels({ country }) { + let lang = 'es' + if (country === 'br') lang = 'pt' + + const axios = require('axios') + const data = await axios + .get(`https://mi.tv/${country}/sitemap`) + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(data) + + let channels = [] + $(`#page-contents a[href*="${country}/canales"], a[href*="${country}/canais"]`).each( + (i, el) => { + const name = $(el).text() + const url = $(el).attr('href') + const [, , , channelId] = url.split('/') + + channels.push({ + lang, + name, + site_id: `${country}#${channelId}` + }) + } + ) + + return channels + } +} + +function parseStart($item, date) { + const timeString = $item('a > div.content > span.time').text() + if (!timeString) return null + const dateString = `${date.format('MM/DD/YYYY')} ${timeString}` + + return dayjs.utc(dateString, 'MM/DD/YYYY HH:mm') +} + +function parseTitle($item) { + return $item('a > div.content > h2').text().trim() +} + +function parseCategory($item) { + return $item('a > div.content > span.sub-title').text().trim() +} + +function parseDescription($item) { + return $item('a > div.content > p.synopsis').text().trim() +} + + +function parseImage($item) { + const styleAttr = $item('a > div.image-parent > div.image').attr('style') + + if (styleAttr) { + const match = styleAttr.match(/background-image:\s*url\(['"]?(.*?)['"]?\)/) + if (match) { + return cleanUrl(match[1]) + } + } + + const backgroundImage = $item('a > div.image-parent > div.image').css('background-image') + + if (backgroundImage && backgroundImage !== 'none') { + const match = backgroundImage.match(/url\(['"]?(.*?)['"]?\)/) + if (match) { + return cleanUrl(match[1]) + } + } + + return null +} + +function cleanUrl(url) { + if (!url) return null + + return url + .replace(/^['"`\\]+/, '') + .replace(/['"`\\]+$/, '') + .replace(/\\'/g, "'") + .replace(/\\"/g, '"') + .replace(/\\\\/g, '\\') +} + + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('#listings > ul > li').toArray() } \ No newline at end of file diff --git a/sites/mi.tv/mi.tv.test.js b/sites/mi.tv/mi.tv.test.js index 5bd1a9f3..69a07e8d 100644 --- a/sites/mi.tv/mi.tv.test.js +++ b/sites/mi.tv/mi.tv.test.js @@ -1,66 +1,67 @@ -const { parser, url } = require('./mi.tv.config.js') -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('2021-11-24', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'ar#24-7-canal-de-noticias', - xmltv_id: '247CanaldeNoticias.ar' -} -const content = - '' - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://mi.tv/ar/async/channel/24-7-canal-de-noticias/2021-11-24/0' - ) -}) - -it('can parse response', () => { - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-24T03:00:00.000Z', - stop: '2021-11-24T23:00:00.000Z', - title: 'Trasnoche de 24/7', - category: 'Interés general', - description: 'Lo más visto de la semana en nuestra pantalla.', - image: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg' - }, - { - start: '2021-11-24T23:00:00.000Z', - stop: '2021-11-25T01:00:00.000Z', - title: 'Noticiero central - Segunda edición', - category: 'Noticiero', - description: - 'Cerramos el día con un completo resumen de los temas más relevantes con columnistas y análisis especiales para terminar el día.', - image: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg' - }, - { - start: '2021-11-25T01:00:00.000Z', - stop: '2021-11-25T02:00:00.000Z', - title: 'Plus energético', - category: 'Cultural', - description: - 'La energía tiene mucho para mostrar. Este programa reúne a las principales empresas y protagonistas de la actividad que esta revolucionando la región.', - image: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./mi.tv.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('2021-11-24', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'ar#24-7-canal-de-noticias', + xmltv_id: '247CanaldeNoticias.ar' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://mi.tv/ar/async/channel/24-7-canal-de-noticias/2021-11-24/0' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-24T03:00:00.000Z', + stop: '2021-11-24T23:00:00.000Z', + title: 'Trasnoche de 24/7', + category: 'Interés general', + description: 'Lo más visto de la semana en nuestra pantalla.', + image: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg' + }, + { + start: '2021-11-24T23:00:00.000Z', + stop: '2021-11-25T01:00:00.000Z', + title: 'Noticiero central - Segunda edición', + category: 'Noticiero', + description: + 'Cerramos el día con un completo resumen de los temas más relevantes con columnistas y análisis especiales para terminar el día.', + image: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg' + }, + { + start: '2021-11-25T01:00:00.000Z', + stop: '2021-11-25T02:00:00.000Z', + title: 'Plus energético', + category: 'Cultural', + description: + 'La energía tiene mucho para mostrar. Este programa reúne a las principales empresas y protagonistas de la actividad que esta revolucionando la región.', + image: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mncvision.id/mncvision.id.config.js b/sites/mncvision.id/mncvision.id.config.js index 88f2b430..40896953 100644 --- a/sites/mncvision.id/mncvision.id.config.js +++ b/sites/mncvision.id/mncvision.id.config.js @@ -1,167 +1,167 @@ -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:mncvision.id') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -doFetch.setCheckResult(false).setDebugger(debug) - -const languages = { en: 'english', id: 'indonesia' } -const cookies = {} -const timeout = 30000 - -module.exports = { - site: 'mncvision.id', - days: 2, - url: 'https://www.mncvision.id/schedule/table', - request: { - method: 'POST', - data({ channel, date }) { - const formData = new URLSearchParams() - formData.append('search_model', 'channel') - formData.append('af0rmelement', 'aformelement') - formData.append('fdate', date.format('YYYY-MM-DD')) - formData.append('fchannel', channel.site_id) - formData.append('submit', 'Search') - - return formData - }, - async headers({ channel }) { - const headers = { - 'Content-Type': 'application/x-www-form-urlencoded' - } - if (channel) { - if (!cookies[channel.lang]) { - cookies[channel.lang] = await loadLangCookies(channel) - } - if (cookies[channel.lang]) { - headers.Cookie = cookies[channel.lang] - } - } - return headers - }, - jar: null - }, - async parser({ content, headers, date, channel }) { - if (!cookies[channel.lang]) { - cookies[channel.lang] = parseCookies(headers) - } - - return await parseItems(content, date, cookies[channel.lang]) - }, - async channels({ lang = 'id' }) { - const result = await axios - .get('https://www.mncvision.id/schedule') - .then(response => response.data) - .catch(console.error) - - const $ = cheerio.load(result) - const items = $('select[name="fchannel"] option').toArray() - const channels = items.map(item => { - const $item = $(item) - - return { - lang, - site_id: $item.attr('value'), - name: $item.text().split(' - ')[0].trim() - } - }) - - return channels - } -} - -function parseSeason($item) { - const title = parseTitle($item) - const [, season] = title.match(/ S(\d+)/) || [null, null] - - return season ? parseInt(season) : null -} - -function parseEpisode($item) { - const title = parseTitle($item) - const [, episode] = title.match(/ Ep (\d+)/) || [null, null] - - return episode ? parseInt(episode) : null -} - -function parseDuration($item) { - let duration = $item.find('td:nth-child(3)').text() - const match = duration.match(/(\d{2}):(\d{2})/) - const hours = parseInt(match[1]) - const minutes = parseInt(match[2]) - - return hours * 60 + minutes -} - -function parseStart($item, date) { - let time = $item.find('td:nth-child(1)').text() - time = `${date.format('DD/MM/YYYY')} ${time}` - - return dayjs.tz(time, 'DD/MM/YYYY HH:mm', 'Asia/Jakarta') -} - -function parseTitle($item) { - return $item.find('td:nth-child(2) > a').text() -} - -async function parseItems(content, date, cookies) { - const programs = [] - const $ = cheerio.load(content) - const items = $('tr[valign="top"]').toArray() - if (items.length) { - const queues = [] - for (const item of items) { - const $item = $(item) - const url = $item.find('a').attr('href') - const headers = { - 'X-Requested-With': 'XMLHttpRequest', - Cookie: cookies - } - queues.push({ i: $item, url, params: { headers, timeout } }) - } - await doFetch(queues, (queue, res) => { - const $item = queue.i - const description = res ? cheerio.load(res)('.synopsis').text().trim() : null - const start = parseStart($item, date) - const duration = parseDuration($item) - const stop = start.add(duration, 'm') - programs.push({ - title: parseTitle($item), - season: parseSeason($item), - episode: parseEpisode($item), - description: description && description !== '-' ? description : null, - start, - stop - }) - }) - } - - return programs -} - -function loadLangCookies(channel) { - const url = `https://www.mncvision.id/language_switcher/setlang/${languages[channel.lang]}/` - - return axios - .get(url, { timeout }) - .then(r => parseCookies(r.headers)) - .catch(error => console.error(error.message)) -} - -function parseCookies(headers) { - const cookies = [] - if (Array.isArray(headers['set-cookie'])) { - headers['set-cookie'].forEach(cookie => { - cookies.push(cookie.split('; ')[0]) - }) - } - return cookies.length ? cookies.join('; ') : null -} +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:mncvision.id') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +doFetch.setCheckResult(false).setDebugger(debug) + +const languages = { en: 'english', id: 'indonesia' } +const cookies = {} +const timeout = 30000 + +module.exports = { + site: 'mncvision.id', + days: 2, + url: 'https://www.mncvision.id/schedule/table', + request: { + method: 'POST', + data({ channel, date }) { + const formData = new URLSearchParams() + formData.append('search_model', 'channel') + formData.append('af0rmelement', 'aformelement') + formData.append('fdate', date.format('YYYY-MM-DD')) + formData.append('fchannel', channel.site_id) + formData.append('submit', 'Search') + + return formData + }, + async headers({ channel }) { + const headers = { + 'Content-Type': 'application/x-www-form-urlencoded' + } + if (channel) { + if (!cookies[channel.lang]) { + cookies[channel.lang] = await loadLangCookies(channel) + } + if (cookies[channel.lang]) { + headers.Cookie = cookies[channel.lang] + } + } + return headers + }, + jar: null + }, + async parser({ content, headers, date, channel }) { + if (!cookies[channel.lang]) { + cookies[channel.lang] = parseCookies(headers) + } + + return await parseItems(content, date, cookies[channel.lang]) + }, + async channels({ lang = 'id' }) { + const result = await axios + .get('https://www.mncvision.id/schedule') + .then(response => response.data) + .catch(console.error) + + const $ = cheerio.load(result) + const items = $('select[name="fchannel"] option').toArray() + const channels = items.map(item => { + const $item = $(item) + + return { + lang, + site_id: $item.attr('value'), + name: $item.text().split(' - ')[0].trim() + } + }) + + return channels + } +} + +function parseSeason($item) { + const title = parseTitle($item) + const [, season] = title.match(/ S(\d+)/) || [null, null] + + return season ? parseInt(season) : null +} + +function parseEpisode($item) { + const title = parseTitle($item) + const [, episode] = title.match(/ Ep (\d+)/) || [null, null] + + return episode ? parseInt(episode) : null +} + +function parseDuration($item) { + let duration = $item.find('td:nth-child(3)').text() + const match = duration.match(/(\d{2}):(\d{2})/) + const hours = parseInt(match[1]) + const minutes = parseInt(match[2]) + + return hours * 60 + minutes +} + +function parseStart($item, date) { + let time = $item.find('td:nth-child(1)').text() + time = `${date.format('DD/MM/YYYY')} ${time}` + + return dayjs.tz(time, 'DD/MM/YYYY HH:mm', 'Asia/Jakarta') +} + +function parseTitle($item) { + return $item.find('td:nth-child(2) > a').text() +} + +async function parseItems(content, date, cookies) { + const programs = [] + const $ = cheerio.load(content) + const items = $('tr[valign="top"]').toArray() + if (items.length) { + const queues = [] + for (const item of items) { + const $item = $(item) + const url = $item.find('a').attr('href') + const headers = { + 'X-Requested-With': 'XMLHttpRequest', + Cookie: cookies + } + queues.push({ i: $item, url, params: { headers, timeout } }) + } + await doFetch(queues, (queue, res) => { + const $item = queue.i + const description = res ? cheerio.load(res)('.synopsis').text().trim() : null + const start = parseStart($item, date) + const duration = parseDuration($item) + const stop = start.add(duration, 'm') + programs.push({ + title: parseTitle($item), + season: parseSeason($item), + episode: parseEpisode($item), + description: description && description !== '-' ? description : null, + start, + stop + }) + }) + } + + return programs +} + +function loadLangCookies(channel) { + const url = `https://www.mncvision.id/language_switcher/setlang/${languages[channel.lang]}/` + + return axios + .get(url, { timeout }) + .then(r => parseCookies(r.headers)) + .catch(error => console.error(error.message)) +} + +function parseCookies(headers) { + const cookies = [] + if (Array.isArray(headers['set-cookie'])) { + headers['set-cookie'].forEach(cookie => { + cookies.push(cookie.split('; ')[0]) + }) + } + return cookies.length ? cookies.join('; ') : null +} diff --git a/sites/mncvision.id/mncvision.id.test.js b/sites/mncvision.id/mncvision.id.test.js index 1ce385d6..481ffa95 100644 --- a/sites/mncvision.id/mncvision.id.test.js +++ b/sites/mncvision.id/mncvision.id.test.js @@ -1,132 +1,132 @@ -const { parser, url, request } = require('./mncvision.id.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('2023-11-19').startOf('d') -const channel = { - site_id: '154', - xmltv_id: 'AXN.id', - lang: 'id' -} -const indonesiaHeaders = { - 'set-cookie': [ - 's1nd0vL=uo6gsashc1rmloqbb50m6b13qkglfvpl; expires=Sat, 18-Nov-2023 20:45:02 GMT; Max-Age=7200; path=/; HttpOnly' - ] -} -const englishHeaders = { - 'set-cookie': [ - 's1nd0vL=imtot2v1cs0pbemaohj9fee3hlbqo699; expires=Sat, 18-Nov-2023 20:38:31 GMT; Max-Age=7200; path=/; HttpOnly' - ] -} - -axios.get.mockImplementation((url, opts) => { - if (url === 'https://www.mncvision.id/language_switcher/setlang/indonesia/') { - return Promise.resolve({ - headers: indonesiaHeaders - }) - } - if (url === 'https://www.mncvision.id/language_switcher/setlang/english/') { - return Promise.resolve({ - headers: englishHeaders - }) - } - if ( - url === 'https://www.mncvision.id/schedule/detail/20231119001500154/Blue-Bloods-S13-Ep-19/1' - ) { - const getCookie = headers => { - if (Array.isArray(headers['set-cookie'])) { - return headers['set-cookie'][0].split('; ')[0] - } - } - if (opts.headers['Cookie'] === getCookie(indonesiaHeaders)) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/program_id.html')) - }) - } - if (opts.headers['Cookie'] === getCookie(englishHeaders)) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/program_en.html')) - }) - } - } - - return Promise.resolve({ data: '' }) -}) - -it('can generate valid url', () => { - expect(url).toBe('https://www.mncvision.id/schedule/table') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', async () => { - expect(await request.headers({ channel })).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded' - }) -}) - -it('can generate valid request data', () => { - const data = request.data({ channel, date }) - expect(data.get('search_model')).toBe('channel') - expect(data.get('af0rmelement')).toBe('aformelement') - expect(data.get('fdate')).toBe('2023-11-19') - expect(data.get('fchannel')).toBe('154') - expect(data.get('submit')).toBe('Search') -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const indonesiaResults = ( - await parser({ date, content, channel, headers: indonesiaHeaders }) - ).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(indonesiaResults[0]).toMatchObject({ - start: '2023-11-18T17:15:00.000Z', - stop: '2023-11-18T18:05:00.000Z', - title: 'Blue Bloods S13, Ep 19', - episode: 19, - description: - 'Jamie bekerja sama dengan FDNY untuk menemukan pelaku pembakaran yang bertanggung jawab atas kebakaran hebat yang terjadi di fasilitas penyimpanan bukti milik NYPD.' - }) - - const englishResults = ( - await parser({ date, content, channel: { ...channel, lang: 'en' }, headers: englishHeaders }) - ).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(englishResults[0]).toMatchObject({ - start: '2023-11-18T17:15:00.000Z', - stop: '2023-11-18T18:05:00.000Z', - title: 'Blue Bloods S13, Ep 19', - episode: 19, - description: - 'Jamie partners with the FDNY to find the arsonist responsible for a massive fire at an NYPD evidence storage facility.' - }) -}) - -it('can handle empty guide', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - const results = await parser({ - date, - channel, - content, - headers: indonesiaHeaders - }) - expect(results).toMatchObject([]) -}) +const { parser, url, request } = require('./mncvision.id.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('2023-11-19').startOf('d') +const channel = { + site_id: '154', + xmltv_id: 'AXN.id', + lang: 'id' +} +const indonesiaHeaders = { + 'set-cookie': [ + 's1nd0vL=uo6gsashc1rmloqbb50m6b13qkglfvpl; expires=Sat, 18-Nov-2023 20:45:02 GMT; Max-Age=7200; path=/; HttpOnly' + ] +} +const englishHeaders = { + 'set-cookie': [ + 's1nd0vL=imtot2v1cs0pbemaohj9fee3hlbqo699; expires=Sat, 18-Nov-2023 20:38:31 GMT; Max-Age=7200; path=/; HttpOnly' + ] +} + +axios.get.mockImplementation((url, opts) => { + if (url === 'https://www.mncvision.id/language_switcher/setlang/indonesia/') { + return Promise.resolve({ + headers: indonesiaHeaders + }) + } + if (url === 'https://www.mncvision.id/language_switcher/setlang/english/') { + return Promise.resolve({ + headers: englishHeaders + }) + } + if ( + url === 'https://www.mncvision.id/schedule/detail/20231119001500154/Blue-Bloods-S13-Ep-19/1' + ) { + const getCookie = headers => { + if (Array.isArray(headers['set-cookie'])) { + return headers['set-cookie'][0].split('; ')[0] + } + } + if (opts.headers['Cookie'] === getCookie(indonesiaHeaders)) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/program_id.html')) + }) + } + if (opts.headers['Cookie'] === getCookie(englishHeaders)) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/program_en.html')) + }) + } + } + + return Promise.resolve({ data: '' }) +}) + +it('can generate valid url', () => { + expect(url).toBe('https://www.mncvision.id/schedule/table') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', async () => { + expect(await request.headers({ channel })).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded' + }) +}) + +it('can generate valid request data', () => { + const data = request.data({ channel, date }) + expect(data.get('search_model')).toBe('channel') + expect(data.get('af0rmelement')).toBe('aformelement') + expect(data.get('fdate')).toBe('2023-11-19') + expect(data.get('fchannel')).toBe('154') + expect(data.get('submit')).toBe('Search') +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const indonesiaResults = ( + await parser({ date, content, channel, headers: indonesiaHeaders }) + ).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(indonesiaResults[0]).toMatchObject({ + start: '2023-11-18T17:15:00.000Z', + stop: '2023-11-18T18:05:00.000Z', + title: 'Blue Bloods S13, Ep 19', + episode: 19, + description: + 'Jamie bekerja sama dengan FDNY untuk menemukan pelaku pembakaran yang bertanggung jawab atas kebakaran hebat yang terjadi di fasilitas penyimpanan bukti milik NYPD.' + }) + + const englishResults = ( + await parser({ date, content, channel: { ...channel, lang: 'en' }, headers: englishHeaders }) + ).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(englishResults[0]).toMatchObject({ + start: '2023-11-18T17:15:00.000Z', + stop: '2023-11-18T18:05:00.000Z', + title: 'Blue Bloods S13, Ep 19', + episode: 19, + description: + 'Jamie partners with the FDNY to find the arsonist responsible for a massive fire at an NYPD evidence storage facility.' + }) +}) + +it('can handle empty guide', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + const results = await parser({ + date, + channel, + content, + headers: indonesiaHeaders + }) + expect(results).toMatchObject([]) +}) diff --git a/sites/moji.id/__data__/content.html b/sites/moji.id/__data__/content.html new file mode 100644 index 00000000..1870f972 --- /dev/null +++ b/sites/moji.id/__data__/content.html @@ -0,0 +1 @@ +

    schedule

    FriAug 18
    SatAug 19
    SunAug 20
    Jam TayangProgram
    00:00TRUST
    Informasi seputar menjaga vitalitas pria
    00:302023 AVC CHALLENGE CUP FOR WOMEN (RECORDED)
    India Vs. Vietnam
    02:30ONE CHAMPIONSHIP 2021
    Siaran laga-laga pertandingan tinju gaya bebas internasional. Meyuguhkan pertarungan sengit dari para petarung profeisional kelas dunia.
    03:30VOLLEYBALL NATION\'S LEAGUE 2023 (RECORDED)
    TURKI vs BRAZIL
    05:00MOJI SPORT
    MOJI SPORT
    06:15LIPUTAN 6 PAGI MOJI
    Kompilasi ragam berita hard news dan soft news baik dari dalam negeri maupun internasional juga info prediksi cuaca di wilayah Indonesia
    07:00UNGKAP
    Liputan investigasi seputar berbagai topik dan peristiwa hangat serta kontroversial yang terjadi di Indonesia
    08:00PIALA KAPOLRI 2023 PUTRI (LIVE)
    PIALA KAPOLRI 2023 PUTRI (LIVE)
    10:30SERIES PAGI
    GANTENG GANTENG SERIGALA
    12:30DIAM-DIAM SUKA
    DIAM-DIAM SUKA
    13:30PIALA KAPOLRI 2023 PUTRA (LIVE)
    PIALA KAPOLRI 2023 PUTRA (LIVE)
    16:00PIALA KAPOLRI 2023 PUTRI (LIVE)
    PIALA KAPOLRI 2023 PUTRI (LIVE)
    18:00PIALA KAPOLRI 2023 PUTRA (LIVE)
    PIALA KAPOLRI 2023 PUTRA (LIVE)
    20:00MOJI DRAMA (CHHOTI SARDARNI)
    CHHOTI SARDARNI
    21:30SINEMA MALAM (BIDADARI CANTIK DI RUMAH KOST)
    (BIDADARI CANTIK DI RUMAH KOST
    23:00TRUST
    Informasi seputar menjaga vitalitas pria
    23:30TRUST
    Informasi seputar menjaga vitalitas pria
    \ No newline at end of file diff --git a/sites/moji.id/moji.id.config.js b/sites/moji.id/moji.id.config.js index ee6d6db2..f6205a1c 100644 --- a/sites/moji.id/moji.id.config.js +++ b/sites/moji.id/moji.id.config.js @@ -1,103 +1,103 @@ -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) - -const currentYear = new Date().getFullYear() -const tz = 'Asia/Jakarta' - -module.exports = { - site: 'moji.id', - days: 2, - url: 'https://moji.id/schedule', - logo: function (context) { - return context.channel.logo - }, - parser: function (context) { - const programs = [] - const items = parseItems(context) - - items.forEach(item => { - programs.push({ - title: item.progTitle, - description: item.progDesc, - start: item.progStart, - stop: item.progStop - }) - }) - - return programs - } -} - -function parseItems(context) { - const $ = cheerio.load(context.content) - const schDayMonths = $('.date-slider .month').toArray() - const schPrograms = $('.desc-slider .list-slider').toArray() - const monthDate = dayjs(context.date).format('MMM DD') - const items = [] - - schDayMonths.forEach((schDayMonth, i) => { - if (monthDate == $(schDayMonth).text()) { - const schDayPrograms = $(schPrograms[i]).find('.accordion').toArray() - schDayPrograms.forEach((program, i) => { - const itemDay = { - progStart: parseStart($(schDayMonth), $(program)), - progStop: parseStop( - $(schDayMonth), - schDayPrograms[i + 1] ? $(schDayPrograms[i + 1]) : null - ), - progTitle: parseTitle($(program)), - progDesc: parseDescription($(program)) - } - items.push(itemDay) - }) - } - }) - - return items -} - -function parseTitle(item) { - return item.find('.name-prog').text() -} - -function parseDescription(item) { - return item.find('.content-acc span').text() -} - -function parseStart(schDayMonth, item) { - const monthDate = schDayMonth.text().split(' ') - const startTime = item.find('.pkl').text() - - return dayjs.tz( - `${currentYear}-${monthDate[0]}-${monthDate[1]} ${startTime}`, - 'YYYY-MMM-DD HH:mm', - tz - ) -} - -function parseStop(schDayMonth, itemNext) { - const monthDate = schDayMonth.text().split(' ') - if (itemNext) { - const stopTime = itemNext.find('.pkl').text() - return dayjs.tz( - `${currentYear}-${monthDate[0]}-${monthDate[1]} ${stopTime}`, - 'YYYY-MMM-DD HH:mm', - tz - ) - } else { - return dayjs.tz( - `${currentYear}-${monthDate[0]}-${(parseInt(monthDate[1]) + 1) - .toString() - .padStart(2, '0')} 00:00`, - 'YYYY-MMM-DD HH:mm', - tz - ) - } -} +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) + +const currentYear = new Date().getFullYear() +const tz = 'Asia/Jakarta' + +module.exports = { + site: 'moji.id', + days: 2, + url: 'https://moji.id/schedule', + logo: function (context) { + return context.channel.logo + }, + parser: function (context) { + const programs = [] + const items = parseItems(context) + + items.forEach(item => { + programs.push({ + title: item.progTitle, + description: item.progDesc, + start: item.progStart, + stop: item.progStop + }) + }) + + return programs + } +} + +function parseItems(context) { + const $ = cheerio.load(context.content) + const schDayMonths = $('.date-slider .month').toArray() + const schPrograms = $('.desc-slider .list-slider').toArray() + const monthDate = dayjs(context.date).format('MMM DD') + const items = [] + + schDayMonths.forEach((schDayMonth, i) => { + if (monthDate == $(schDayMonth).text()) { + const schDayPrograms = $(schPrograms[i]).find('.accordion').toArray() + schDayPrograms.forEach((program, i) => { + const itemDay = { + progStart: parseStart($(schDayMonth), $(program)), + progStop: parseStop( + $(schDayMonth), + schDayPrograms[i + 1] ? $(schDayPrograms[i + 1]) : null + ), + progTitle: parseTitle($(program)), + progDesc: parseDescription($(program)) + } + items.push(itemDay) + }) + } + }) + + return items +} + +function parseTitle(item) { + return item.find('.name-prog').text() +} + +function parseDescription(item) { + return item.find('.content-acc span').text() +} + +function parseStart(schDayMonth, item) { + const monthDate = schDayMonth.text().split(' ') + const startTime = item.find('.pkl').text() + + return dayjs.tz( + `${currentYear}-${monthDate[0]}-${monthDate[1]} ${startTime}`, + 'YYYY-MMM-DD HH:mm', + tz + ) +} + +function parseStop(schDayMonth, itemNext) { + const monthDate = schDayMonth.text().split(' ') + if (itemNext) { + const stopTime = itemNext.find('.pkl').text() + return dayjs.tz( + `${currentYear}-${monthDate[0]}-${monthDate[1]} ${stopTime}`, + 'YYYY-MMM-DD HH:mm', + tz + ) + } else { + return dayjs.tz( + `${currentYear}-${monthDate[0]}-${(parseInt(monthDate[1]) + 1) + .toString() + .padStart(2, '0')} 00:00`, + 'YYYY-MMM-DD HH:mm', + tz + ) + } +} diff --git a/sites/moji.id/moji.id.test.js b/sites/moji.id/moji.id.test.js index 452a7072..589931e6 100644 --- a/sites/moji.id/moji.id.test.js +++ b/sites/moji.id/moji.id.test.js @@ -1,29 +1,29 @@ -const { parser } = require('./moji.id.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2023-08-18', 'YYYY-MM-DD').startOf('d') - -const content = - '

    schedule

    FriAug 18
    SatAug 19
    SunAug 20
    Jam TayangProgram
    00:00TRUST
    Informasi seputar menjaga vitalitas pria
    00:302023 AVC CHALLENGE CUP FOR WOMEN (RECORDED)
    India Vs. Vietnam
    02:30ONE CHAMPIONSHIP 2021
    Siaran laga-laga pertandingan tinju gaya bebas internasional. Meyuguhkan pertarungan sengit dari para petarung profeisional kelas dunia.
    03:30VOLLEYBALL NATION\'S LEAGUE 2023 (RECORDED)
    TURKI vs BRAZIL
    05:00MOJI SPORT
    MOJI SPORT
    06:15LIPUTAN 6 PAGI MOJI
    Kompilasi ragam berita hard news dan soft news baik dari dalam negeri maupun internasional juga info prediksi cuaca di wilayah Indonesia
    07:00UNGKAP
    Liputan investigasi seputar berbagai topik dan peristiwa hangat serta kontroversial yang terjadi di Indonesia
    08:00PIALA KAPOLRI 2023 PUTRI (LIVE)
    PIALA KAPOLRI 2023 PUTRI (LIVE)
    10:30SERIES PAGI
    GANTENG GANTENG SERIGALA
    12:30DIAM-DIAM SUKA
    DIAM-DIAM SUKA
    13:30PIALA KAPOLRI 2023 PUTRA (LIVE)
    PIALA KAPOLRI 2023 PUTRA (LIVE)
    16:00PIALA KAPOLRI 2023 PUTRI (LIVE)
    PIALA KAPOLRI 2023 PUTRI (LIVE)
    18:00PIALA KAPOLRI 2023 PUTRA (LIVE)
    PIALA KAPOLRI 2023 PUTRA (LIVE)
    20:00MOJI DRAMA (CHHOTI SARDARNI)
    CHHOTI SARDARNI
    21:30SINEMA MALAM (BIDADARI CANTIK DI RUMAH KOST)
    (BIDADARI CANTIK DI RUMAH KOST
    23:00TRUST
    Informasi seputar menjaga vitalitas pria
    23:30TRUST
    Informasi seputar menjaga vitalitas pria
    ' - -it('can handle empty guide', () => { - const results = parser({ content: '' }) - expect(results).toMatchObject([]) -}) - -it('can parse response', () => { - const results = parser({ content, date }).map(p => { - p.start = p.start.year(2023).toJSON() - p.stop = p.stop.year(2023).toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - title: 'TRUST', - start: '2023-08-17T17:00:00.000Z', - stop: '2023-08-17T17:30:00.000Z', - description: 'Informasi seputar menjaga vitalitas pria' - }) -}) +const { parser } = require('./moji.id.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2023-08-18', 'YYYY-MM-DD').startOf('d') + +it('can handle empty guide', () => { + const results = parser({ content: '' }) + expect(results).toMatchObject([]) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + const results = parser({ content, date }).map(p => { + p.start = p.start.year(2023).toJSON() + p.stop = p.stop.year(2023).toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + title: 'TRUST', + start: '2023-08-17T17:00:00.000Z', + stop: '2023-08-17T17:30:00.000Z', + description: 'Informasi seputar menjaga vitalitas pria' + }) +}) diff --git a/sites/mojmaxtv.hrvatskitelekom.hr/mojmaxtv.hrvatskitelekom.hr.config.js b/sites/mojmaxtv.hrvatskitelekom.hr/mojmaxtv.hrvatskitelekom.hr.config.js index db783843..bbad1e6c 100644 --- a/sites/mojmaxtv.hrvatskitelekom.hr/mojmaxtv.hrvatskitelekom.hr.config.js +++ b/sites/mojmaxtv.hrvatskitelekom.hr/mojmaxtv.hrvatskitelekom.hr.config.js @@ -1,192 +1,192 @@ -const doFetch = require('@ntlab/sfetch') -const axios = require('axios') -const dayjs = require('dayjs') -const _ = require('lodash') -const crypto = require('crypto') - -// API Configuration Constants -const NATCO_CODE = 'hr' -const APP_LANGUAGE = 'hr' -const APP_KEY = 'GWaBW4RTloLwpUgYVzOiW5zUxFLmoMj5' -const APP_VERSION = '02.0.1080' -const NATCO_KEY = 'l2lyvGVbUm2EKJE96ImQgcc8PKMZWtbE' -const SITE_URL = 'mojmaxtv.hrvatskitelekom.hr' - -// Role Types -const ROLE_TYPES = { - ACTOR: 'GLUMI', // Croatian for "ACTS" - DIRECTOR: 'REŽIJA', // Croatian for "DIRECTOR" - PRODUCER: 'PRODUKCIJA', // Croatian for "PRODUCER" - WRITER: 'AUTOR', - SCENARIO: 'SCENARIJ' -} - -// Dynamic API Endpoint based on NATCO_CODE -const API_ENDPOINT = `https://tv-${NATCO_CODE}-prod.yo-digital.com/${NATCO_CODE}-bifrost` - -// Session/Device IDs -const DEVICE_ID = crypto.randomUUID() -const SESSION_ID = crypto.randomUUID() - -const cached = {} - -const getHeaders = () => ({ - 'app_key': APP_KEY, - 'app_version': APP_VERSION, - 'device-id': DEVICE_ID, - 'tenant': 'tv', - 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36', - 'origin': `https://${SITE_URL}`, - 'x-request-session-id': SESSION_ID, - 'x-request-tracking-id': crypto.randomUUID(), - 'x-tv-step': 'EPG_SCHEDULES', - 'x-tv-flow': 'EPG', - 'x-call-type': 'GUEST_USER', - 'x-user-agent': `web|web|Chrome-133|${APP_VERSION}|1` -}) - -module.exports = { - site: SITE_URL, - url({ date }) { - return `${API_ENDPOINT}/epg/channel/schedules?date=${date.format( - 'YYYY-MM-DD' - )}&hour_offset=0&hour_range=3&channelMap_id&filler=true&app_language=${APP_LANGUAGE}&natco_code=${NATCO_CODE}` - }, - request: { - headers: getHeaders(), - cache: { - ttl: 24 * 60 * 60 * 1000 // 1 day - } - }, - async parser({ content, channel, date }) { - const data = parseData(content) - if (!data) return [] - - let items = parseItems(data, channel) - if (!items.length) return [] - - const queue = [3, 6, 9, 12, 15, 18, 21] - .map(offset => { - const url = module.exports.url({ date }).replace('hour_offset=0', `hour_offset=${offset}`) - const params = { ...module.exports.request, headers: getHeaders() } - - if (cached[url]) { - items = items.concat(parseItems(cached[url], channel)) - return null - } - - return { url, params } - }) - .filter(Boolean) - - await doFetch(queue, (_req, _data) => { - if (_data) { - cached[_req.url] = _data - items = items.concat(parseItems(_data, channel)) - } - }) - - items = _.sortBy(items, i => dayjs(i.start_time).valueOf()) - - // Fetch program details for each item - const programs = [] - for (let item of items) { - const detail = await loadProgramDetails(item) - -// detectUnknownRoles(detail) - - programs.push({ - title: item.description, - sub_title: item.episode_name, - description: parseDescription(detail), - categories: Array.isArray(item.genres) ? item.genres.map(g => g.name) : [], - date: parseDate(item), - image: detail.poster_image_url, - actors: parseRoles(detail, ROLE_TYPES.ACTOR), - directors: parseRoles(detail, ROLE_TYPES.DIRECTOR), - producers: parseRoles(detail, ROLE_TYPES.PRODUCER), - season: parseSeason(item), - episode: parseEpisode(item), - rating: parseRating(item), - start: item.start_time, - stop: item.end_time - }) - } - - return programs - }, - async channels() { - const data = await axios - .get( - `${API_ENDPOINT}/epg/channel?channelMap_id=&includeVirtualChannels=false&natco_key=${NATCO_KEY}&app_language=${APP_LANGUAGE}&natco_code=${NATCO_CODE}`, - { ...module.exports.request, headers: getHeaders() } - ) - .then(r => r.data) - .catch(console.error) - - return data.channels.map(channel => ({ - lang: NATCO_CODE, - name: channel.title, - site_id: channel.station_id - })) - } -} - -async function loadProgramDetails(item) { - if (!item.program_id) return {} - const url = `${API_ENDPOINT}/details/series/${item.program_id}?natco_code=${NATCO_CODE}` - const data = await axios - .get(url, { headers: getHeaders() }) - .then(r => r.data) - .catch(console.log) - - return data || {} -} - -function parseData(content) { - try { - const data = JSON.parse(content) - return data || null - } catch { - return null - } -} - -function parseItems(data, channel) { - if (!data.channels || !Array.isArray(data.channels[channel.site_id])) return [] - return data.channels[channel.site_id] -} - -function parseDate(item) { - return item && item.release_year ? item.release_year.toString() : null -} - -function parseRating(item) { - return item.ratings - ? { - system: 'MPA', - value: item.ratings - } - : null -} - -function parseSeason(item) { - if (item.season_display_number === 'Epizode') return null // 'Epizode' is 'Episodes' in Croatian - return item.season_number -} - -function parseEpisode(item) { - if (item.episode_number) return parseInt(item.episode_number) - if (item.season_display_number === 'Epizode') return item.season_number - return null -} - -function parseDescription(item) { - if (!item.details) return null - return item.details.description -} - -function parseRoles(item, role_name) { - if (!item.roles) return null - return item.roles.filter(role => role.role_name === role_name).map(role => role.person_name) -} +const doFetch = require('@ntlab/sfetch') +const axios = require('axios') +const dayjs = require('dayjs') +const crypto = require('crypto') +const sortBy = require('lodash.sortby') + +// API Configuration Constants +const NATCO_CODE = 'hr' +const APP_LANGUAGE = 'hr' +const APP_KEY = 'GWaBW4RTloLwpUgYVzOiW5zUxFLmoMj5' +const APP_VERSION = '02.0.1080' +const NATCO_KEY = 'l2lyvGVbUm2EKJE96ImQgcc8PKMZWtbE' +const SITE_URL = 'mojmaxtv.hrvatskitelekom.hr' + +// Role Types +const ROLE_TYPES = { + ACTOR: 'GLUMI', // Croatian for "ACTS" + DIRECTOR: 'REŽIJA', // Croatian for "DIRECTOR" + PRODUCER: 'PRODUKCIJA', // Croatian for "PRODUCER" + WRITER: 'AUTOR', + SCENARIO: 'SCENARIJ' +} + +// Dynamic API Endpoint based on NATCO_CODE +const API_ENDPOINT = `https://tv-${NATCO_CODE}-prod.yo-digital.com/${NATCO_CODE}-bifrost` + +// Session/Device IDs +const DEVICE_ID = crypto.randomUUID() +const SESSION_ID = crypto.randomUUID() + +const cached = {} + +const getHeaders = () => ({ + 'app_key': APP_KEY, + 'app_version': APP_VERSION, + 'device-id': DEVICE_ID, + 'tenant': 'tv', + 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36', + 'origin': `https://${SITE_URL}`, + 'x-request-session-id': SESSION_ID, + 'x-request-tracking-id': crypto.randomUUID(), + 'x-tv-step': 'EPG_SCHEDULES', + 'x-tv-flow': 'EPG', + 'x-call-type': 'GUEST_USER', + 'x-user-agent': `web|web|Chrome-133|${APP_VERSION}|1` +}) + +module.exports = { + site: SITE_URL, + url({ date }) { + return `${API_ENDPOINT}/epg/channel/schedules?date=${date.format( + 'YYYY-MM-DD' + )}&hour_offset=0&hour_range=3&channelMap_id&filler=true&app_language=${APP_LANGUAGE}&natco_code=${NATCO_CODE}` + }, + request: { + headers: getHeaders(), + cache: { + ttl: 24 * 60 * 60 * 1000 // 1 day + } + }, + async parser({ content, channel, date }) { + const data = parseData(content) + if (!data) return [] + + let items = parseItems(data, channel) + if (!items.length) return [] + + const queue = [3, 6, 9, 12, 15, 18, 21] + .map(offset => { + const url = module.exports.url({ date }).replace('hour_offset=0', `hour_offset=${offset}`) + const params = { ...module.exports.request, headers: getHeaders() } + + if (cached[url]) { + items = items.concat(parseItems(cached[url], channel)) + return null + } + + return { url, params } + }) + .filter(Boolean) + + await doFetch(queue, (_req, _data) => { + if (_data) { + cached[_req.url] = _data + items = items.concat(parseItems(_data, channel)) + } + }) + + items = sortBy(items, i => dayjs(i.start_time).valueOf()) + + // Fetch program details for each item + const programs = [] + for (let item of items) { + const detail = await loadProgramDetails(item) + +// detectUnknownRoles(detail) + + programs.push({ + title: item.description, + sub_title: item.episode_name, + description: parseDescription(detail), + categories: Array.isArray(item.genres) ? item.genres.map(g => g.name) : [], + date: parseDate(item), + image: detail.poster_image_url, + actors: parseRoles(detail, ROLE_TYPES.ACTOR), + directors: parseRoles(detail, ROLE_TYPES.DIRECTOR), + producers: parseRoles(detail, ROLE_TYPES.PRODUCER), + season: parseSeason(item), + episode: parseEpisode(item), + rating: parseRating(item), + start: item.start_time, + stop: item.end_time + }) + } + + return programs + }, + async channels() { + const data = await axios + .get( + `${API_ENDPOINT}/epg/channel?channelMap_id=&includeVirtualChannels=false&natco_key=${NATCO_KEY}&app_language=${APP_LANGUAGE}&natco_code=${NATCO_CODE}`, + { ...module.exports.request, headers: getHeaders() } + ) + .then(r => r.data) + .catch(console.error) + + return data.channels.map(channel => ({ + lang: NATCO_CODE, + name: channel.title, + site_id: channel.station_id + })) + } +} + +async function loadProgramDetails(item) { + if (!item.program_id) return {} + const url = `${API_ENDPOINT}/details/series/${item.program_id}?natco_code=${NATCO_CODE}` + const data = await axios + .get(url, { headers: getHeaders() }) + .then(r => r.data) + .catch(console.log) + + return data || {} +} + +function parseData(content) { + try { + const data = JSON.parse(content) + return data || null + } catch { + return null + } +} + +function parseItems(data, channel) { + if (!data.channels || !Array.isArray(data.channels[channel.site_id])) return [] + return data.channels[channel.site_id] +} + +function parseDate(item) { + return item && item.release_year ? item.release_year.toString() : null +} + +function parseRating(item) { + return item.ratings + ? { + system: 'MPA', + value: item.ratings + } + : null +} + +function parseSeason(item) { + if (item.season_display_number === 'Epizode') return null // 'Epizode' is 'Episodes' in Croatian + return item.season_number +} + +function parseEpisode(item) { + if (item.episode_number) return parseInt(item.episode_number) + if (item.season_display_number === 'Epizode') return item.season_number + return null +} + +function parseDescription(item) { + if (!item.details) return null + return item.details.description +} + +function parseRoles(item, role_name) { + if (!item.roles) return null + return item.roles.filter(role => role.role_name === role_name).map(role => role.person_name) +} diff --git a/sites/mojmaxtv.hrvatskitelekom.hr/mojmaxtv.hrvatskitelekom.hr.test.js b/sites/mojmaxtv.hrvatskitelekom.hr/mojmaxtv.hrvatskitelekom.hr.test.js index c91a7eb9..4e4c228c 100644 --- a/sites/mojmaxtv.hrvatskitelekom.hr/mojmaxtv.hrvatskitelekom.hr.test.js +++ b/sites/mojmaxtv.hrvatskitelekom.hr/mojmaxtv.hrvatskitelekom.hr.test.js @@ -1,85 +1,85 @@ -const { parser, url, request } = require('./mojmaxtv.hrvatskitelekom.hr.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) - -const date = dayjs.utc('2025-01-24', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: '274913832105', xmltv_id: 'HRT1.hr' } - -jest.mock('axios') - -axios.get.mockImplementation(url => { - if ( - url === - 'https://tv-hr-prod.yo-digital.com/hr-bifrost/epg/channel/schedules?date=2025-01-24&hour_offset=3&hour_range=3&channelMap_id&filler=true&app_language=hr&natco_code=hr' - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_3.json'))) - }) - } else if ( - url === - 'https://tv-hr-prod.yo-digital.com/hr-bifrost/epg/channel/schedules?date=2025-01-24&hour_offset=21&hour_range=3&channelMap_id&filler=true&app_language=hr&natco_code=hr' - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_21.json'))) - }) - } else { - return Promise.resolve({ - data: {} - }) - } -}) - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://tv-hr-prod.yo-digital.com/hr-bifrost/epg/channel/schedules?date=2025-01-24&hour_offset=0&hour_range=3&channelMap_id&filler=true&app_language=hr&natco_code=hr' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - app_key: 'GWaBW4RTloLwpUgYVzOiW5zUxFLmoMj5', - app_version: '02.0.1080', - }) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0.json')) - - const results = await parser({ content, channel, date }) - - expect(results.length).toBe(17) - expect(results[0]).toMatchObject({ - title: 'Planet Zemlja: Junaci', - categories: ['Dokumentarni'], - season: 3, - episode: 8, - date: '2023', - start: '2025-01-23T23:16:00.00Z', - stop: '2025-01-24T00:08:00.00Z' - }) - expect(results[16]).toMatchObject({ - title: 'Harry Haft, film', - categories: ['Film', 'Drama', 'Biografski'], - season: null, - episode: null, - date: '2021', - start: '2025-01-24T21:50:00.00Z', - stop: '2025-01-25T00:00:00.00Z' - }) -}) - -it('can handle empty guide', async () => { - const results = await parser({ - date, - channel, - content: '{}' - }) - - expect(results).toMatchObject([]) -}) +const { parser, url, request } = require('./mojmaxtv.hrvatskitelekom.hr.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) + +const date = dayjs.utc('2025-01-24', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: '274913832105', xmltv_id: 'HRT1.hr' } + +jest.mock('axios') + +axios.get.mockImplementation(url => { + if ( + url === + 'https://tv-hr-prod.yo-digital.com/hr-bifrost/epg/channel/schedules?date=2025-01-24&hour_offset=3&hour_range=3&channelMap_id&filler=true&app_language=hr&natco_code=hr' + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_3.json'))) + }) + } else if ( + url === + 'https://tv-hr-prod.yo-digital.com/hr-bifrost/epg/channel/schedules?date=2025-01-24&hour_offset=21&hour_range=3&channelMap_id&filler=true&app_language=hr&natco_code=hr' + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_21.json'))) + }) + } else { + return Promise.resolve({ + data: {} + }) + } +}) + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://tv-hr-prod.yo-digital.com/hr-bifrost/epg/channel/schedules?date=2025-01-24&hour_offset=0&hour_range=3&channelMap_id&filler=true&app_language=hr&natco_code=hr' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + app_key: 'GWaBW4RTloLwpUgYVzOiW5zUxFLmoMj5', + app_version: '02.0.1080', + }) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0.json')) + + const results = await parser({ content, channel, date }) + + expect(results.length).toBe(17) + expect(results[0]).toMatchObject({ + title: 'Planet Zemlja: Junaci', + categories: ['Dokumentarni'], + season: 3, + episode: 8, + date: '2023', + start: '2025-01-23T23:16:00.00Z', + stop: '2025-01-24T00:08:00.00Z' + }) + expect(results[16]).toMatchObject({ + title: 'Harry Haft, film', + categories: ['Film', 'Drama', 'Biografski'], + season: null, + episode: null, + date: '2021', + start: '2025-01-24T21:50:00.00Z', + stop: '2025-01-25T00:00:00.00Z' + }) +}) + +it('can handle empty guide', async () => { + const results = await parser({ + date, + channel, + content: '{}' + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/mon-programme-tv.be/mon-programme-tv.be.config.js b/sites/mon-programme-tv.be/mon-programme-tv.be.config.js index 23307f4f..7e105adc 100644 --- a/sites/mon-programme-tv.be/mon-programme-tv.be.config.js +++ b/sites/mon-programme-tv.be/mon-programme-tv.be.config.js @@ -1,102 +1,102 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'mon-programme-tv.be', - days: 2, - url({ date, channel }) { - return `https://www.mon-programme-tv.be/chaine/${date.format('DDMMYYYY')}/${ - channel.site_id - }.html` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - category: parseCategory($item), - image: parseImage($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://www.mon-programme-tv.be/chaine/toutes-les-chaines-television.html') - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(data) - - const channels = [] - $('.list-chaines > ul > li').each((i, el) => { - const [, site_id] = $(el) - .find('a') - .attr('href') - .match(/\/chaine\/(.*).html/) || [null, null] - const [, name] = $(el) - .find('a') - .attr('title') - .match(/Programme TV ce soir (.*)/) || [null, null] - - if (!site_id || !name) return - - channels.push({ - site_id, - name, - lang: 'fr' - }) - }) - - return channels - } -} - -function parseTitle($item) { - return $item('.title').text().trim() -} - -function parseDescription($item) { - return $item('.episode').text().trim() -} - -function parseCategory($item) { - return $item('.type').text().trim() -} - -function parseImage($item) { - return $item('.image img').data('src') -} - -function parseStart($item, date) { - const time = $item('.hour').text().trim() - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Brussels') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.box').toArray() -} +const cheerio = require('cheerio') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'mon-programme-tv.be', + days: 2, + url({ date, channel }) { + return `https://www.mon-programme-tv.be/chaine/${date.format('DDMMYYYY')}/${ + channel.site_id + }.html` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + category: parseCategory($item), + image: parseImage($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://www.mon-programme-tv.be/chaine/toutes-les-chaines-television.html') + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(data) + + const channels = [] + $('.list-chaines > ul > li').each((i, el) => { + const [, site_id] = $(el) + .find('a') + .attr('href') + .match(/\/chaine\/(.*).html/) || [null, null] + const [, name] = $(el) + .find('a') + .attr('title') + .match(/Programme TV ce soir (.*)/) || [null, null] + + if (!site_id || !name) return + + channels.push({ + site_id, + name, + lang: 'fr' + }) + }) + + return channels + } +} + +function parseTitle($item) { + return $item('.title').text().trim() +} + +function parseDescription($item) { + return $item('.episode').text().trim() +} + +function parseCategory($item) { + return $item('.type').text().trim() +} + +function parseImage($item) { + return $item('.image img').data('src') +} + +function parseStart($item, date) { + const time = $item('.hour').text().trim() + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Brussels') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.box').toArray() +} diff --git a/sites/mon-programme-tv.be/mon-programme-tv.be.test.js b/sites/mon-programme-tv.be/mon-programme-tv.be.test.js index c6dc381a..10c6e2b0 100644 --- a/sites/mon-programme-tv.be/mon-programme-tv.be.test.js +++ b/sites/mon-programme-tv.be/mon-programme-tv.be.test.js @@ -1,63 +1,63 @@ -const { parser, url } = require('./mon-programme-tv.be.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('2023-01-19', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1873/programme-television-ln24', - xmltv_id: 'LN24.be' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://www.mon-programme-tv.be/chaine/19012023/1873/programme-television-ln24.html' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-19T05:30:00.000Z', - stop: '2023-01-19T05:55:00.000Z', - title: 'LN Matin', - category: 'Magazine Actualité', - image: 'https://dnsmptv-img.pragma-consult.be/imgs/picto/132/Reportage_1.jpg' - }) - - expect(results[1]).toMatchObject({ - start: '2023-01-19T05:55:00.000Z', - stop: '2023-01-19T06:00:00.000Z', - title: 'Météo', - category: 'Météo', - image: 'https://dnsmptv-img.pragma-consult.be/imgs/picto/132/Meteo.jpg' - }) - - expect(results[8]).toMatchObject({ - start: '2023-01-19T08:00:00.000Z', - stop: '2023-01-19T08:05:00.000Z', - title: 'Le journal', - description: "L'information de la mi-journée avec des JT...", - category: 'Journal', - image: 'https://dnsmptv-img.pragma-consult.be/imgs/picto/132/journal.jpg' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')), - date - }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./mon-programme-tv.be.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('2023-01-19', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1873/programme-television-ln24', + xmltv_id: 'LN24.be' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://www.mon-programme-tv.be/chaine/19012023/1873/programme-television-ln24.html' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-19T05:30:00.000Z', + stop: '2023-01-19T05:55:00.000Z', + title: 'LN Matin', + category: 'Magazine Actualité', + image: 'https://dnsmptv-img.pragma-consult.be/imgs/picto/132/Reportage_1.jpg' + }) + + expect(results[1]).toMatchObject({ + start: '2023-01-19T05:55:00.000Z', + stop: '2023-01-19T06:00:00.000Z', + title: 'Météo', + category: 'Météo', + image: 'https://dnsmptv-img.pragma-consult.be/imgs/picto/132/Meteo.jpg' + }) + + expect(results[8]).toMatchObject({ + start: '2023-01-19T08:00:00.000Z', + stop: '2023-01-19T08:05:00.000Z', + title: 'Le journal', + description: "L'information de la mi-journée avec des JT...", + category: 'Journal', + image: 'https://dnsmptv-img.pragma-consult.be/imgs/picto/132/journal.jpg' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')), + date + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/movistarplus.es/movistarplus.es.config.js b/sites/movistarplus.es/movistarplus.es.config.js index c58b5190..ef94c0a9 100644 --- a/sites/movistarplus.es/movistarplus.es.config.js +++ b/sites/movistarplus.es/movistarplus.es.config.js @@ -1,97 +1,97 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') - -module.exports = { - site: 'movistarplus.es', - days: 2, - url({ channel, date }) { - return `https://www.movistarplus.es/programacion-tv/${channel.site_id}/${date.format('YYYY-MM-DD')}` - }, - async parser({ content }) { - let programs = [] - let items = parseItems(content) - if (!items.length) return programs - - const $ = cheerio.load(content) - const programElements = $('div[id^="ele-"]').get() - - for (let i = 0; i < items.length; i++) { - const el = items[i] - let description = null - - if (programElements[i]) { - const programDiv = $(programElements[i]) - const programLink = programDiv.find('a').attr('href') - - if (programLink) { - const idMatch = programLink.match(/id=(\d+)/) - if (idMatch && idMatch[1]) { - description = await getProgramDescription(programLink).catch(() => null) - } - } - } - - programs.push({ - title: el.item.name, - description: description, - start: dayjs(el.item.startDate), - stop: dayjs(el.item.endDate) - }) - } - - return programs - }, - async channels() { - const html = await axios - .get('https://www.movistarplus.es/programacion-tv') - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(html) - let scheme = $('script:contains(ItemList)').html() - scheme = JSON.parse(scheme) - - return scheme.itemListElement.map(el => { - const urlParts = el.item.url.split('/') - const site_id = urlParts.pop().toLowerCase() - - return { - lang: 'es', - name: el.item.name, - site_id - } - }) - } -} - -function parseItems(content) { - try { - const $ = cheerio.load(content) - let scheme = $('script:contains("@type": "ItemList")').html() - scheme = JSON.parse(scheme) - if (!scheme || !Array.isArray(scheme.itemListElement)) return [] - - return scheme.itemListElement - } catch { - return [] - } -} - -async function getProgramDescription(programUrl) { - try { - const response = await axios.get(programUrl, { - headers: { - 'Referer': 'https://www.movistarplus.es/programacion-tv/' - } - }) - - const $ = cheerio.load(response.data) - const description = $('.show-content .text p').first().text().trim() || null - - return description - } catch (error) { - console.error(`Error fetching description from ${programUrl}:`, error.message) - return null - } -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') + +module.exports = { + site: 'movistarplus.es', + days: 2, + url({ channel, date }) { + return `https://www.movistarplus.es/programacion-tv/${channel.site_id}/${date.format('YYYY-MM-DD')}` + }, + async parser({ content }) { + let programs = [] + let items = parseItems(content) + if (!items.length) return programs + + const $ = cheerio.load(content) + const programElements = $('div[id^="ele-"]').get() + + for (let i = 0; i < items.length; i++) { + const el = items[i] + let description = null + + if (programElements[i]) { + const programDiv = $(programElements[i]) + const programLink = programDiv.find('a').attr('href') + + if (programLink) { + const idMatch = programLink.match(/id=(\d+)/) + if (idMatch && idMatch[1]) { + description = await getProgramDescription(programLink).catch(() => null) + } + } + } + + programs.push({ + title: el.item.name, + description: description, + start: dayjs(el.item.startDate), + stop: dayjs(el.item.endDate) + }) + } + + return programs + }, + async channels() { + const html = await axios + .get('https://www.movistarplus.es/programacion-tv') + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(html) + let scheme = $('script:contains(ItemList)').html() + scheme = JSON.parse(scheme) + + return scheme.itemListElement.map(el => { + const urlParts = el.item.url.split('/') + const site_id = urlParts.pop().toLowerCase() + + return { + lang: 'es', + name: el.item.name, + site_id + } + }) + } +} + +function parseItems(content) { + try { + const $ = cheerio.load(content) + let scheme = $('script:contains("@type": "ItemList")').html() + scheme = JSON.parse(scheme) + if (!scheme || !Array.isArray(scheme.itemListElement)) return [] + + return scheme.itemListElement + } catch { + return [] + } +} + +async function getProgramDescription(programUrl) { + try { + const response = await axios.get(programUrl, { + headers: { + 'Referer': 'https://www.movistarplus.es/programacion-tv/' + } + }) + + const $ = cheerio.load(response.data) + const description = $('.show-content .text p').first().text().trim() || null + + return description + } catch (error) { + console.error(`Error fetching description from ${programUrl}:`, error.message) + return null + } +} diff --git a/sites/movistarplus.es/movistarplus.es.test.js b/sites/movistarplus.es/movistarplus.es.test.js index 2e288d15..1f81dd55 100644 --- a/sites/movistarplus.es/movistarplus.es.test.js +++ b/sites/movistarplus.es/movistarplus.es.test.js @@ -1,80 +1,80 @@ -const { parser, url } = require('./movistarplus.es.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 axios = require('axios') -jest.mock('axios') - -const date = dayjs.utc('2025-05-30', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'sexta', - xmltv_id: 'LaSexta.es' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.movistarplus.es/programacion-tv/sexta/2025-05-30' - ) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - - axios.get.mockImplementation(url => { - if ( - url === - 'https://www.movistarplus.es/entretenimiento/venta-prime-t1/ficha?tipo=E&id=3414523' - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/program1.html')) - }) - } else if ( - url === - 'https://www.movistarplus.es/deportes/programa/pokerstars-casino-1/ficha?tipo=E&id=2057641' - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/program2.html')) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, date }) - 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-05-30T03:15:00.000Z', - stop: '2025-05-30T04:25:00.000Z', - title: 'Venta Prime', - description: - 'Espacio de televenta.' - }) - expect(results[19]).toMatchObject({ - start: '2025-05-31T00:45:00.000Z', - stop: '2025-05-31T01:25:00.000Z', - title: 'Pokerstars casino', - description: - 'El programa trae cada día toda la emoción de su ruleta en vivo, Spin & Win, una versión exclusiva del clásico juego de casino.' - }) -}) - - -it('can handle empty guide', async () => { - const results = await parser({ - date, - channel, - content: '[]' - }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./movistarplus.es.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 axios = require('axios') +jest.mock('axios') + +const date = dayjs.utc('2025-05-30', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'sexta', + xmltv_id: 'LaSexta.es' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.movistarplus.es/programacion-tv/sexta/2025-05-30' + ) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + + axios.get.mockImplementation(url => { + if ( + url === + 'https://www.movistarplus.es/entretenimiento/venta-prime-t1/ficha?tipo=E&id=3414523' + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/program1.html')) + }) + } else if ( + url === + 'https://www.movistarplus.es/deportes/programa/pokerstars-casino-1/ficha?tipo=E&id=2057641' + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/program2.html')) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, date }) + 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-05-30T03:15:00.000Z', + stop: '2025-05-30T04:25:00.000Z', + title: 'Venta Prime', + description: + 'Espacio de televenta.' + }) + expect(results[19]).toMatchObject({ + start: '2025-05-31T00:45:00.000Z', + stop: '2025-05-31T01:25:00.000Z', + title: 'Pokerstars casino', + description: + 'El programa trae cada día toda la emoción de su ruleta en vivo, Spin & Win, una versión exclusiva del clásico juego de casino.' + }) +}) + + +it('can handle empty guide', async () => { + const results = await parser({ + date, + channel, + content: '[]' + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/mtel.ba/mtel.ba.config.js b/sites/mtel.ba/mtel.ba.config.js index d5f6f225..6ade976d 100644 --- a/sites/mtel.ba/mtel.ba.config.js +++ b/sites/mtel.ba/mtel.ba.config.js @@ -1,110 +1,110 @@ -const _ = require('lodash') -const doFetch = require('@ntlab/sfetch') -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: 'mtel.ba', - days: 2, - url({ channel, date }) { - const [platform] = channel.site_id.split('#') - - return `https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/epg?platform=tv-${platform}¤tPage=0&pageSize=1000&date=${date.format( - 'YYYY-MM-DD' - )}` - }, - request: { - timeout: 20000, // 20 seconds - maxContentLength: 10000000, // 10 Mb - cache: { - interpretHeader: false, - ttl: 24 * 60 * 60 * 1000 // 1 day - } - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - if (item.title === 'Nema informacija o programu') return - programs.push({ - title: item.title, - description: item.description, - categories: parseCategories(item), - image: parseImage(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels({ platform = 'msat' }) { - const platforms = { - msat: 'https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/search?pageSize=100¤tPage=&query=:relevantno:tv-kategorija:tv-msat', - iptv: 'https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/search?pageSize=100¤tPage=&query=:relevantno:tv-kategorija:tv-iptv:tv-iptv-paket:Svi+kanali' - } - - const queue = [ - { - platform, - url: platforms[platform].replace('', 0) - } - ] - - let channels = [] - await doFetch(queue, (req, data) => { - if (data && data.pagination.currentPage < data.pagination.totalPages) { - queue.push({ - platform: req.platform, - url: platforms[req.platform].replace('', ++data.pagination.currentPage) - }) - } - - data.products.forEach(channel => { - channels.push({ - lang: 'bs', - name: channel.name, - site_id: `${req.platform}#${channel.code}` - }) - }) - }) - - return channels - } -} - -function parseStart(item) { - return dayjs.tz(item.start, 'YYYY-MM-DD HH:mm', 'Europe/Sarajevo') -} - -function parseStop(item) { - return dayjs.tz(item.end, 'YYYY-MM-DD HH:mm', 'Europe/Sarajevo') -} - -function parseCategories(item) { - return item.category ? item.category.split(' / ') : [] -} - -function parseImage(item) { - return item?.picture?.url ? item.picture.url : null -} - -function parseItems(content, channel) { - try { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.products)) return [] - const [, channelId] = channel.site_id.split('#') - const channelData = data.products.find(channel => channel.code === channelId) - if (!channelData || !Array.isArray(channelData.programs)) return [] - - return _.sortBy(channelData.programs, p => parseStart(p).valueOf()) - } catch { - return [] - } -} +const doFetch = require('@ntlab/sfetch') +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('lodash.sortby') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'mtel.ba', + days: 2, + url({ channel, date }) { + const [platform] = channel.site_id.split('#') + + return `https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/epg?platform=tv-${platform}&pageSize=999&date=${date.format( + 'YYYY-MM-DD' + )}` + }, + request: { + timeout: 20000, // 20 seconds + maxContentLength: 10000000, // 10 Mb + cache: { + interpretHeader: false, + ttl: 24 * 60 * 60 * 1000 // 1 day + } + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.description, + categories: parseCategories(item), + image: parseImage(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels({ platform = 'msat' }) { + const platforms = { + msat: 'https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/search?pageSize=999&query=:relevantno:tv-kategorija:tv-msat', + iptv: 'https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/search?pageSize=999&query=:relevantno:tv-kategorija:tv-iptv' + } + + const queue = [ + { + platform, + url: platforms[platform] + } + ] + + let channels = [] + await doFetch(queue, (req, data) => { + if (data && data.pagination.currentPage < data.pagination.totalPages) { + queue.push({ + platform: req.platform, + url: platforms[req.platform] + }) + } + + data.products.forEach(channel => { + channels.push({ + lang: 'bs', + name: channel.name, + site_id: `${req.platform}#${channel.code}` + }) + }) + }) + + return channels + } +} + +function parseStart(item) { + return dayjs.tz(item.start, 'YYYY-MM-DD HH:mm', 'Europe/Sarajevo') +} + +function parseStop(item) { + return dayjs.tz(item.end, 'YYYY-MM-DD HH:mm', 'Europe/Sarajevo') +} + +function parseCategories(item) { + return item.category ? item.category.split(' / ') : [] +} + +function parseImage(item) { + return item?.picture?.url ? item.picture.url : null +} + +function parseItems(content, channel) { + try { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.products)) return [] + const [, channelId] = channel.site_id.split('#') + const channelData = data.products.find(channel => channel.code === channelId) + if (!channelData || !Array.isArray(channelData.programs)) return [] + // filter out programs that have the sentence "no program information available" + channelData.programs = channelData.programs.filter(p => !p.title.includes('Nema informacija o programu')) + return sortBy(channelData.programs, p => parseStart(p).valueOf()) + } catch { + return [] + } +} diff --git a/sites/mtel.ba/mtel.ba.test.js b/sites/mtel.ba/mtel.ba.test.js index 2664d66b..122ee19b 100644 --- a/sites/mtel.ba/mtel.ba.test.js +++ b/sites/mtel.ba/mtel.ba.test.js @@ -1,58 +1,58 @@ -const { parser, url } = require('./mtel.ba.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-02-04', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'msat#ch-11-rtrs' } - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/epg?platform=tv-msat¤tPage=0&pageSize=1000&date=2025-02-04' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ channel, content }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - - return p - }) - - expect(results.length).toBe(38) - expect(results[0]).toMatchObject({ - start: '2025-02-03T22:38:00.000Z', - stop: '2025-02-03T23:38:00.000Z', - title: 'Neka pesma kaže', - image: - 'https://medias.services.mtel.ba/medias/407368591.jpg?context=bWFzdGVyfHJvb3R8MTM2MTZ8aW1hZ2UvanBlZ3xhR1F5TDJnell5ODBOekExTmpFMk1qRTJNRFkzTUM4ME1EY3pOamcxT1RFdWFuQm58ZWM3Zjc4MDNlZTY5OWU1ZGJiZDI5N2UzMDg4ODA3NzQ1NWM0OThlMjdhYmU4MjI4NGJhOWE2YzYwMTc5ODM3NQ', - description: - 'Zabavni-muzički program donosi nam divne zvukove prave, narodne muzike, u kojoj se izvođači oslanjaju na kvalitet i tradiciju.', - categories: ['Music', 'Ballet', 'Dance'] - }) - expect(results[37]).toMatchObject({ - start: '2025-02-04T22:27:00.000Z', - stop: '2025-02-04T23:58:00.000Z', - title: 'Bitanga s plaže', - image: - 'https://medias.services.mtel.ba/medias/117604203.jpg?context=bWFzdGVyfHJvb3R8MTY1MTZ8aW1hZ2UvanBlZ3xhRGd6TDJnek1DODBOekExTmpFMk16STNORGM0TWk4eE1UYzJNRFF5TURNdWFuQm58YmU5MjdkOTljMGE4YjIyNjg3ZmI1YWJjYWQ0ZDY5YjA0YWJiY2RlN2E0ZGVjOTdlYzM4MzI4MzYyMzFiODBlMg', - description: - 'Film prati urnebesne avanture Moondoga, buntovnika i skitnicu koji svoj život živi isključivo prema vlastitim pravilima. Uz glumačke nastupe Snoop Dogga, Zaca Efrona i Isle Fisher, Bitanga s plaže osvježavajuće je originalna i subverzivna nova komedija scenarista i redatelja Harmonyja Korinea.', - categories: ['Movie', 'Drama'] - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - channel, - content: '{}' - }) - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./mtel.ba.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-02-04', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'msat#ch-11-rtrs' } + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/epg?platform=tv-msat&pageSize=999&date=2025-02-04' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ channel, content }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + + return p + }) + + expect(results.length).toBe(38) + expect(results[0]).toMatchObject({ + start: '2025-02-03T22:38:00.000Z', + stop: '2025-02-03T23:38:00.000Z', + title: 'Neka pesma kaže', + image: + 'https://medias.services.mtel.ba/medias/407368591.jpg?context=bWFzdGVyfHJvb3R8MTM2MTZ8aW1hZ2UvanBlZ3xhR1F5TDJnell5ODBOekExTmpFMk1qRTJNRFkzTUM4ME1EY3pOamcxT1RFdWFuQm58ZWM3Zjc4MDNlZTY5OWU1ZGJiZDI5N2UzMDg4ODA3NzQ1NWM0OThlMjdhYmU4MjI4NGJhOWE2YzYwMTc5ODM3NQ', + description: + 'Zabavni-muzički program donosi nam divne zvukove prave, narodne muzike, u kojoj se izvođači oslanjaju na kvalitet i tradiciju.', + categories: ['Music', 'Ballet', 'Dance'] + }) + expect(results[37]).toMatchObject({ + start: '2025-02-04T22:27:00.000Z', + stop: '2025-02-04T23:58:00.000Z', + title: 'Bitanga s plaže', + image: + 'https://medias.services.mtel.ba/medias/117604203.jpg?context=bWFzdGVyfHJvb3R8MTY1MTZ8aW1hZ2UvanBlZ3xhRGd6TDJnek1DODBOekExTmpFMk16STNORGM0TWk4eE1UYzJNRFF5TURNdWFuQm58YmU5MjdkOTljMGE4YjIyNjg3ZmI1YWJjYWQ0ZDY5YjA0YWJiY2RlN2E0ZGVjOTdlYzM4MzI4MzYyMzFiODBlMg', + description: + 'Film prati urnebesne avanture Moondoga, buntovnika i skitnicu koji svoj život živi isključivo prema vlastitim pravilima. Uz glumačke nastupe Snoop Dogga, Zaca Efrona i Isle Fisher, Bitanga s plaže osvježavajuće je originalna i subverzivna nova komedija scenarista i redatelja Harmonyja Korinea.', + categories: ['Movie', 'Drama'] + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + channel, + content: '{}' + }) + expect(results).toMatchObject([]) +}) diff --git a/sites/mts.rs/mts.rs.config.js b/sites/mts.rs/mts.rs.config.js index 0ad5215b..aee11d40 100644 --- a/sites/mts.rs/mts.rs.config.js +++ b/sites/mts.rs/mts.rs.config.js @@ -1,55 +1,55 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'mts.rs', - days: 2, - url({ date }) { - return `https://mts.rs/hybris/ecommerce/b2c/v1/products/search?sort=pozicija-rastuce&searchQueryContext=CHANNEL_PROGRAM&query=:pozicija-rastuce:tip-kanala-radio:TV kanali:channelProgramDates:${date.format( - 'YYYY-MM-DD' - )}&pageSize=10000` - }, - request: { - maxContentLength: 10000000 // 10 Mb - }, - parser({ content, channel }) { - const items = parseItems(content, channel) - - return items.map(item => { - return { - title: item.title, - category: item.category, - description: item.description, - image: item?.picture?.url || null, - start: dayjs(item.start), - stop: dayjs(item.end) - } - }) - }, - async channels() { - const data = await axios - .get(module.exports.url({ date: dayjs() })) - .then(r => r.data) - .catch(console.error) - - return data.products.map(channel => ({ - lang: 'bs', - name: channel.name, - site_id: encodeURIComponent(channel.code) - })) - } -} - -function parseItems(content, channel) { - try { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.products)) return [] - - const channelData = data.products.find(c => c.code === channel.site_id) - if (!channelData || !Array.isArray(channelData.programs)) return [] - - return channelData.programs - } catch { - return [] - } -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'mts.rs', + days: 2, + url({ date }) { + return `https://mts.rs/hybris/ecommerce/b2c/v1/products/search?sort=pozicija-rastuce&searchQueryContext=CHANNEL_PROGRAM&query=:pozicija-rastuce:tip-kanala-radio:TV kanali:channelProgramDates:${date.format( + 'YYYY-MM-DD' + )}&pageSize=10000` + }, + request: { + maxContentLength: 10000000 // 10 Mb + }, + parser({ content, channel }) { + const items = parseItems(content, channel) + + return items.map(item => { + return { + title: item.title, + category: item.category, + description: item.description, + image: item?.picture?.url || null, + start: dayjs(item.start), + stop: dayjs(item.end) + } + }) + }, + async channels() { + const data = await axios + .get(module.exports.url({ date: dayjs() })) + .then(r => r.data) + .catch(console.error) + + return data.products.map(channel => ({ + lang: 'bs', + name: channel.name, + site_id: encodeURIComponent(channel.code) + })) + } +} + +function parseItems(content, channel) { + try { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.products)) return [] + + const channelData = data.products.find(c => c.code === channel.site_id) + if (!channelData || !Array.isArray(channelData.programs)) return [] + + return channelData.programs + } catch { + return [] + } +} diff --git a/sites/mts.rs/mts.rs.test.js b/sites/mts.rs/mts.rs.test.js index 27d3d970..24611dac 100644 --- a/sites/mts.rs/mts.rs.test.js +++ b/sites/mts.rs/mts.rs.test.js @@ -1,59 +1,59 @@ -const { parser, url } = require('./mts.rs.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-23', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'rts_1_hd', - xmltv_id: 'RTS1HD.rs' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://mts.rs/hybris/ecommerce/b2c/v1/products/search?sort=pozicija-rastuce&searchQueryContext=CHANNEL_PROGRAM&query=:pozicija-rastuce:tip-kanala-radio:TV kanali:channelProgramDates:2025-01-23&pageSize=10000' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const results = parser({ channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(31) - expect(results[0]).toMatchObject({ - start: '2025-01-22T23:25:00.000Z', - stop: '2025-01-23T00:15:00.000Z', - title: 'Jeloustoun', - category: 'Tv-serijali', - image: - 'https://mediasb2c.mts.rs/medias/5-72517fcb4505f9d7809814598fed5ce6d84571a1-99415C04AED37264BC49C11115B94633.jpg?context=bWFzdGVyfHJvb3R8Nzc4MjN8aW1hZ2UvanBlZ3xhRFpsTDJoa01pODBOakF6T0RnME9UVTROVEU0TWk4MVh6Y3lOVEUzWm1OaU5EVXdOV1k1WkRjNE1EazRNVFExT1RobVpXUTFZMlUyWkRnME5UY3hZVEZmT1RrME1UVkRNRFJCUlVRek56STJORUpETkRsRE1URXhNVFZDT1RRMk16TXVhbkJufGUwZDIyMWU4MDIxZWVhZjY5MDY0ODQ0YjI5OWVjMGJjMDNlNWI3ZjMwNmE0MjYwMWJlMWQxNGFiMzNlMzU1NDE', - description: - 'Serija prati život Džona Datona, koga tumači oskarovac Kevin Kostner, koji mora da se bori sa spoljnim i unutrašnjim pretnjama kako bi zaštitio svoju porodicu, ranč i imanje. Smeštena u divlje prostranstvo Montane, serija istražuje složene moralne dileme, borbe za opstanak i porodične sukobe u modernom zapadnom okruženju. Sa prelepim pejzažima i napetim zapletima, Jeloustoun nudi priču o ljubavi, lojalnosti, moći i borbi za očuvanje tradicije. Kevin Kostner nije samo glumac u seriji, već i jedan od producenata. Njegovo bogato iskustvo u filmskoj industriji, uključujući režiju i produkciju, pomoglo je da Jeloustoun bude verodostojan i autentičan prikaz života na ranču. Serija je dobila silne nagrade, a među njima i Zlatni globus za najbolju televizijsku seriju (drama) 2021. godine, dok je Kevin Kostner je osvojio nagradu za Najboljeg glumca u dramskoj televizijskoj seriji, iste godine godine. Nekoliko puta je bila nominovana za nagradu Emi.' - }) - expect(results[30]).toMatchObject({ - start: '2025-01-23T23:30:00.000Z', - stop: '2025-01-24T00:20:00.000Z', - title: 'Jeloustoun', - category: 'Tv-serijali', - image: - 'https://mediasb2c.mts.rs/medias/5-72517fcb4505f9d7809814598fed5ce6d84571a1-99415C04AED37264BC49C11115B94633.jpg?context=bWFzdGVyfHJvb3R8Nzc4MjN8aW1hZ2UvanBlZ3xhRFpsTDJoa01pODBOakF6T0RnME9UVTROVEU0TWk4MVh6Y3lOVEUzWm1OaU5EVXdOV1k1WkRjNE1EazRNVFExT1RobVpXUTFZMlUyWkRnME5UY3hZVEZmT1RrME1UVkRNRFJCUlVRek56STJORUpETkRsRE1URXhNVFZDT1RRMk16TXVhbkJufGUwZDIyMWU4MDIxZWVhZjY5MDY0ODQ0YjI5OWVjMGJjMDNlNWI3ZjMwNmE0MjYwMWJlMWQxNGFiMzNlMzU1NDE', - description: - 'Serija prati život Džona Datona, koga tumači oskarovac Kevin Kostner, koji mora da se bori sa spoljnim i unutrašnjim pretnjama kako bi zaštitio svoju porodicu, ranč i imanje. Smeštena u divlje prostranstvo Montane, serija istražuje složene moralne dileme, borbe za opstanak i porodične sukobe u modernom zapadnom okruženju. Sa prelepim pejzažima i napetim zapletima, Jeloustoun nudi priču o ljubavi, lojalnosti, moći i borbi za očuvanje tradicije. Kevin Kostner nije samo glumac u seriji, već i jedan od producenata. Njegovo bogato iskustvo u filmskoj industriji, uključujući režiju i produkciju, pomoglo je da Jeloustoun bude verodostojan i autentičan prikaz života na ranču. Serija je dobila silne nagrade, a među njima i Zlatni globus za najbolju televizijsku seriju (drama) 2021. godine, dok je Kevin Kostner je osvojio nagradu za Najboljeg glumca u dramskoj televizijskoj seriji, iste godine godine. Nekoliko puta je bila nominovana za nagradu Emi.' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - channel, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./mts.rs.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-23', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'rts_1_hd', + xmltv_id: 'RTS1HD.rs' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://mts.rs/hybris/ecommerce/b2c/v1/products/search?sort=pozicija-rastuce&searchQueryContext=CHANNEL_PROGRAM&query=:pozicija-rastuce:tip-kanala-radio:TV kanali:channelProgramDates:2025-01-23&pageSize=10000' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = parser({ channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(31) + expect(results[0]).toMatchObject({ + start: '2025-01-22T23:25:00.000Z', + stop: '2025-01-23T00:15:00.000Z', + title: 'Jeloustoun', + category: 'Tv-serijali', + image: + 'https://mediasb2c.mts.rs/medias/5-72517fcb4505f9d7809814598fed5ce6d84571a1-99415C04AED37264BC49C11115B94633.jpg?context=bWFzdGVyfHJvb3R8Nzc4MjN8aW1hZ2UvanBlZ3xhRFpsTDJoa01pODBOakF6T0RnME9UVTROVEU0TWk4MVh6Y3lOVEUzWm1OaU5EVXdOV1k1WkRjNE1EazRNVFExT1RobVpXUTFZMlUyWkRnME5UY3hZVEZmT1RrME1UVkRNRFJCUlVRek56STJORUpETkRsRE1URXhNVFZDT1RRMk16TXVhbkJufGUwZDIyMWU4MDIxZWVhZjY5MDY0ODQ0YjI5OWVjMGJjMDNlNWI3ZjMwNmE0MjYwMWJlMWQxNGFiMzNlMzU1NDE', + description: + 'Serija prati život Džona Datona, koga tumači oskarovac Kevin Kostner, koji mora da se bori sa spoljnim i unutrašnjim pretnjama kako bi zaštitio svoju porodicu, ranč i imanje. Smeštena u divlje prostranstvo Montane, serija istražuje složene moralne dileme, borbe za opstanak i porodične sukobe u modernom zapadnom okruženju. Sa prelepim pejzažima i napetim zapletima, Jeloustoun nudi priču o ljubavi, lojalnosti, moći i borbi za očuvanje tradicije. Kevin Kostner nije samo glumac u seriji, već i jedan od producenata. Njegovo bogato iskustvo u filmskoj industriji, uključujući režiju i produkciju, pomoglo je da Jeloustoun bude verodostojan i autentičan prikaz života na ranču. Serija je dobila silne nagrade, a među njima i Zlatni globus za najbolju televizijsku seriju (drama) 2021. godine, dok je Kevin Kostner je osvojio nagradu za Najboljeg glumca u dramskoj televizijskoj seriji, iste godine godine. Nekoliko puta je bila nominovana za nagradu Emi.' + }) + expect(results[30]).toMatchObject({ + start: '2025-01-23T23:30:00.000Z', + stop: '2025-01-24T00:20:00.000Z', + title: 'Jeloustoun', + category: 'Tv-serijali', + image: + 'https://mediasb2c.mts.rs/medias/5-72517fcb4505f9d7809814598fed5ce6d84571a1-99415C04AED37264BC49C11115B94633.jpg?context=bWFzdGVyfHJvb3R8Nzc4MjN8aW1hZ2UvanBlZ3xhRFpsTDJoa01pODBOakF6T0RnME9UVTROVEU0TWk4MVh6Y3lOVEUzWm1OaU5EVXdOV1k1WkRjNE1EazRNVFExT1RobVpXUTFZMlUyWkRnME5UY3hZVEZmT1RrME1UVkRNRFJCUlVRek56STJORUpETkRsRE1URXhNVFZDT1RRMk16TXVhbkJufGUwZDIyMWU4MDIxZWVhZjY5MDY0ODQ0YjI5OWVjMGJjMDNlNWI3ZjMwNmE0MjYwMWJlMWQxNGFiMzNlMzU1NDE', + description: + 'Serija prati život Džona Datona, koga tumači oskarovac Kevin Kostner, koji mora da se bori sa spoljnim i unutrašnjim pretnjama kako bi zaštitio svoju porodicu, ranč i imanje. Smeštena u divlje prostranstvo Montane, serija istražuje složene moralne dileme, borbe za opstanak i porodične sukobe u modernom zapadnom okruženju. Sa prelepim pejzažima i napetim zapletima, Jeloustoun nudi priču o ljubavi, lojalnosti, moći i borbi za očuvanje tradicije. Kevin Kostner nije samo glumac u seriji, već i jedan od producenata. Njegovo bogato iskustvo u filmskoj industriji, uključujući režiju i produkciju, pomoglo je da Jeloustoun bude verodostojan i autentičan prikaz života na ranču. Serija je dobila silne nagrade, a među njima i Zlatni globus za najbolju televizijsku seriju (drama) 2021. godine, dok je Kevin Kostner je osvojio nagradu za Najboljeg glumca u dramskoj televizijskoj seriji, iste godine godine. Nekoliko puta je bila nominovana za nagradu Emi.' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mujtvprogram.cz/mujtvprogram.cz.config.js b/sites/mujtvprogram.cz/mujtvprogram.cz.config.js index 0a8ddb95..34d2dc56 100644 --- a/sites/mujtvprogram.cz/mujtvprogram.cz.config.js +++ b/sites/mujtvprogram.cz/mujtvprogram.cz.config.js @@ -1,111 +1,111 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const convert = require('xml-js') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'mujtvprogram.cz', - days: 2, - url({ channel, date }) { - const diff = date.diff(dayjs.utc().startOf('d'), 'd') - return `https://services.mujtvprogram.cz/tvprogram2services/services/tvprogrammelist_mobile.php?channel_cid=${channel.site_id}&day=${diff}` - }, - parser({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.name._text, - start: parseTime(item.startDate._text), - stop: parseTime(item.endDate._text), - description: parseDescription(item), - category: parseCategory(item), - date: item.year._text || null, - director: parseList(item.directors), - actor: parseList(item.actors) - }) - }) - return programs - }, - async channels() { - const cheerio = require('cheerio') - const axios = require('axios') - - let channels = [] - - const categories = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - for (let category of categories) { - const params = new URLSearchParams() - params.append('localization', 1) - params.append('list_for_selector', 1) - params.append('category_kid', category) - - const data = await axios - .post( - 'https://services.mujtvprogram.cz/tvprogram2services/services/tvchannellist_mobile.php', - params, - { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - } - ) - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(data, { xmlMode: true }) - - $('channel').each((i, el) => { - let lang = $(el).find('lang').text() - if (lang === 'cz') lang = 'cs' - - channels.push({ - lang, - site_id: $(el).find('cid').text(), - name: $(el).find('name').first().text() - }) - }) - } - - return channels - } -} - -function parseItems(content) { - try { - const data = convert.xml2js(content, { - compact: true, - ignoreDeclaration: true, - ignoreAttributes: true - }) - if (!data) return [] - const programmes = data['tv-program-programmes'].programme - return programmes && Array.isArray(programmes) ? programmes : [] - } catch { - return [] - } -} -function parseDescription(item) { - if (item.longDescription) return item.longDescription._text - if (item.shortDescription) return item.shortDescription._text - return null -} - -function parseList(list) { - if (!list) return [] - if (!list._text) return [] - return typeof list._text === 'string' ? list._text.split(', ') : [] -} -function parseTime(time) { - return dayjs.tz(time, 'DD.MM.YYYY HH.mm', 'Europe/Prague') -} - -function parseCategory(item) { - if (!item['programme-type']) return null - return item['programme-type'].name._text -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const convert = require('xml-js') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'mujtvprogram.cz', + days: 2, + url({ channel, date }) { + const diff = date.diff(dayjs.utc().startOf('d'), 'd') + return `https://services.mujtvprogram.cz/tvprogram2services/services/tvprogrammelist_mobile.php?channel_cid=${channel.site_id}&day=${diff}` + }, + parser({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.name._text, + start: parseTime(item.startDate._text), + stop: parseTime(item.endDate._text), + description: parseDescription(item), + category: parseCategory(item), + date: item.year._text || null, + director: parseList(item.directors), + actor: parseList(item.actors) + }) + }) + return programs + }, + async channels() { + const cheerio = require('cheerio') + const axios = require('axios') + + let channels = [] + + const categories = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + for (let category of categories) { + const params = new URLSearchParams() + params.append('localization', 1) + params.append('list_for_selector', 1) + params.append('category_kid', category) + + const data = await axios + .post( + 'https://services.mujtvprogram.cz/tvprogram2services/services/tvchannellist_mobile.php', + params, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ) + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(data, { xmlMode: true }) + + $('channel').each((i, el) => { + let lang = $(el).find('lang').text() + if (lang === 'cz') lang = 'cs' + + channels.push({ + lang, + site_id: $(el).find('cid').text(), + name: $(el).find('name').first().text() + }) + }) + } + + return channels + } +} + +function parseItems(content) { + try { + const data = convert.xml2js(content, { + compact: true, + ignoreDeclaration: true, + ignoreAttributes: true + }) + if (!data) return [] + const programmes = data['tv-program-programmes'].programme + return programmes && Array.isArray(programmes) ? programmes : [] + } catch { + return [] + } +} +function parseDescription(item) { + if (item.longDescription) return item.longDescription._text + if (item.shortDescription) return item.shortDescription._text + return null +} + +function parseList(list) { + if (!list) return [] + if (!list._text) return [] + return typeof list._text === 'string' ? list._text.split(', ') : [] +} +function parseTime(time) { + return dayjs.tz(time, 'DD.MM.YYYY HH.mm', 'Europe/Prague') +} + +function parseCategory(item) { + if (!item['programme-type']) return null + return item['programme-type'].name._text +} diff --git a/sites/mujtvprogram.cz/mujtvprogram.cz.test.js b/sites/mujtvprogram.cz/mujtvprogram.cz.test.js index 568e8b12..a83d95e0 100644 --- a/sites/mujtvprogram.cz/mujtvprogram.cz.test.js +++ b/sites/mujtvprogram.cz/mujtvprogram.cz.test.js @@ -1,54 +1,54 @@ -const { parser, url } = require('./mujtvprogram.cz.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 channel = { - site_id: '1', - xmltv_id: 'CT1.cz' -} - -it('can generate valid url for today', () => { - const date = dayjs.utc().startOf('d') - expect(url({ channel, date })).toBe( - 'https://services.mujtvprogram.cz/tvprogram2services/services/tvprogrammelist_mobile.php?channel_cid=1&day=0' - ) -}) - -it('can generate valid url for tomorrow', () => { - const date = dayjs.utc().startOf('d').add(1, 'd') - expect(url({ channel, date })).toBe( - 'https://services.mujtvprogram.cz/tvprogram2services/services/tvprogrammelist_mobile.php?channel_cid=1&day=1' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - - let results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(results[3]).toMatchObject({ - title: 'Čepice', - description: - 'Jarka (J. Bohdalová) vyčítá manželovi Jiřímu (F. Řehák), že jí nepomáhá při předvánočním úklidu. Vzápětí ale náhodou najde ve skříni ukrytou dámskou čepici a napadne ji, že jde o Jiřího dárek pro ni pod stromeček. Její chování se ihned změní. Jen muži naznačí, že by chtěla čepici jiné barvy. Manžel jí ovšem řekne, že čepici si u něj schoval kamarád Venca (M. Šulc). Zklamaná žena to prozradí Vencově manželce Božce (A. Tománková). Na Štědrý den však Božka najde pod stromečkem jen rtěnku...', - category: 'film', - date: '1983', - director: ['Mudra F.'], - actor: ['Bohdalová J.', 'Řehák F.', 'Šulc M.'], - start: '2022-12-23T08:00:00.000Z', - stop: '2022-12-23T08:20:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - const result = parser(content, channel) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./mujtvprogram.cz.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 channel = { + site_id: '1', + xmltv_id: 'CT1.cz' +} + +it('can generate valid url for today', () => { + const date = dayjs.utc().startOf('d') + expect(url({ channel, date })).toBe( + 'https://services.mujtvprogram.cz/tvprogram2services/services/tvprogrammelist_mobile.php?channel_cid=1&day=0' + ) +}) + +it('can generate valid url for tomorrow', () => { + const date = dayjs.utc().startOf('d').add(1, 'd') + expect(url({ channel, date })).toBe( + 'https://services.mujtvprogram.cz/tvprogram2services/services/tvprogrammelist_mobile.php?channel_cid=1&day=1' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + + let results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(results[3]).toMatchObject({ + title: 'Čepice', + description: + 'Jarka (J. Bohdalová) vyčítá manželovi Jiřímu (F. Řehák), že jí nepomáhá při předvánočním úklidu. Vzápětí ale náhodou najde ve skříni ukrytou dámskou čepici a napadne ji, že jde o Jiřího dárek pro ni pod stromeček. Její chování se ihned změní. Jen muži naznačí, že by chtěla čepici jiné barvy. Manžel jí ovšem řekne, že čepici si u něj schoval kamarád Venca (M. Šulc). Zklamaná žena to prozradí Vencově manželce Božce (A. Tománková). Na Štědrý den však Božka najde pod stromečkem jen rtěnku...', + category: 'film', + date: '1983', + director: ['Mudra F.'], + actor: ['Bohdalová J.', 'Řehák F.', 'Šulc M.'], + start: '2022-12-23T08:00:00.000Z', + stop: '2022-12-23T08:20:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + const result = parser(content, channel) + expect(result).toMatchObject([]) +}) diff --git a/sites/musor.tv/musor.tv.config.js b/sites/musor.tv/musor.tv.config.js index d645a811..d6fadb31 100644 --- a/sites/musor.tv/musor.tv.config.js +++ b/sites/musor.tv/musor.tv.config.js @@ -1,93 +1,93 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -const headers = { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 OPR/115.0.0.0' -} - -module.exports = { - site: 'musor.tv', - days: 2, - request: { headers }, - url({ channel, date }) { - return dayjs.utc().isSame(date, 'd') - ? `https://musor.tv/mai/tvmusor/${channel.site_id}` - : `https://musor.tv/napi/tvmusor/${channel.site_id}/${date.format('YYYY.MM.DD')}` - }, - parser({ content }) { - const programs = [] - const [$, items] = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = $(item) - let start = parseStart($item) - if (prev) prev.stop = start - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - image: parseImage($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const html = await axios - .get('https://musor.tv/', { headers }) - .then(r => r.data) - .catch(console.error) - - const $ = cheerio.load(html) - const channels = $('body > div.big_content > div > nav > table > tbody > tr > td > a').toArray() - return channels - .map(item => { - const $item = $(item) - const url = $item.attr('href') - if (!url.startsWith('//musor.tv/mai/tvmusor/')) return null - const site_id = url.replace('//musor.tv/mai/tvmusor/', '') - return { - lang: 'hu', - site_id, - name: $item.text() - } - }) - .filter(i => i) - } -} - -function parseImage($item) { - const imgSrc = $item.find('div.smartpe_screenshot > img').attr('src') - - return imgSrc ? `https:${imgSrc}` : null -} - -function parseTitle($item) { - return $item.find('div:nth-child(2) > div > h3 > a').text().trim() -} - -function parseDescription($item) { - return $item.find('div:nth-child(5) > div > div').text().trim() -} - -function parseStart($item) { - let datetime = $item.find('div:nth-child(1) > div > div > div > div > time').attr('content') - if (!datetime) return null - - return dayjs.utc(datetime.replace('GMT', 'T'), 'YYYY-MM-DDTHH:mm:ss') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return [$, $('div.multicolumndayprogarea > div.smartpe_progentry').toArray()] -} +const cheerio = require('cheerio') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +const headers = { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 OPR/115.0.0.0' +} + +module.exports = { + site: 'musor.tv', + days: 2, + request: { headers }, + url({ channel, date }) { + return dayjs.utc().isSame(date, 'd') + ? `https://musor.tv/mai/tvmusor/${channel.site_id}` + : `https://musor.tv/napi/tvmusor/${channel.site_id}/${date.format('YYYY.MM.DD')}` + }, + parser({ content }) { + const programs = [] + const [$, items] = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = $(item) + let start = parseStart($item) + if (prev) prev.stop = start + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + image: parseImage($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const html = await axios + .get('https://musor.tv/', { headers }) + .then(r => r.data) + .catch(console.error) + + const $ = cheerio.load(html) + const channels = $('body > div.big_content > div > nav > table > tbody > tr > td > a').toArray() + return channels + .map(item => { + const $item = $(item) + const url = $item.attr('href') + if (!url.startsWith('//musor.tv/mai/tvmusor/')) return null + const site_id = url.replace('//musor.tv/mai/tvmusor/', '') + return { + lang: 'hu', + site_id, + name: $item.text() + } + }) + .filter(i => i) + } +} + +function parseImage($item) { + const imgSrc = $item.find('div.smartpe_screenshot > img').attr('src') + + return imgSrc ? `https:${imgSrc}` : null +} + +function parseTitle($item) { + return $item.find('div:nth-child(2) > div > h3 > a').text().trim() +} + +function parseDescription($item) { + return $item.find('div:nth-child(5) > div > div').text().trim() +} + +function parseStart($item) { + let datetime = $item.find('div:nth-child(1) > div > div > div > div > time').attr('content') + if (!datetime) return null + + return dayjs.utc(datetime.replace('GMT', 'T'), 'YYYY-MM-DDTHH:mm:ss') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return [$, $('div.multicolumndayprogarea > div.smartpe_progentry').toArray()] +} diff --git a/sites/musor.tv/musor.tv.test.js b/sites/musor.tv/musor.tv.test.js index 98bbd4be..e709284e 100644 --- a/sites/musor.tv/musor.tv.test.js +++ b/sites/musor.tv/musor.tv.test.js @@ -1,57 +1,57 @@ -const { parser, url } = require('./musor.tv.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('2022-11-19', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'HATOS_CSATORNA', - xmltv_id: 'Hatoscsatorna.hu' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://musor.tv/napi/tvmusor/HATOS_CSATORNA/2022.11.19') -}) - -it('can generate valid url for today', () => { - const today = dayjs.utc().startOf('d') - - expect(url({ channel, date: today })).toBe('https://musor.tv/mai/tvmusor/HATOS_CSATORNA') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-19T23:00:00.000Z', - stop: '2022-11-19T23:30:00.000Z', - title: 'Egészségtér', - description: - 'Egészségtér címmel új természetgyógyászattal foglalkozó magazinműsor indult hetente fél órás időtartamban a hatoscsatornán. A műsor derűs, objektív hangvételével és szakmailag magas színvonalú ismeretterjesztő jellegével az e' - }) - - expect(results[1]).toMatchObject({ - start: '2022-11-19T23:30:00.000Z', - stop: '2022-11-20T00:00:00.000Z', - title: 'Tradíció Klipek', - description: 'Tradíció Klipek Birinyi József néprajzi, vallási, népzenei, népszokás filmjeiből.' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./musor.tv.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('2022-11-19', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'HATOS_CSATORNA', + xmltv_id: 'Hatoscsatorna.hu' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://musor.tv/napi/tvmusor/HATOS_CSATORNA/2022.11.19') +}) + +it('can generate valid url for today', () => { + const today = dayjs.utc().startOf('d') + + expect(url({ channel, date: today })).toBe('https://musor.tv/mai/tvmusor/HATOS_CSATORNA') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-19T23:00:00.000Z', + stop: '2022-11-19T23:30:00.000Z', + title: 'Egészségtér', + description: + 'Egészségtér címmel új természetgyógyászattal foglalkozó magazinműsor indult hetente fél órás időtartamban a hatoscsatornán. A műsor derűs, objektív hangvételével és szakmailag magas színvonalú ismeretterjesztő jellegével az e' + }) + + expect(results[1]).toMatchObject({ + start: '2022-11-19T23:30:00.000Z', + stop: '2022-11-20T00:00:00.000Z', + title: 'Tradíció Klipek', + description: 'Tradíció Klipek Birinyi József néprajzi, vallási, népzenei, népszokás filmjeiből.' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mysky.com.ph/__data__/content.json b/sites/mysky.com.ph/__data__/content.json new file mode 100644 index 00000000..0cc6dd0f --- /dev/null +++ b/sites/mysky.com.ph/__data__/content.json @@ -0,0 +1 @@ +{"events":[{"name":"TV PATROL","location":"8","start":"2022/10/04 19:00","end":"2022/10/04 20:00","userData":{"description":"Description example"}},{"name":"DARNA","location":"8","start":"2022/10/05 20:00","end":"2022/10/05 20:45","userData":{"description":""}},{"name":"Zoe Bakes S1","location":"22","start":"2022/10/04 20:30","end":"2022/10/04 21:00","userData":{"description":"Zo Franois Dad is a beekeeper. So for his birthday, she bakes him a special beehiveshaped cake."}}]} \ No newline at end of file diff --git a/sites/mysky.com.ph/mysky.com.ph.config.js b/sites/mysky.com.ph/mysky.com.ph.config.js index c04df7bd..04c4efe7 100644 --- a/sites/mysky.com.ph/mysky.com.ph.config.js +++ b/sites/mysky.com.ph/mysky.com.ph.config.js @@ -1,63 +1,63 @@ -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') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'mysky.com.ph', - days: 2, - url: 'https://skyepg.mysky.com.ph/Main/getEventsbyType', - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel, date) - items.forEach(item => { - programs.push({ - title: item.name, - description: item.userData.description, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const items = await axios - .get('https://skyepg.mysky.com.ph/Main/getEventsbyType') - .then(r => r.data.location) - .catch(console.log) - - return items.map(item => ({ - lang: 'en', - site_id: item.id, - name: item.name - })) - } -} - -function parseStart(item) { - return dayjs.tz(item.start, 'YYYY/MM/DD HH:mm', 'Asia/Manila') -} - -function parseStop(item) { - return dayjs.tz(item.end, 'YYYY/MM/DD HH:mm', 'Asia/Manila') -} - -function parseItems(content, channel, date) { - if (!content) return [] - const data = JSON.parse(content) - if (!data || !Array.isArray(data.events)) return [] - const d = date.format('YYYY/MM/DD') - - return data.events.filter(i => i.location == channel.site_id && i.start.includes(d)) -} +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') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'mysky.com.ph', + days: 2, + url: 'https://skyepg.mysky.com.ph/Main/getEventsbyType', + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel, date) + items.forEach(item => { + programs.push({ + title: item.name, + description: item.userData.description, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const items = await axios + .get('https://skyepg.mysky.com.ph/Main/getEventsbyType') + .then(r => r.data.location) + .catch(console.log) + + return items.map(item => ({ + lang: 'en', + site_id: item.id, + name: item.name + })) + } +} + +function parseStart(item) { + return dayjs.tz(item.start, 'YYYY/MM/DD HH:mm', 'Asia/Manila') +} + +function parseStop(item) { + return dayjs.tz(item.end, 'YYYY/MM/DD HH:mm', 'Asia/Manila') +} + +function parseItems(content, channel, date) { + if (!content) return [] + const data = JSON.parse(content) + if (!data || !Array.isArray(data.events)) return [] + const d = date.format('YYYY/MM/DD') + + return data.events.filter(i => i.location == channel.site_id && i.start.includes(d)) +} diff --git a/sites/mysky.com.ph/mysky.com.ph.test.js b/sites/mysky.com.ph/mysky.com.ph.test.js index 1890037b..9a340a7e 100644 --- a/sites/mysky.com.ph/mysky.com.ph.test.js +++ b/sites/mysky.com.ph/mysky.com.ph.test.js @@ -1,44 +1,45 @@ -const { parser, url } = require('./mysky.com.ph.config.js') -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('2022-10-04', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '8', - xmltv_id: 'KapamilyaChannel.ph' -} - -it('can generate valid url', () => { - expect(url).toBe('https://skyepg.mysky.com.ph/Main/getEventsbyType') -}) - -it('can parse response', () => { - const content = - '{"events":[{"name":"TV PATROL","location":"8","start":"2022/10/04 19:00","end":"2022/10/04 20:00","userData":{"description":"Description example"}},{"name":"DARNA","location":"8","start":"2022/10/05 20:00","end":"2022/10/05 20:45","userData":{"description":""}},{"name":"Zoe Bakes S1","location":"22","start":"2022/10/04 20:30","end":"2022/10/04 21:00","userData":{"description":"Zo Franois Dad is a beekeeper. So for his birthday, she bakes him a special beehiveshaped cake."}}]}' - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-10-04T11:00:00.000Z', - stop: '2022-10-04T12:00:00.000Z', - title: 'TV PATROL', - description: 'Description example' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '', - channel, - date - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./mysky.com.ph.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('2022-10-04', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '8', + xmltv_id: 'KapamilyaChannel.ph' +} + +it('can generate valid url', () => { + expect(url).toBe('https://skyepg.mysky.com.ph/Main/getEventsbyType') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-10-04T11:00:00.000Z', + stop: '2022-10-04T12:00:00.000Z', + title: 'TV PATROL', + description: 'Description example' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '', + channel, + date + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mytelly.co.uk/mytelly.co.uk.config.js b/sites/mytelly.co.uk/mytelly.co.uk.config.js index dbfd58e1..26afdab5 100644 --- a/sites/mytelly.co.uk/mytelly.co.uk.config.js +++ b/sites/mytelly.co.uk/mytelly.co.uk.config.js @@ -1,211 +1,211 @@ -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:mytelly.co.uk') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -doFetch.setDebugger(debug) - -const detailedGuide = true -const tz = 'Europe/London' - -module.exports = { - site: 'mytelly.co.uk', - days: 2, - request: { - headers: { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 OPR/117.0.0.0' - } - }, - url({ date, channel }) { - return `https://www.mytelly.co.uk/tv-guide/listings/channel/${ - channel.site_id - }.html?dt=${date.format('YYYY-MM-DD')}` - }, - async parser({ content, date }) { - const programs = [] - - if (content) { - const queues = [] - const $ = cheerio.load(content) - - $('table.table > tbody > tr') - .toArray() - .forEach(el => { - const td = $(el).find('td:eq(1)') - const title = td.find('h5 a') - if (detailedGuide) { - queues.push({ url: title.attr('href'), params: module.exports.request }) - } else { - const subtitle = td.find('h6') - const time = $(el).find('td:eq(0)') - let start = parseTime(date, time.text().trim()) - const prev = programs[programs.length - 1] - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseText(title), - subTitle: parseText(subtitle), - start, - stop - }) - } - }) - - if (queues.length) { - await doFetch(queues, (url, res) => { - const $ = cheerio.load(res) - const time = $('center > h5 > b').text() - const title = parseText($('.inner-heading.sub h2')) - const subTitle = parseText($('.tab-pane > h5 > strong')) - const description = parseText($('.tab-pane > .tvbody > p')) - const image = $('.program-media-image img').attr('src') - const category = $('.schedule-attributes-genres span') - .toArray() - .map(el => $(el).text()) - const casts = $('.single-cast-head:not([id])') - .toArray() - .map(el => { - const cast = { name: parseText($(el).find('a')) } - const [, role] = $(el) - .text() - .match(/\((.*)\)/) || [null, null] - if (role) { - cast.role = role - } - return cast - }) - const [start, stop] = parseStartStop(date, time) - let season, episode - if (subTitle) { - const [, ses, epi] = subTitle.match(/Season (\d+), Episode (\d+)/) || [null, null] - if (ses) { - season = parseInt(ses) - } - if (epi) { - episode = parseInt(epi) - } - } - programs.push({ - title, - subTitle, - description, - image, - category, - season, - episode, - actor: casts.filter(c => c.role === 'Actor').map(c => c.name), - director: casts.filter(c => c.role === 'Director').map(c => c.name), - presenter: casts.filter(c => c.role === 'Presenter').map(c => c.name), - start, - stop - }) - }) - } - } - - return programs - }, - async channels() { - const channels = {} - const queues = [{ t: 'p', url: 'https://www.mytelly.co.uk/getform', params: this.request }] - await doFetch(queues, (queue, res) => { - // process form -> provider - if (queue.t === 'p') { - const $ = cheerio.load(res) - $('#guide_provider option') - .toArray() - .forEach(el => { - const opt = $(el) - const provider = opt.attr('value') - queues.push({ - t: 'r', - url: 'https://www.mytelly.co.uk/getregions', - params: { ...this.request, provider } - }) - }) - } - // process provider -> region - if (queue.t === 'r') { - const now = dayjs() - for (const r of Object.values(res)) { - const params = { - provider: queue.params.provider, - region: r.title, - TVperiod: 'Night', - date: now.format('YYYY-MM-DD'), - st: 0, - u_time: now.format('HHmm'), - is_mobile: 1 - } - queues.push({ - t: 's', - method: 'post', - url: 'https://www.mytelly.co.uk/tv-guide/schedule', - params: { ...this.request, data: params } - }) - } - } - // process schedule -> channels - if (queue.t === 's') { - const $ = cheerio.load(res) - $('.channelname').each((i, el) => { - const name = $(el).find('center > a:eq(1)').text() - const url = $(el).find('center > a:eq(1)').attr('href') - const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/) - const site_id = `${number}/${slug}` - if (channels[site_id] === undefined) { - channels[site_id] = { - lang: 'en', - site_id, - name - } - } - }) - } - }) - - return Object.values(channels) - } -} - -function parseStartStop(date, time) { - const [s, e] = time.split(' - ') - const start = parseTime(date, s) - let stop = parseTime(date, e) - if (stop.isBefore(start)) { - stop = stop.add(1, 'd') - } - - return [start, stop] -} - -function parseTime(date, time) { - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD H:mm a', tz) -} - -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 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:mytelly.co.uk') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +doFetch.setDebugger(debug) + +const detailedGuide = true +const tz = 'Europe/London' + +module.exports = { + site: 'mytelly.co.uk', + days: 2, + request: { + headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 OPR/117.0.0.0' + } + }, + url({ date, channel }) { + return `https://www.mytelly.co.uk/tv-guide/listings/channel/${ + channel.site_id + }.html?dt=${date.format('YYYY-MM-DD')}` + }, + async parser({ content, date }) { + const programs = [] + + if (content) { + const queues = [] + const $ = cheerio.load(content) + + $('table.table > tbody > tr') + .toArray() + .forEach(el => { + const td = $(el).find('td:eq(1)') + const title = td.find('h5 a') + if (detailedGuide) { + queues.push({ url: title.attr('href'), params: module.exports.request }) + } else { + const subtitle = td.find('h6') + const time = $(el).find('td:eq(0)') + let start = parseTime(date, time.text().trim()) + const prev = programs[programs.length - 1] + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseText(title), + subTitle: parseText(subtitle), + start, + stop + }) + } + }) + + if (queues.length) { + await doFetch(queues, (url, res) => { + const $ = cheerio.load(res) + const time = $('center > h5 > b').text() + const title = parseText($('.inner-heading.sub h2')) + const subTitle = parseText($('.tab-pane > h5 > strong')) + const description = parseText($('.tab-pane > .tvbody > p')) + const image = $('.program-media-image img').attr('src') + const category = $('.schedule-attributes-genres span') + .toArray() + .map(el => $(el).text()) + const casts = $('.single-cast-head:not([id])') + .toArray() + .map(el => { + const cast = { name: parseText($(el).find('a')) } + const [, role] = $(el) + .text() + .match(/\((.*)\)/) || [null, null] + if (role) { + cast.role = role + } + return cast + }) + const [start, stop] = parseStartStop(date, time) + let season, episode + if (subTitle) { + const [, ses, epi] = subTitle.match(/Season (\d+), Episode (\d+)/) || [null, null] + if (ses) { + season = parseInt(ses) + } + if (epi) { + episode = parseInt(epi) + } + } + programs.push({ + title, + subTitle, + description, + image, + category, + season, + episode, + actor: casts.filter(c => c.role === 'Actor').map(c => c.name), + director: casts.filter(c => c.role === 'Director').map(c => c.name), + presenter: casts.filter(c => c.role === 'Presenter').map(c => c.name), + start, + stop + }) + }) + } + } + + return programs + }, + async channels() { + const channels = {} + const queues = [{ t: 'p', url: 'https://www.mytelly.co.uk/getform', params: this.request }] + await doFetch(queues, (queue, res) => { + // process form -> provider + if (queue.t === 'p') { + const $ = cheerio.load(res) + $('#guide_provider option') + .toArray() + .forEach(el => { + const opt = $(el) + const provider = opt.attr('value') + queues.push({ + t: 'r', + url: 'https://www.mytelly.co.uk/getregions', + params: { ...this.request, provider } + }) + }) + } + // process provider -> region + if (queue.t === 'r') { + const now = dayjs() + for (const r of Object.values(res)) { + const params = { + provider: queue.params.provider, + region: r.title, + TVperiod: 'Night', + date: now.format('YYYY-MM-DD'), + st: 0, + u_time: now.format('HHmm'), + is_mobile: 1 + } + queues.push({ + t: 's', + method: 'post', + url: 'https://www.mytelly.co.uk/tv-guide/schedule', + params: { ...this.request, data: params } + }) + } + } + // process schedule -> channels + if (queue.t === 's') { + const $ = cheerio.load(res) + $('.channelname').each((i, el) => { + const name = $(el).find('center > a:eq(1)').text() + const url = $(el).find('center > a:eq(1)').attr('href') + const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/) + const site_id = `${number}/${slug}` + if (channels[site_id] === undefined) { + channels[site_id] = { + lang: 'en', + site_id, + name + } + } + }) + } + }) + + return Object.values(channels) + } +} + +function parseStartStop(date, time) { + const [s, e] = time.split(' - ') + const start = parseTime(date, s) + let stop = parseTime(date, e) + if (stop.isBefore(start)) { + stop = stop.add(1, 'd') + } + + return [start, stop] +} + +function parseTime(date, time) { + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD H:mm a', tz) +} + +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/mytelly.co.uk/mytelly.co.uk.test.js b/sites/mytelly.co.uk/mytelly.co.uk.test.js index 6199a416..ff2856c4 100644 --- a/sites/mytelly.co.uk/mytelly.co.uk.test.js +++ b/sites/mytelly.co.uk/mytelly.co.uk.test.js @@ -1,88 +1,88 @@ -const { parser, url } = require('./mytelly.co.uk.config.js') -const axios = require('axios') -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) - -jest.mock('axios') - -const date = dayjs.utc('2024-12-07', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '713/bbc-one-london', - xmltv_id: 'BBCOneLondon.uk' -} - -axios.get.mockImplementation(url => { - if ( - url === - 'https://www.mytelly.co.uk/tv-guide/listings/programme?cid=713&pid=1906433&tm=2024-12-07+00%3A00%3A00' - ) { - return Promise.resolve({ - data: fs.readFileSync(path.join(__dirname, '__data__', 'programme.html')) - }) - } - if ( - url === - 'https://www.mytelly.co.uk/tv-guide/listings/programme?cid=713&pid=5656624&tm=2024-12-07+23%3A35%3A00' - ) { - return Promise.resolve({ - data: fs.readFileSync(path.join(__dirname, '__data__', 'programme2.html')) - }) - } - - return Promise.resolve({ data: '' }) -}) - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.mytelly.co.uk/tv-guide/listings/channel/713/bbc-one-london.html?dt=2024-12-07' - ) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.html')) - const results = (await parser({ content, channel, date })).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(2) - expect(results[0]).toMatchObject({ - start: '2024-12-07T00:00:00.000Z', - stop: '2024-12-07T02:05:00.000Z', - title: 'Captain Phillips', - description: - 'An American cargo ship sets a dangerous course around the coast of Somalia, while inland, four men are pressed into service as pirates by the local warlords. The captain is taken hostage when the raiding party hijacks the vessel, resulting in a tense five-day crisis. Fact-based thriller, starring Tom Hanks and Barkhad Abdi', - image: - 'https://d16ia5iwuvax6y.cloudfront.net/uk-prog-images/c44ce7b0d3ae602c0c93ece5af140815.jpg?k=VeeNdUjml3bSHdlZ0OXbGLy%2BmsLdYPwTV6iAxGkzq4dsylOCGGE7OWlqwSWt0cd0Qtrin4DkEMC0Zzdp8ZeNk2vNIQzjMF0DG0h3IeTR5NM%3D', - category: ['Factual', 'Movie/Drama', 'Thriller'] - }) - expect(results[1]).toMatchObject({ - start: '2024-12-07T23:35:00.000Z', - stop: '2024-12-08T00:40:00.000Z', - title: 'The Rap Game UK', - subTitle: 'Past and Pressure Season 6, Episode 5', - description: - 'The artists are tasked with writing a song about their heritage. For some, the pressure of the competition proves too much for them to match. In their final challenge, they are put face to face with industry experts who grill them about their plans after the competition. Some impress, while others leave the mentors confused', - image: - 'https://d16ia5iwuvax6y.cloudfront.net/uk-prog-images/2039278182b27cc279570b9ab9b89379.jpg?k=VeeNdUjml3bSHdlZ0OXbGLy%2BmsLdYPwTV6iAxGkzq4cDhR7jXTNFW3tgwQCdOPUobhXwlT81mIsqOe93HPusDG6tw1aoeYOgafojtynNWxc%3D', - category: ['Challenge/Reality Show', 'Show/Game Show'], - season: 6, - episode: 5 - }) -}) - -it('can handle empty guide', async () => { - const result = await parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./mytelly.co.uk.config.js') +const axios = require('axios') +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) + +jest.mock('axios') + +const date = dayjs.utc('2024-12-07', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '713/bbc-one-london', + xmltv_id: 'BBCOneLondon.uk' +} + +axios.get.mockImplementation(url => { + if ( + url === + 'https://www.mytelly.co.uk/tv-guide/listings/programme?cid=713&pid=1906433&tm=2024-12-07+00%3A00%3A00' + ) { + return Promise.resolve({ + data: fs.readFileSync(path.join(__dirname, '__data__', 'programme.html')) + }) + } + if ( + url === + 'https://www.mytelly.co.uk/tv-guide/listings/programme?cid=713&pid=5656624&tm=2024-12-07+23%3A35%3A00' + ) { + return Promise.resolve({ + data: fs.readFileSync(path.join(__dirname, '__data__', 'programme2.html')) + }) + } + + return Promise.resolve({ data: '' }) +}) + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.mytelly.co.uk/tv-guide/listings/channel/713/bbc-one-london.html?dt=2024-12-07' + ) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.html')) + const results = (await parser({ content, channel, date })).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(2) + expect(results[0]).toMatchObject({ + start: '2024-12-07T00:00:00.000Z', + stop: '2024-12-07T02:05:00.000Z', + title: 'Captain Phillips', + description: + 'An American cargo ship sets a dangerous course around the coast of Somalia, while inland, four men are pressed into service as pirates by the local warlords. The captain is taken hostage when the raiding party hijacks the vessel, resulting in a tense five-day crisis. Fact-based thriller, starring Tom Hanks and Barkhad Abdi', + image: + 'https://d16ia5iwuvax6y.cloudfront.net/uk-prog-images/c44ce7b0d3ae602c0c93ece5af140815.jpg?k=VeeNdUjml3bSHdlZ0OXbGLy%2BmsLdYPwTV6iAxGkzq4dsylOCGGE7OWlqwSWt0cd0Qtrin4DkEMC0Zzdp8ZeNk2vNIQzjMF0DG0h3IeTR5NM%3D', + category: ['Factual', 'Movie/Drama', 'Thriller'] + }) + expect(results[1]).toMatchObject({ + start: '2024-12-07T23:35:00.000Z', + stop: '2024-12-08T00:40:00.000Z', + title: 'The Rap Game UK', + subTitle: 'Past and Pressure Season 6, Episode 5', + description: + 'The artists are tasked with writing a song about their heritage. For some, the pressure of the competition proves too much for them to match. In their final challenge, they are put face to face with industry experts who grill them about their plans after the competition. Some impress, while others leave the mentors confused', + image: + 'https://d16ia5iwuvax6y.cloudfront.net/uk-prog-images/2039278182b27cc279570b9ab9b89379.jpg?k=VeeNdUjml3bSHdlZ0OXbGLy%2BmsLdYPwTV6iAxGkzq4cDhR7jXTNFW3tgwQCdOPUobhXwlT81mIsqOe93HPusDG6tw1aoeYOgafojtynNWxc%3D', + category: ['Challenge/Reality Show', 'Show/Game Show'], + season: 6, + episode: 5 + }) +}) + +it('can handle empty guide', async () => { + const result = await parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mytvsuper.com/mytvsuper.com.config.js b/sites/mytvsuper.com/mytvsuper.com.config.js index 7d3bb17f..8a73c0e9 100644 --- a/sites/mytvsuper.com/mytvsuper.com.config.js +++ b/sites/mytvsuper.com/mytvsuper.com.config.js @@ -1,82 +1,82 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -const API_ENDPOINT = 'https://content-api.mytvsuper.com/v1' - -module.exports = { - site: 'mytvsuper.com', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1h - } - }, - url: function ({ channel, date }) { - return `${API_ENDPOINT}/epg?network_code=${channel.site_id}&from=${date.format( - 'YYYYMMDD' - )}&to=${date.format('YYYYMMDD')}&platform=web` - }, - parser({ content, channel, date }) { - const programs = [] - const items = parseItems(content, date) - for (let item of items) { - const prev = programs[programs.length - 1] - const start = parseStart(item) - const stop = start.add(30, 'm') - if (prev) { - prev.stop = start - } - programs.push({ - title: parseTitle(item, channel), - description: parseDescription(item, channel), - episode: parseInt(item.episode_no), - start: start, - stop: stop - }) - } - - return programs - }, - async channels({ lang }) { - const data = await axios - .get(`${API_ENDPOINT}/channel/list?platform=web`) - .then(r => r.data) - .catch(console.error) - - return data.channels.map(c => { - const name = lang === 'en' ? c.name_en : c.name_tc - - return { - site_id: c.network_code, - name, - lang - } - }) - } -} - -function parseTitle(item, channel) { - return channel.lang === 'en' ? item.programme_title_en : item.programme_title_tc -} - -function parseDescription(item, channel) { - return channel.lang === 'en' ? item.episode_synopsis_en : item.episode_synopsis_tc -} - -function parseStart(item) { - return dayjs.tz(item.start_datetime, 'Asia/Hong_Kong') -} - -function parseItems(content, date) { - const data = JSON.parse(content) - if (!Array.isArray(data) || !data.length || !Array.isArray(data[0].item)) return [] - const dayData = data[0].item.find(i => i.date === date.format('YYYY-MM-DD')) - if (!dayData || !Array.isArray(dayData.epg)) return [] - - return dayData.epg -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +const API_ENDPOINT = 'https://content-api.mytvsuper.com/v1' + +module.exports = { + site: 'mytvsuper.com', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1h + } + }, + url: function ({ channel, date }) { + return `${API_ENDPOINT}/epg?network_code=${channel.site_id}&from=${date.format( + 'YYYYMMDD' + )}&to=${date.format('YYYYMMDD')}&platform=web` + }, + parser({ content, channel, date }) { + const programs = [] + const items = parseItems(content, date) + for (let item of items) { + const prev = programs[programs.length - 1] + const start = parseStart(item) + const stop = start.add(30, 'm') + if (prev) { + prev.stop = start + } + programs.push({ + title: parseTitle(item, channel), + description: parseDescription(item, channel), + episode: parseInt(item.episode_no), + start: start, + stop: stop + }) + } + + return programs + }, + async channels({ lang }) { + const data = await axios + .get(`${API_ENDPOINT}/channel/list?platform=web`) + .then(r => r.data) + .catch(console.error) + + return data.channels.map(c => { + const name = lang === 'en' ? c.name_en : c.name_tc + + return { + site_id: c.network_code, + name, + lang + } + }) + } +} + +function parseTitle(item, channel) { + return channel.lang === 'en' ? item.programme_title_en : item.programme_title_tc +} + +function parseDescription(item, channel) { + return channel.lang === 'en' ? item.episode_synopsis_en : item.episode_synopsis_tc +} + +function parseStart(item) { + return dayjs.tz(item.start_datetime, 'Asia/Hong_Kong') +} + +function parseItems(content, date) { + const data = JSON.parse(content) + if (!Array.isArray(data) || !data.length || !Array.isArray(data[0].item)) return [] + const dayData = data[0].item.find(i => i.date === date.format('YYYY-MM-DD')) + if (!dayData || !Array.isArray(dayData.epg)) return [] + + return dayData.epg +} diff --git a/sites/mytvsuper.com/mytvsuper.com.test.js b/sites/mytvsuper.com/mytvsuper.com.test.js index 767735bf..c7a3abb0 100644 --- a/sites/mytvsuper.com/mytvsuper.com.test.js +++ b/sites/mytvsuper.com/mytvsuper.com.test.js @@ -1,68 +1,68 @@ -const { parser, url } = require('./mytvsuper.com.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) - -jest.mock('axios') - -const date = dayjs.utc('2022-11-15', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'B', - xmltv_id: 'J2.hk', - lang: 'zh' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://content-api.mytvsuper.com/v1/epg?network_code=B&from=20221115&to=20221115&platform=web' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-14T22:00:00.000Z', - stop: '2022-11-14T23:00:00.000Z', - title: '想見你#3[粵/普][PG]', - description: - '韻如因父母離婚都不要自己而跑出家門,遇到子維,兩人互吐心事。雨萱順著照片上的唱片行線索,找到一家同名咖啡店,從文磊處得知照片中人是已經過世的韻如,從而推測那個男生也不是詮勝,但她內心反而更加痛苦。', - episode: 1000003 - }) -}) - -it('can parse response in English', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const channelEN = { ...channel, lang: 'en' } - let results = parser({ content, channel: channelEN, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-14T22:00:00.000Z', - stop: '2022-11-14T23:00:00.000Z', - title: 'Someday or One Day#3[Can/Man][PG]', - description: 'Description', - episode: 1000003 - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const results = parser({ date, channel, content }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./mytvsuper.com.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) + +jest.mock('axios') + +const date = dayjs.utc('2022-11-15', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'B', + xmltv_id: 'J2.hk', + lang: 'zh' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://content-api.mytvsuper.com/v1/epg?network_code=B&from=20221115&to=20221115&platform=web' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-14T22:00:00.000Z', + stop: '2022-11-14T23:00:00.000Z', + title: '想見你#3[粵/普][PG]', + description: + '韻如因父母離婚都不要自己而跑出家門,遇到子維,兩人互吐心事。雨萱順著照片上的唱片行線索,找到一家同名咖啡店,從文磊處得知照片中人是已經過世的韻如,從而推測那個男生也不是詮勝,但她內心反而更加痛苦。', + episode: 1000003 + }) +}) + +it('can parse response in English', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const channelEN = { ...channel, lang: 'en' } + let results = parser({ content, channel: channelEN, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-14T22:00:00.000Z', + stop: '2022-11-14T23:00:00.000Z', + title: 'Someday or One Day#3[Can/Man][PG]', + description: 'Description', + episode: 1000003 + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const results = parser({ date, channel, content }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/neo.io/__data__/content.json b/sites/neo.io/__data__/content.json new file mode 100644 index 00000000..92aa32fe --- /dev/null +++ b/sites/neo.io/__data__/content.json @@ -0,0 +1,64 @@ +{ + "shows": [ + { + "title": "Napovedujemo", + "show_start": 1735185900, + "show_end": 1735192200, + "timestamp": "5:05 - 6:50", + "show_id": "CUP_IECOM_SLO1_10004660", + "thumbnail": "https://ngimg.siol.tv/sioltv/mtcmsprod/52/0/0/5200d01a-fe5f-487e-835a-274e77227a6b.jpg", + "is_adult": false, + "friendly_id": "napovedujemo_db48", + "pg": "", + "genres": [ + "napovednik" + ], + "year": 0, + "summary": "Vabilo k ogledu naših oddaj.", + "categories": "Ostalo", + "stb_only": false, + "is_live": false, + "original_title": "Napovedujemo" + }, + { + "title": "S0E0 - Hrabri zajčki: Prvi sneg", + "show_start": 1735192200, + "show_end": 1735192800, + "timestamp": "6:50 - 7:00", + "show_id": "CUP_IECOM_SLO1_79637910", + "thumbnail": "https://ngimg.siol.tv/sioltv/mtcmsprod/d6/4/5/d6456f4a-4f0a-4825-90c1-1749abd59688.jpg", + "is_adult": false, + "friendly_id": "hrabri_zajcki_prvi_sneg_1619", + "pg": "", + "genres": [ + "risanka" + ], + "year": 2020, + "summary": "Hrabri zajčki so prispeli v borov gozd in izkusili prvi sneg. Bob in Bu še nikoli nista videla snega. Mami kuha korenčkov kakav, Bu in Bob pa kmalu spoznata novega prijatelja, losa Danija.", + "categories": "Otroški/Mladinski", + "stb_only": false, + "is_live": false, + "original_title": "S0E0 - Brave Bunnies" + }, + { + "title": "Dobro jutro", + "show_start": 1735192800, + "show_end": 1735203900, + "timestamp": "7:00 - 10:05", + "show_id": "CUP_IECOM_SLO1_79637911", + "thumbnail": "https://ngimg.siol.tv/sioltv/mtcmsprod/e1/2/d/e12d8eb4-693a-43d3-89d4-fd96dade9f0f.jpg", + "is_adult": false, + "friendly_id": "dobro_jutro_2f10", + "pg": "", + "genres": [ + "zabavna oddaja" + ], + "year": 2024, + "summary": "Oddaja Dobro jutro poleg informativnih in zabavnih vsebin podaja koristne nasvete o najrazličnejših tematikah iz vsakdanjega življenja.", + "categories": "Razvedrilni program", + "stb_only": false, + "is_live": false, + "original_title": "Dobro jutro" + } + ] + } \ No newline at end of file diff --git a/sites/neo.io/__data__/no_content.json b/sites/neo.io/__data__/no_content.json new file mode 100644 index 00000000..78743050 --- /dev/null +++ b/sites/neo.io/__data__/no_content.json @@ -0,0 +1 @@ +{"shows":[]} \ No newline at end of file diff --git a/sites/neo.io/neo.io.config.js b/sites/neo.io/neo.io.config.js index bea58fca..e0c5ae20 100644 --- a/sites/neo.io/neo.io.config.js +++ b/sites/neo.io/neo.io.config.js @@ -1,80 +1,80 @@ -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') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'neo.io', - timezone: 'Europe/Ljubljana', - days: 5, - url() { - return 'https://stargate.telekom.si/api/titan.tv.WebEpg/GetWebEpgData' - }, - request: { - method: 'POST', - headers: { - Host: 'stargate.telekom.si', - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0', - Accept: 'application/json, text/plain, */*', - 'Accept-Language': 'nl,en-US;q=0.7,en;q=0.3', - 'Content-Type': 'application/json', - 'X-AppLayout': '1', - 'x-language': 'sl', - Origin: 'https://neo.io', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'cross-site', - 'Sec-GPC': '1', - Connection: 'keep-alive' - }, - data({ channel, date }) { - const todayEpoch = date.startOf('day').unix() - const nextDayEpoch = date.add(1, 'day').startOf('day').unix() - return JSON.stringify({ - ch_ext_id: channel.site_id, - from: todayEpoch, - to: nextDayEpoch - }) - } - }, - parser: function ({ content }) { - const programs = [] - const data = JSON.parse(content) - data.shows.forEach(show => { - const start = dayjs.unix(show.show_start).utc() - const stop = dayjs.unix(show.show_end).utc() - const programData = { - title: show.title, - description: show.summary || 'No description available', - start: start.toISOString(), - stop: stop.toISOString(), - thumbnail: show.thumbnail - } - programs.push(programData) - }) - return programs - }, - async channels() { - const response = await axios.post( - 'https://stargate.telekom.si/api/titan.tv.WebEpg/ZapList', - JSON.stringify({ includeRadioStations: true }), - { - headers: this.request.headers - } - ) - - const data = response.data.data - return data.map(item => ({ - lang: 'sq', - name: String(item.channel.title), - site_id: String(item.channel.id) - //logo: String(item.channel.logo) - })) - } -} +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') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'neo.io', + timezone: 'Europe/Ljubljana', + days: 5, + url() { + return 'https://stargate.telekom.si/api/titan.tv.WebEpg/GetWebEpgData' + }, + request: { + method: 'POST', + headers: { + Host: 'stargate.telekom.si', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0', + Accept: 'application/json, text/plain, */*', + 'Accept-Language': 'nl,en-US;q=0.7,en;q=0.3', + 'Content-Type': 'application/json', + 'X-AppLayout': '1', + 'x-language': 'sl', + Origin: 'https://neo.io', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'cross-site', + 'Sec-GPC': '1', + Connection: 'keep-alive' + }, + data({ channel, date }) { + const todayEpoch = date.startOf('day').unix() + const nextDayEpoch = date.add(1, 'day').startOf('day').unix() + return JSON.stringify({ + ch_ext_id: channel.site_id, + from: todayEpoch, + to: nextDayEpoch + }) + } + }, + parser: function ({ content }) { + const programs = [] + const data = JSON.parse(content) + data.shows.forEach(show => { + const start = dayjs.unix(show.show_start).utc() + const stop = dayjs.unix(show.show_end).utc() + const programData = { + title: show.title, + description: show.summary || 'No description available', + start: start.toISOString(), + stop: stop.toISOString(), + thumbnail: show.thumbnail + } + programs.push(programData) + }) + return programs + }, + async channels() { + const response = await axios.post( + 'https://stargate.telekom.si/api/titan.tv.WebEpg/ZapList', + JSON.stringify({ includeRadioStations: true }), + { + headers: this.request.headers + } + ) + + const data = response.data.data + return data.map(item => ({ + lang: 'sq', + name: String(item.channel.title), + site_id: String(item.channel.id) + //logo: String(item.channel.logo) + })) + } +} diff --git a/sites/neo.io/neo.io.test.js b/sites/neo.io/neo.io.test.js index 295120f7..afb05a55 100644 --- a/sites/neo.io/neo.io.test.js +++ b/sites/neo.io/neo.io.test.js @@ -1,124 +1,61 @@ -const { parser, url } = require('./neo.io.config.js') -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('2024-12-26', 'YYYY-MM-DD').startOf('day') -const channel = { - site_id: 'tv-slo-1', - xmltv_id: 'TVSLO1.si' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://stargate.telekom.si/api/titan.tv.WebEpg/GetWebEpgData' - ) -}) - -it('can parse response', () => { - const content = ` - { - "shows": [ - { - "title": "Napovedujemo", - "show_start": 1735185900, - "show_end": 1735192200, - "timestamp": "5:05 - 6:50", - "show_id": "CUP_IECOM_SLO1_10004660", - "thumbnail": "https://ngimg.siol.tv/sioltv/mtcmsprod/52/0/0/5200d01a-fe5f-487e-835a-274e77227a6b.jpg", - "is_adult": false, - "friendly_id": "napovedujemo_db48", - "pg": "", - "genres": [ - "napovednik" - ], - "year": 0, - "summary": "Vabilo k ogledu naših oddaj.", - "categories": "Ostalo", - "stb_only": false, - "is_live": false, - "original_title": "Napovedujemo" - }, - { - "title": "S0E0 - Hrabri zajčki: Prvi sneg", - "show_start": 1735192200, - "show_end": 1735192800, - "timestamp": "6:50 - 7:00", - "show_id": "CUP_IECOM_SLO1_79637910", - "thumbnail": "https://ngimg.siol.tv/sioltv/mtcmsprod/d6/4/5/d6456f4a-4f0a-4825-90c1-1749abd59688.jpg", - "is_adult": false, - "friendly_id": "hrabri_zajcki_prvi_sneg_1619", - "pg": "", - "genres": [ - "risanka" - ], - "year": 2020, - "summary": "Hrabri zajčki so prispeli v borov gozd in izkusili prvi sneg. Bob in Bu še nikoli nista videla snega. Mami kuha korenčkov kakav, Bu in Bob pa kmalu spoznata novega prijatelja, losa Danija.", - "categories": "Otroški/Mladinski", - "stb_only": false, - "is_live": false, - "original_title": "S0E0 - Brave Bunnies" - }, - { - "title": "Dobro jutro", - "show_start": 1735192800, - "show_end": 1735203900, - "timestamp": "7:00 - 10:05", - "show_id": "CUP_IECOM_SLO1_79637911", - "thumbnail": "https://ngimg.siol.tv/sioltv/mtcmsprod/e1/2/d/e12d8eb4-693a-43d3-89d4-fd96dade9f0f.jpg", - "is_adult": false, - "friendly_id": "dobro_jutro_2f10", - "pg": "", - "genres": [ - "zabavna oddaja" - ], - "year": 2024, - "summary": "Oddaja Dobro jutro poleg informativnih in zabavnih vsebin podaja koristne nasvete o najrazličnejših tematikah iz vsakdanjega življenja.", - "categories": "Razvedrilni program", - "stb_only": false, - "is_live": false, - "original_title": "Dobro jutro" - } - ] - }` - - const result = parser({ content, channel }) - - expect(result).toMatchObject([ - { - title: 'Napovedujemo', - description: 'Vabilo k ogledu naših oddaj.', - start: '2024-12-26T04:05:00.000Z', - stop: '2024-12-26T05:50:00.000Z', - thumbnail: - 'https://ngimg.siol.tv/sioltv/mtcmsprod/52/0/0/5200d01a-fe5f-487e-835a-274e77227a6b.jpg' - }, - { - title: 'S0E0 - Hrabri zajčki: Prvi sneg', - description: - 'Hrabri zajčki so prispeli v borov gozd in izkusili prvi sneg. Bob in Bu še nikoli nista videla snega. Mami kuha korenčkov kakav, Bu in Bob pa kmalu spoznata novega prijatelja, losa Danija.', - start: '2024-12-26T05:50:00.000Z', - stop: '2024-12-26T06:00:00.000Z', - thumbnail: - 'https://ngimg.siol.tv/sioltv/mtcmsprod/d6/4/5/d6456f4a-4f0a-4825-90c1-1749abd59688.jpg' - }, - { - title: 'Dobro jutro', - description: - 'Oddaja Dobro jutro poleg informativnih in zabavnih vsebin podaja koristne nasvete o najrazličnejših tematikah iz vsakdanjega življenja.', - start: '2024-12-26T06:00:00.000Z', - stop: '2024-12-26T09:05:00.000Z', - thumbnail: - 'https://ngimg.siol.tv/sioltv/mtcmsprod/e1/2/d/e12d8eb4-693a-43d3-89d4-fd96dade9f0f.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '{"shows":[]}' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./neo.io.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('2024-12-26', 'YYYY-MM-DD').startOf('day') +const channel = { + site_id: 'tv-slo-1', + xmltv_id: 'TVSLO1.si' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://stargate.telekom.si/api/titan.tv.WebEpg/GetWebEpgData' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content, channel }) + + expect(result).toMatchObject([ + { + title: 'Napovedujemo', + description: 'Vabilo k ogledu naših oddaj.', + start: '2024-12-26T04:05:00.000Z', + stop: '2024-12-26T05:50:00.000Z', + thumbnail: + 'https://ngimg.siol.tv/sioltv/mtcmsprod/52/0/0/5200d01a-fe5f-487e-835a-274e77227a6b.jpg' + }, + { + title: 'S0E0 - Hrabri zajčki: Prvi sneg', + description: + 'Hrabri zajčki so prispeli v borov gozd in izkusili prvi sneg. Bob in Bu še nikoli nista videla snega. Mami kuha korenčkov kakav, Bu in Bob pa kmalu spoznata novega prijatelja, losa Danija.', + start: '2024-12-26T05:50:00.000Z', + stop: '2024-12-26T06:00:00.000Z', + thumbnail: + 'https://ngimg.siol.tv/sioltv/mtcmsprod/d6/4/5/d6456f4a-4f0a-4825-90c1-1749abd59688.jpg' + }, + { + title: 'Dobro jutro', + description: + 'Oddaja Dobro jutro poleg informativnih in zabavnih vsebin podaja koristne nasvete o najrazličnejših tematikah iz vsakdanjega življenja.', + start: '2024-12-26T06:00:00.000Z', + stop: '2024-12-26T09:05:00.000Z', + thumbnail: + 'https://ngimg.siol.tv/sioltv/mtcmsprod/e1/2/d/e12d8eb4-693a-43d3-89d4-fd96dade9f0f.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/nhkworldpremium.com/nhkworldpremium.com.config.js b/sites/nhkworldpremium.com/nhkworldpremium.com.config.js index 79ba59bf..a514c6b3 100644 --- a/sites/nhkworldpremium.com/nhkworldpremium.com.config.js +++ b/sites/nhkworldpremium.com/nhkworldpremium.com.config.js @@ -1,56 +1,56 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'nhkworldpremium.com', - days: 7, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url({ channel }) { - return `https://nhkworldpremium.com/backend/api/v1/front/episodes?lang=${channel.lang}` - }, - parser({ content, date }) { - let programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const start = dayjs.tz(item.schedule, 'Asia/Seoul') - const duration = parseDuration(item) - const stop = start.add(duration, 's') - programs.push({ - title: item.programTitle, - sub_title: item.episodeTitle, - start, - stop - }) - }) - - return programs - } -} - -function parseDuration(item) { - const [h, m, s] = item.period.split(':') - - if (!h || !m || !s) return 0 - - return parseInt(h) * 3600 + parseInt(m) * 60 + parseInt(s) -} - -function parseItems(content, date) { - try { - const data = JSON.parse(content) - - if (!data || !data.item || !Array.isArray(data.item.episodes)) return [] - - return data.item.episodes.filter(ep => ep.schedule.startsWith(date.format('YYYY-MM-DD'))) - } catch { - return [] - } -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'nhkworldpremium.com', + days: 7, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url({ channel }) { + return `https://nhkworldpremium.com/backend/api/v1/front/episodes?lang=${channel.lang}` + }, + parser({ content, date }) { + let programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const start = dayjs.tz(item.schedule, 'Asia/Seoul') + const duration = parseDuration(item) + const stop = start.add(duration, 's') + programs.push({ + title: item.programTitle, + sub_title: item.episodeTitle, + start, + stop + }) + }) + + return programs + } +} + +function parseDuration(item) { + const [h, m, s] = item.period.split(':') + + if (!h || !m || !s) return 0 + + return parseInt(h) * 3600 + parseInt(m) * 60 + parseInt(s) +} + +function parseItems(content, date) { + try { + const data = JSON.parse(content) + + if (!data || !data.item || !Array.isArray(data.item.episodes)) return [] + + return data.item.episodes.filter(ep => ep.schedule.startsWith(date.format('YYYY-MM-DD'))) + } catch { + return [] + } +} diff --git a/sites/nhkworldpremium.com/nhkworldpremium.com.test.js b/sites/nhkworldpremium.com/nhkworldpremium.com.test.js index fc181b1e..f6e2ab04 100644 --- a/sites/nhkworldpremium.com/nhkworldpremium.com.test.js +++ b/sites/nhkworldpremium.com/nhkworldpremium.com.test.js @@ -1,87 +1,87 @@ -const { parser, url } = require('./nhkworldpremium.com.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) - -jest.mock('axios') - -const date = dayjs.utc('2023-07-10', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '#', - xmltv_id: 'NHKWorldPremium.jp', - lang: 'en' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://nhkworldpremium.com/backend/api/v1/front/episodes?lang=en') -}) - -it('can generate valid url for Japanese guide', () => { - const channel = { - site_id: '#', - xmltv_id: 'NHKWorldPremium.jp', - lang: 'ja' - } - - expect(url({ channel })).toBe('https://nhkworldpremium.com/backend/api/v1/front/episodes?lang=ja') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_en.json')) - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(56) - expect(results[0]).toMatchObject({ - start: '2023-07-09T15:35:00.000Z', - stop: '2023-07-09T16:20:00.000Z', - title: 'NHK Amateur Singing Contest', - sub_title: '"Maizuru City, Kyoto Prefecture"' - }) - - expect(results[55]).toMatchObject({ - start: '2023-07-10T14:35:00.000Z', - stop: '2023-07-10T15:15:00.000Z', - title: 'International News Report 2023', - sub_title: null - }) -}) - -it('can parse response with Japanese guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_ja.json')) - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(56) - expect(results[0]).toMatchObject({ - start: '2023-07-09T15:35:00.000Z', - stop: '2023-07-09T16:20:00.000Z', - title: 'NHKのど自慢', - sub_title: '【京都から生放送!▽前川清・相川七瀬】' - }) - - expect(results[55]).toMatchObject({ - start: '2023-07-10T14:35:00.000Z', - stop: '2023-07-10T15:15:00.000Z', - title: '国際報道2023', - sub_title: null - }) -}) - -it('can handle empty guide', () => { - const results = parser({ content: {}, date }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./nhkworldpremium.com.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) + +jest.mock('axios') + +const date = dayjs.utc('2023-07-10', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '#', + xmltv_id: 'NHKWorldPremium.jp', + lang: 'en' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://nhkworldpremium.com/backend/api/v1/front/episodes?lang=en') +}) + +it('can generate valid url for Japanese guide', () => { + const channel = { + site_id: '#', + xmltv_id: 'NHKWorldPremium.jp', + lang: 'ja' + } + + expect(url({ channel })).toBe('https://nhkworldpremium.com/backend/api/v1/front/episodes?lang=ja') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_en.json')) + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(56) + expect(results[0]).toMatchObject({ + start: '2023-07-09T15:35:00.000Z', + stop: '2023-07-09T16:20:00.000Z', + title: 'NHK Amateur Singing Contest', + sub_title: '"Maizuru City, Kyoto Prefecture"' + }) + + expect(results[55]).toMatchObject({ + start: '2023-07-10T14:35:00.000Z', + stop: '2023-07-10T15:15:00.000Z', + title: 'International News Report 2023', + sub_title: null + }) +}) + +it('can parse response with Japanese guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_ja.json')) + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(56) + expect(results[0]).toMatchObject({ + start: '2023-07-09T15:35:00.000Z', + stop: '2023-07-09T16:20:00.000Z', + title: 'NHKのど自慢', + sub_title: '【京都から生放送!▽前川清・相川七瀬】' + }) + + expect(results[55]).toMatchObject({ + start: '2023-07-10T14:35:00.000Z', + stop: '2023-07-10T15:15:00.000Z', + title: '国際報道2023', + sub_title: null + }) +}) + +it('can handle empty guide', () => { + const results = parser({ content: {}, date }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/nhl.com/nhl.com.config.js b/sites/nhl.com/nhl.com.config.js index 46127652..ab059ef5 100644 --- a/sites/nhl.com/nhl.com.config.js +++ b/sites/nhl.com/nhl.com.config.js @@ -1,46 +1,46 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'nhl.com', - // I'm not sure what `endDate` represents but they only return 1 day of - // results, with `endTime`s ocassionally in the following day. - days: 1, - url: ({ date }) => - `https://api-web.nhle.com/v1/network/tv-schedule/${date.toJSON().split('T')[0]}`, - parser({ content }) { - const programs = [] - const items = parseItems(content) - for (const item of items) { - programs.push({ - title: item.title, - description: item.description === item.title ? undefined : item.description, - category: 'Sports', - // image: parseImage(item), - start: parseStart(item), - stop: parseStop(item) - }) - } - - return programs - } -} - -// Unfortunately I couldn't determine how these are -// supposed to be formatted. Pointers appreciated! -// function parseImage(item) { -// const uri = item.broadcastImageUrl - -// return uri ? `https://???/${uri}` : null -// } - -function parseStart(item) { - return dayjs(item.startTime) -} - -function parseStop(item) { - return dayjs(item.endTime) -} - -function parseItems(content) { - return JSON.parse(content).broadcasts -} +const dayjs = require('dayjs') + +module.exports = { + site: 'nhl.com', + // I'm not sure what `endDate` represents but they only return 1 day of + // results, with `endTime`s ocassionally in the following day. + days: 1, + url: ({ date }) => + `https://api-web.nhle.com/v1/network/tv-schedule/${date.toJSON().split('T')[0]}`, + parser({ content }) { + const programs = [] + const items = parseItems(content) + for (const item of items) { + programs.push({ + title: item.title, + description: item.description === item.title ? undefined : item.description, + category: 'Sports', + // image: parseImage(item), + start: parseStart(item), + stop: parseStop(item) + }) + } + + return programs + } +} + +// Unfortunately I couldn't determine how these are +// supposed to be formatted. Pointers appreciated! +// function parseImage(item) { +// const uri = item.broadcastImageUrl + +// return uri ? `https://???/${uri}` : null +// } + +function parseStart(item) { + return dayjs(item.startTime) +} + +function parseStop(item) { + return dayjs(item.endTime) +} + +function parseItems(content) { + return JSON.parse(content).broadcasts +} diff --git a/sites/nhl.com/nhl.com.test.js b/sites/nhl.com/nhl.com.test.js index 49ce4a0e..1e4ed97b 100644 --- a/sites/nhl.com/nhl.com.test.js +++ b/sites/nhl.com/nhl.com.test.js @@ -1,44 +1,44 @@ -const { parser, url } = require('./nhl.com.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('2024-11-21', 'YYYY-MM-DD').startOf('d') - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://api-web.nhle.com/v1/network/tv-schedule/2024-11-21') -}) - -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: '2024-11-21T12:00:00.000Z', - stop: '2024-11-21T13:00:00.000Z', - title: 'On The Fly', - category: 'Sports' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: JSON.stringify({ - // extra props not necessary but they form a valid response - date: '2024-11-21', - startDate: '2024-11-07', - endDate: '2024-12-05', - broadcasts: [] - }) - }) - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./nhl.com.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('2024-11-21', 'YYYY-MM-DD').startOf('d') + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://api-web.nhle.com/v1/network/tv-schedule/2024-11-21') +}) + +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: '2024-11-21T12:00:00.000Z', + stop: '2024-11-21T13:00:00.000Z', + title: 'On The Fly', + category: 'Sports' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: JSON.stringify({ + // extra props not necessary but they form a valid response + date: '2024-11-21', + startDate: '2024-11-07', + endDate: '2024-12-05', + broadcasts: [] + }) + }) + expect(results).toMatchObject([]) +}) diff --git a/sites/nostv.pt/nostv.pt.config.js b/sites/nostv.pt/nostv.pt.config.js index ffeb7a6e..26ae443c 100644 --- a/sites/nostv.pt/nostv.pt.config.js +++ b/sites/nostv.pt/nostv.pt.config.js @@ -1,71 +1,71 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -const headers = { - 'X-Apikey': 'xe1dgrShwdR1DVOKGmsj8Ut4QLlGyOFI', - 'X-Core-Appversion': '2.20.0.3', - 'X-Core-Contentratinglimit': '0', - 'X-Core-Deviceid': '', - 'X-Core-Devicetype': 'web', - Origin: 'https://nostv.pt', - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' -} - -module.exports = { - site: 'nostv.pt', - days: 2, - url({ channel, date }) { - return `https://api.clg.nos.pt/nostv/ott/schedule/range/contents/guest?channels=${channel.site_id - }&minDate=${date.format('YYYY-MM-DD')}T00:00:00Z&maxDate=${date.format( - 'YYYY-MM-DD' - )}T23:59:59Z&isDateInclusive=true&client_id=${headers['X-Apikey']}` - }, - request: { headers }, - parser({ content }) { - const programs = [] - if (content) { - const items = Array.isArray(content) ? content : JSON.parse(content) - items.forEach(item => { - const image = item.Images - ? `https://mage.stream.nos.pt/mage/v1/Images?sourceUri=${item.Images[0].Url}&profile=ott_1_452x340&client_id=${headers['X-Apikey']}` - : null - programs.push({ - title: item.Metadata?.Title, - sub_title: item.Metadata?.SubTitle ? item.Metadata?.SubTitle : null, - description: item.Metadata?.Description, - season: item.Metadata?.Season, - episode: item.Metadata?.Episode, - icon: { - src: image - }, - image, - start: dayjs.utc(item.UtcDateTimeStart), - stop: dayjs.utc(item.UtcDateTimeEnd) - }) - }) - } - - return programs - }, - async channels() { - const result = await axios - .get( - `https://api.clg.nos.pt/nostv/ott/channels/guest?client_id=${headers['X-Apikey']}`, - { headers } - ) - .then(r => r.data) - .catch(console.error) - - return result.map(item => { - return { - lang: 'pt', - site_id: item.ServiceId, - name: item.Name - } - }) - } -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +const headers = { + 'X-Apikey': 'xe1dgrShwdR1DVOKGmsj8Ut4QLlGyOFI', + 'X-Core-Appversion': '2.20.0.3', + 'X-Core-Contentratinglimit': '0', + 'X-Core-Deviceid': '', + 'X-Core-Devicetype': 'web', + Origin: 'https://nostv.pt', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' +} + +module.exports = { + site: 'nostv.pt', + days: 2, + url({ channel, date }) { + return `https://api.clg.nos.pt/nostv/ott/schedule/range/contents/guest?channels=${channel.site_id + }&minDate=${date.format('YYYY-MM-DD')}T00:00:00Z&maxDate=${date.format( + 'YYYY-MM-DD' + )}T23:59:59Z&isDateInclusive=true&client_id=${headers['X-Apikey']}` + }, + request: { headers }, + parser({ content }) { + const programs = [] + if (content) { + const items = Array.isArray(content) ? content : JSON.parse(content) + items.forEach(item => { + const image = item.Images + ? `https://mage.stream.nos.pt/mage/v1/Images?sourceUri=${item.Images[0].Url}&profile=ott_1_452x340&client_id=${headers['X-Apikey']}` + : null + programs.push({ + title: item.Metadata?.Title, + sub_title: item.Metadata?.SubTitle ? item.Metadata?.SubTitle : null, + description: item.Metadata?.Description, + season: item.Metadata?.Season, + episode: item.Metadata?.Episode, + icon: { + src: image + }, + image, + start: dayjs.utc(item.UtcDateTimeStart), + stop: dayjs.utc(item.UtcDateTimeEnd) + }) + }) + } + + return programs + }, + async channels() { + const result = await axios + .get( + `https://api.clg.nos.pt/nostv/ott/channels/guest?client_id=${headers['X-Apikey']}`, + { headers } + ) + .then(r => r.data) + .catch(console.error) + + return result.map(item => { + return { + lang: 'pt', + site_id: item.ServiceId, + name: item.Name + } + }) + } +} diff --git a/sites/nostv.pt/nostv.pt.test.js b/sites/nostv.pt/nostv.pt.test.js index 939b8aad..c7892bc7 100644 --- a/sites/nostv.pt/nostv.pt.test.js +++ b/sites/nostv.pt/nostv.pt.test.js @@ -1,55 +1,55 @@ -const { parser, url } = require('./nostv.pt.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('2023-12-11').startOf('d') -const channel = { - site_id: '510', - xmltv_id: 'SPlus.pt' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://api.clg.nos.pt/nostv/ott/schedule/range/contents/guest?channels=510&minDate=2023-12-11T00:00:00Z&maxDate=2023-12-11T23:59:59Z&isDateInclusive=true&client_id=xe1dgrShwdR1DVOKGmsj8Ut4QLlGyOFI' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/data.json')) - const results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - const image = 'https://mage.stream.nos.pt/mage/v1/Images?sourceUri=http://vip.pam.local.internal/PAM.Images/Store/8329ed1aec5d4c0faa2056972256ff9f&profile=ott_1_452x340&client_id=xe1dgrShwdR1DVOKGmsj8Ut4QLlGyOFI' - - expect(results[0]).toMatchObject({ - start: '2023-12-11T16:30:00.000Z', - stop: '2023-12-11T17:00:00.000Z', - title: 'Village Vets', - description: - 'A história de dois melhores amigos veterinários e o seu extraordinário trabalho na Austrália.', - season: 1, - episode: 12, - icon: { - src: image - }, - image - }) -}) - -it('can handle empty guide', async () => { - const results = await parser({ - date, - content: '[]' - }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./nostv.pt.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('2023-12-11').startOf('d') +const channel = { + site_id: '510', + xmltv_id: 'SPlus.pt' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://api.clg.nos.pt/nostv/ott/schedule/range/contents/guest?channels=510&minDate=2023-12-11T00:00:00Z&maxDate=2023-12-11T23:59:59Z&isDateInclusive=true&client_id=xe1dgrShwdR1DVOKGmsj8Ut4QLlGyOFI' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/data.json')) + const results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + const image = 'https://mage.stream.nos.pt/mage/v1/Images?sourceUri=http://vip.pam.local.internal/PAM.Images/Store/8329ed1aec5d4c0faa2056972256ff9f&profile=ott_1_452x340&client_id=xe1dgrShwdR1DVOKGmsj8Ut4QLlGyOFI' + + expect(results[0]).toMatchObject({ + start: '2023-12-11T16:30:00.000Z', + stop: '2023-12-11T17:00:00.000Z', + title: 'Village Vets', + description: + 'A história de dois melhores amigos veterinários e o seu extraordinário trabalho na Austrália.', + season: 1, + episode: 12, + icon: { + src: image + }, + image + }) +}) + +it('can handle empty guide', async () => { + const results = await parser({ + date, + content: '[]' + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/novacyprus.com/__data__/content.json b/sites/novacyprus.com/__data__/content.json new file mode 100644 index 00000000..f3504a96 --- /dev/null +++ b/sites/novacyprus.com/__data__/content.json @@ -0,0 +1 @@ +{"nodes":[{"datetime":"2021-11-17 06:20:00","day":"Wednesday","numDay":17,"numMonth":11,"month":"November","channelName":"Cyprus Novacinema1HD","channelLog":"https://ssl2.novago.gr/EPG/jsp/images/universal/film/logo/20200210/000100/XTV100000762/d6a2f5e0-dbc0-49c7-9843-e3161ca5ae5d.png","cid":"42","ChannelId":"614","startingTime":"06:20","endTime":"08:10","title":"Δεσμοί Αίματος","description":"Θρίλερ Μυστηρίου","duration":"109","slotDuration":"110","bref":"COMMOBLOOX","mediaItems":[{"MediaListTypeId":"6","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_GUIDE_STILL.jpg"},{"MediaListTypeId":"7","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_POSTER_CROSS.jpg"},{"MediaListTypeId":"8","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_ICON_CYP.jpg"},{"MediaListTypeId":"9","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_POSTER_CYP.jpg"},{"MediaListTypeId":"10","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_BACKGROUND_CYP.jpg"}]},{"datetime":"2021-11-17 06:00:00","day":"Wednesday","numDay":17,"numMonth":11,"month":"November","channelName":"Cyprus Novacinema2HD","channelLog":"https://ssl2.novago.gr/EPG/jsp/images/universal/film/logo/20200210/000100/XTV100000763/24e05354-d6ad-4949-bcb3-a81d1c1d2cba.png","cid":"62","ChannelId":"653","startingTime":"06:00","endTime":"07:40","title":"Ανυπόφοροι Γείτονες","description":"Κωμωδία","duration":"93","slotDuration":"100","bref":"NEIGHBORSX","mediaItems":[{"MediaListTypeId":"7","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/1/312582_NEIGHBORSX_POSTER_CROSS.jpg"},{"MediaListTypeId":"8","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/1/312582_NEIGHBORSX_ICON_CYP.jpg"},{"MediaListTypeId":"9","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/1/312582_NEIGHBORSX_POSTER_CYP.jpg"},{"MediaListTypeId":"10","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/1/312582_NEIGHBORSX_BACKGROUND_CYP.jpg"}]}]} \ No newline at end of file diff --git a/sites/novacyprus.com/__data__/no_content.json b/sites/novacyprus.com/__data__/no_content.json new file mode 100644 index 00000000..a0b8012c --- /dev/null +++ b/sites/novacyprus.com/__data__/no_content.json @@ -0,0 +1 @@ +{"nodes":[],"total":0,"pages":0} \ No newline at end of file diff --git a/sites/novacyprus.com/novacyprus.com.config.js b/sites/novacyprus.com/novacyprus.com.config.js index 6bf63c84..be6c9ad0 100644 --- a/sites/novacyprus.com/novacyprus.com.config.js +++ b/sites/novacyprus.com/novacyprus.com.config.js @@ -1,67 +1,67 @@ -process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0 - -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') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'novacyprus.com', - days: 2, - url({ date }) { - return `https://www.novacyprus.com/api/v1/tvprogram/from/${date.format('YYYYMMDD')}/to/${date - .add(1, 'd') - .format('YYYYMMDD')}` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const start = parseStart(item) - const stop = start.add(item.slotDuration, 'm') - programs.push({ - title: item.title, - description: item.description, - image: parseImage(item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const channels = await axios - .get('https://www.novacyprus.com/api/v1/guide/dailychannels') - .then(r => r.data) - .catch(console.log) - - return channels.map(item => { - return { - lang: 'el', - site_id: item.ChannelId, - name: item.nameEl - } - }) - } -} - -function parseStart(item) { - return dayjs.tz(item.datetime, 'YYYY-MM-DD HH:mm:ss', 'Asia/Nicosia') -} - -function parseImage(item) { - return item.mediaItems.length ? item.mediaItems[0].CdnUrl : null -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.nodes)) return [] - - return data.nodes.filter(i => i.ChannelId === channel.site_id) -} +process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0 + +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') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'novacyprus.com', + days: 2, + url({ date }) { + return `https://www.novacyprus.com/api/v1/tvprogram/from/${date.format('YYYYMMDD')}/to/${date + .add(1, 'd') + .format('YYYYMMDD')}` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const start = parseStart(item) + const stop = start.add(item.slotDuration, 'm') + programs.push({ + title: item.title, + description: item.description, + image: parseImage(item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const channels = await axios + .get('https://www.novacyprus.com/api/v1/guide/dailychannels') + .then(r => r.data) + .catch(console.log) + + return channels.map(item => { + return { + lang: 'el', + site_id: item.ChannelId, + name: item.nameEl + } + }) + } +} + +function parseStart(item) { + return dayjs.tz(item.datetime, 'YYYY-MM-DD HH:mm:ss', 'Asia/Nicosia') +} + +function parseImage(item) { + return item.mediaItems.length ? item.mediaItems[0].CdnUrl : null +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.nodes)) return [] + + return data.nodes.filter(i => i.ChannelId === channel.site_id) +} diff --git a/sites/novacyprus.com/novacyprus.com.test.js b/sites/novacyprus.com/novacyprus.com.test.js index 095b5998..c2585d92 100644 --- a/sites/novacyprus.com/novacyprus.com.test.js +++ b/sites/novacyprus.com/novacyprus.com.test.js @@ -1,48 +1,49 @@ -const { parser, url } = require('./novacyprus.com.config.js') -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('2021-11-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '614', - xmltv_id: 'NovaCinema1.gr' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://www.novacyprus.com/api/v1/tvprogram/from/20211117/to/20211118' - ) -}) - -it('can parse response', () => { - const content = - '{"nodes":[{"datetime":"2021-11-17 06:20:00","day":"Wednesday","numDay":17,"numMonth":11,"month":"November","channelName":"Cyprus Novacinema1HD","channelLog":"https://ssl2.novago.gr/EPG/jsp/images/universal/film/logo/20200210/000100/XTV100000762/d6a2f5e0-dbc0-49c7-9843-e3161ca5ae5d.png","cid":"42","ChannelId":"614","startingTime":"06:20","endTime":"08:10","title":"Δεσμοί Αίματος","description":"Θρίλερ Μυστηρίου","duration":"109","slotDuration":"110","bref":"COMMOBLOOX","mediaItems":[{"MediaListTypeId":"6","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_GUIDE_STILL.jpg"},{"MediaListTypeId":"7","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_POSTER_CROSS.jpg"},{"MediaListTypeId":"8","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_ICON_CYP.jpg"},{"MediaListTypeId":"9","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_POSTER_CYP.jpg"},{"MediaListTypeId":"10","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_BACKGROUND_CYP.jpg"}]},{"datetime":"2021-11-17 06:00:00","day":"Wednesday","numDay":17,"numMonth":11,"month":"November","channelName":"Cyprus Novacinema2HD","channelLog":"https://ssl2.novago.gr/EPG/jsp/images/universal/film/logo/20200210/000100/XTV100000763/24e05354-d6ad-4949-bcb3-a81d1c1d2cba.png","cid":"62","ChannelId":"653","startingTime":"06:00","endTime":"07:40","title":"Ανυπόφοροι Γείτονες","description":"Κωμωδία","duration":"93","slotDuration":"100","bref":"NEIGHBORSX","mediaItems":[{"MediaListTypeId":"7","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/1/312582_NEIGHBORSX_POSTER_CROSS.jpg"},{"MediaListTypeId":"8","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/1/312582_NEIGHBORSX_ICON_CYP.jpg"},{"MediaListTypeId":"9","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/1/312582_NEIGHBORSX_POSTER_CYP.jpg"},{"MediaListTypeId":"10","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/1/312582_NEIGHBORSX_BACKGROUND_CYP.jpg"}]}]}' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-17T04:20:00.000Z', - stop: '2021-11-17T06:10:00.000Z', - title: 'Δεσμοί Αίματος', - description: 'Θρίλερ Μυστηρίου', - image: - 'http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_GUIDE_STILL.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"nodes":[],"total":0,"pages":0}' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./novacyprus.com.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('2021-11-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '614', + xmltv_id: 'NovaCinema1.gr' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://www.novacyprus.com/api/v1/tvprogram/from/20211117/to/20211118' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-17T04:20:00.000Z', + stop: '2021-11-17T06:10:00.000Z', + title: 'Δεσμοί Αίματος', + description: 'Θρίλερ Μυστηρίου', + image: + 'http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_GUIDE_STILL.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/novasports.gr/novasports.gr.config.js b/sites/novasports.gr/novasports.gr.config.js index 5e452c57..0721f670 100644 --- a/sites/novasports.gr/novasports.gr.config.js +++ b/sites/novasports.gr/novasports.gr.config.js @@ -1,87 +1,87 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'novasports.gr', - days: 2, - url: function ({ date }) { - return `https://www.novasports.gr/wp-admin/admin-ajax.php?action=nova_get_template&template=tv-program/broadcast&dt=${date.format( - 'YYYY-MM-DD' - )}` - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - let stop = start.add(30, 'm') - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - stop = stop.add(1, 'd') - } - prev.stop = start - } - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const html = await axios - .get( - 'https://www.novasports.gr/wp-admin/admin-ajax.php?action=nova_get_template&template=tv-program/broadcast&dt=2022-10-29' - ) - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(html) - const items = $( - '#mc-broadcast-content:nth-child(2) > div > #channelist-slider > div.channelist-wrapper.slider-wrapper.content > div > div' - ).toArray() - return items.map(item => { - const name = $(item).find('.channel').text().trim() - - return { - lang: 'el', - site_id: name, - name - } - }) - } -} - -function parseTitle($item) { - return $item('.title').text().trim() -} - -function parseDescription($item) { - return $item('.subtitle').text().trim() -} - -function parseStart($item, date) { - const timeString = $item('.time').text().trim() - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${timeString}`, 'YYYY-MM-DD HH:mm', 'Europe/Athens') -} - -function parseItems(content, channel) { - const $ = cheerio.load(content) - const $channelElement = $( - `#mc-broadcast-content:nth-child(2) > div > #channelist-slider > div.channelist-wrapper.slider-wrapper.content > div > div:contains("${channel.site_id}")` - ) - - return $channelElement.find('.channel-program > div').toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'novasports.gr', + days: 2, + url: function ({ date }) { + return `https://www.novasports.gr/wp-admin/admin-ajax.php?action=nova_get_template&template=tv-program/broadcast&dt=${date.format( + 'YYYY-MM-DD' + )}` + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + let stop = start.add(30, 'm') + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + stop = stop.add(1, 'd') + } + prev.stop = start + } + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const html = await axios + .get( + 'https://www.novasports.gr/wp-admin/admin-ajax.php?action=nova_get_template&template=tv-program/broadcast&dt=2022-10-29' + ) + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(html) + const items = $( + '#mc-broadcast-content:nth-child(2) > div > #channelist-slider > div.channelist-wrapper.slider-wrapper.content > div > div' + ).toArray() + return items.map(item => { + const name = $(item).find('.channel').text().trim() + + return { + lang: 'el', + site_id: name, + name + } + }) + } +} + +function parseTitle($item) { + return $item('.title').text().trim() +} + +function parseDescription($item) { + return $item('.subtitle').text().trim() +} + +function parseStart($item, date) { + const timeString = $item('.time').text().trim() + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${timeString}`, 'YYYY-MM-DD HH:mm', 'Europe/Athens') +} + +function parseItems(content, channel) { + const $ = cheerio.load(content) + const $channelElement = $( + `#mc-broadcast-content:nth-child(2) > div > #channelist-slider > div.channelist-wrapper.slider-wrapper.content > div > div:contains("${channel.site_id}")` + ) + + return $channelElement.find('.channel-program > div').toArray() +} diff --git a/sites/novasports.gr/novasports.gr.test.js b/sites/novasports.gr/novasports.gr.test.js index 21766fd2..4b936222 100644 --- a/sites/novasports.gr/novasports.gr.test.js +++ b/sites/novasports.gr/novasports.gr.test.js @@ -1,46 +1,46 @@ -const { parser, url } = require('./novasports.gr.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('2022-10-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'Novasports Premier League', - xmltv_id: 'NovasportsPremierLeague.gr' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://www.novasports.gr/wp-admin/admin-ajax.php?action=nova_get_template&template=tv-program/broadcast&dt=2022-10-29' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-29T07:00:00.000Z', - stop: '2022-10-29T07:30:00.000Z', - title: 'Classic Match', - description: 'Τσέλσι - Μάντσεστερ Γ. (1999/00)' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')), - channel, - date - }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./novasports.gr.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('2022-10-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'Novasports Premier League', + xmltv_id: 'NovasportsPremierLeague.gr' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://www.novasports.gr/wp-admin/admin-ajax.php?action=nova_get_template&template=tv-program/broadcast&dt=2022-10-29' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-29T07:00:00.000Z', + stop: '2022-10-29T07:30:00.000Z', + title: 'Classic Match', + description: 'Τσέλσι - Μάντσεστερ Γ. (1999/00)' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')), + channel, + date + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/nowplayer.now.com/__data__/content.json b/sites/nowplayer.now.com/__data__/content.json new file mode 100644 index 00000000..eafb2020 --- /dev/null +++ b/sites/nowplayer.now.com/__data__/content.json @@ -0,0 +1 @@ +[[{"key":"key_202111174524739","vimProgramId":"202111174524739","name":"ViuTVsix Station Closing","start":1637690400000,"end":1637715600000,"date":"20211124","startTime":"02:00AM","endTime":"09:00AM","duration":420,"recordable":false,"restartTv":false,"npvrProg":false,"npvrStartTime":0,"npvrEndTime":0,"cid":"viutvsix station closing","cc":"","isInWatchlist":false}]] \ No newline at end of file diff --git a/sites/nowplayer.now.com/nowplayer.now.com.config.js b/sites/nowplayer.now.com/nowplayer.now.com.config.js index 14697d29..126b040b 100644 --- a/sites/nowplayer.now.com/nowplayer.now.com.config.js +++ b/sites/nowplayer.now.com/nowplayer.now.com.config.js @@ -1,70 +1,70 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'nowplayer.now.com', - days: 2, - url: function ({ channel, date }) { - const diff = date.diff(dayjs.utc().startOf('d'), 'd') + 1 - - return `https://nowplayer.now.com/tvguide/epglist?channelIdList[]=${channel.site_id}&day=${diff}` - }, - request: { - headers({ channel }) { - return { - Cookie: `LANG=${channel.lang}; Expires=null; Path=/; Domain=nowplayer.now.com` - } - } - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.name, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels({ lang }) { - const html = await axios - .get('https://nowplayer.now.com/channels', { headers: { Accept: 'text/html' } }) - .then(r => r.data) - .catch(console.log) - - let channels = [] - - const $ = cheerio.load(html) - $('body > div.container > .tv-guide-s-g > div > div').each((i, el) => { - channels.push({ - lang, - site_id: $(el).find('.guide-g-play > p.channel').text().replace('CH', ''), - name: $(el).find('.thumbnail > a > span.image > p').text() - }) - }) - - return channels - } -} - -function parseStart(item) { - return dayjs(item.start) -} - -function parseStop(item) { - return dayjs(item.end) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data)) return [] - - return Array.isArray(data[0]) ? data[0] : [] -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'nowplayer.now.com', + days: 2, + url: function ({ channel, date }) { + const diff = date.diff(dayjs.utc().startOf('d'), 'd') + 1 + + return `https://nowplayer.now.com/tvguide/epglist?channelIdList[]=${channel.site_id}&day=${diff}` + }, + request: { + headers({ channel }) { + return { + Cookie: `LANG=${channel.lang}; Expires=null; Path=/; Domain=nowplayer.now.com` + } + } + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.name, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels({ lang }) { + const html = await axios + .get('https://nowplayer.now.com/channels', { headers: { Accept: 'text/html' } }) + .then(r => r.data) + .catch(console.log) + + let channels = [] + + const $ = cheerio.load(html) + $('body > div.container > .tv-guide-s-g > div > div').each((i, el) => { + channels.push({ + lang, + site_id: $(el).find('.guide-g-play > p.channel').text().replace('CH', ''), + name: $(el).find('.thumbnail > a > span.image > p').text() + }) + }) + + return channels + } +} + +function parseStart(item) { + return dayjs(item.start) +} + +function parseStop(item) { + return dayjs(item.end) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data)) return [] + + return Array.isArray(data[0]) ? data[0] : [] +} diff --git a/sites/nowplayer.now.com/nowplayer.now.com.test.js b/sites/nowplayer.now.com/nowplayer.now.com.test.js index 35d0528a..0ebc99b3 100644 --- a/sites/nowplayer.now.com/nowplayer.now.com.test.js +++ b/sites/nowplayer.now.com/nowplayer.now.com.test.js @@ -1,57 +1,58 @@ -const { parser, url, request } = require('./nowplayer.now.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const channel = { - lang: 'zh', - site_id: '096', - xmltv_id: 'ViuTVsix.hk' -} - -it('can generate valid url for today', () => { - const date = dayjs.utc().startOf('d') - expect(url({ channel, date })).toBe( - 'https://nowplayer.now.com/tvguide/epglist?channelIdList[]=096&day=1' - ) -}) - -it('can generate valid url for tomorrow', () => { - const date = dayjs.utc().startOf('d').add(1, 'd') - expect(url({ channel, date })).toBe( - 'https://nowplayer.now.com/tvguide/epglist?channelIdList[]=096&day=2' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers({ channel })).toMatchObject({ - Cookie: 'LANG=zh; Expires=null; Path=/; Domain=nowplayer.now.com' - }) -}) - -it('can parse response', () => { - const content = - '[[{"key":"key_202111174524739","vimProgramId":"202111174524739","name":"ViuTVsix Station Closing","start":1637690400000,"end":1637715600000,"date":"20211124","startTime":"02:00AM","endTime":"09:00AM","duration":420,"recordable":false,"restartTv":false,"npvrProg":false,"npvrStartTime":0,"npvrEndTime":0,"cid":"viutvsix station closing","cc":"","isInWatchlist":false}]]' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-23T18:00:00.000Z', - stop: '2021-11-24T01:00:00.000Z', - title: 'ViuTVsix Station Closing' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '[[]]' - }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./nowplayer.now.com.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 channel = { + lang: 'zh', + site_id: '096', + xmltv_id: 'ViuTVsix.hk' +} + +it('can generate valid url for today', () => { + const date = dayjs.utc().startOf('d') + expect(url({ channel, date })).toBe( + 'https://nowplayer.now.com/tvguide/epglist?channelIdList[]=096&day=1' + ) +}) + +it('can generate valid url for tomorrow', () => { + const date = dayjs.utc().startOf('d').add(1, 'd') + expect(url({ channel, date })).toBe( + 'https://nowplayer.now.com/tvguide/epglist?channelIdList[]=096&day=2' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers({ channel })).toMatchObject({ + Cookie: 'LANG=zh; Expires=null; Path=/; Domain=nowplayer.now.com' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-23T18:00:00.000Z', + stop: '2021-11-24T01:00:00.000Z', + title: 'ViuTVsix Station Closing' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '[[]]' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/nuevosiglo.com.uy/nuevosiglo.com.uy.config.js b/sites/nuevosiglo.com.uy/nuevosiglo.com.uy.config.js index 6dd2c7e1..162ec21a 100644 --- a/sites/nuevosiglo.com.uy/nuevosiglo.com.uy.config.js +++ b/sites/nuevosiglo.com.uy/nuevosiglo.com.uy.config.js @@ -1,106 +1,106 @@ -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') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -const API_ENDPOINT = 'https://www.nuevosiglo.com.uy/programacion/getGrilla' - -module.exports = { - site: 'nuevosiglo.com.uy', - days: 2, - url({ date }) { - return `${API_ENDPOINT}?fecha=${date.format('YYYY/MM/DD')}` - }, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - async parser({ content, channel }) { - const programs = [] - const items = parseItems(content, channel) - for (let item of items) { - const $item = cheerio.load(item) - const programId = parseProgramId($item) - const details = await loadProgramDetails(programId) - if (!details) continue - programs.push({ - title: details.main_title, - description: details.short_argument, - image: parseImage(details), - actors: parseActors(details), - rating: parseRating(details), - date: details.year, - start: parseStart(details), - stop: parseStop(details) - }) - } - - return programs - }, - async channels() { - const data = await axios - .get(`${API_ENDPOINT}?fecha=${dayjs().format('YYYY/MM/DD')}`) - .then(r => r.data) - .catch(console.error) - const $ = cheerio.load(data) - - return $('img') - .map(function () { - return { - lang: 'es', - site_id: $(this).attr('alt').replace(/&/gi, '&'), - name: $(this).attr('alt') - } - }) - .get() - } -} - -function parseProgramId($item) { - return $item('*').data('schedule') -} - -function loadProgramDetails(programId) { - return axios - .get(`https://www.nuevosiglo.com.uy/Programacion/getScheduleXId/${programId}`) - .then(r => r.data) - .catch(console.log) -} - -function parseRating(details) { - return details.parental_rating - ? { - system: 'MPAA', - value: details.parental_rating - } - : null -} - -function parseActors(details) { - return details.actors.split(', ') -} - -function parseImage(details) { - return details.image ? `https://img-ns.s3.amazonaws.com/grid_data/${details.image}` : null -} - -function parseStart(details) { - return dayjs.tz(details.time_start, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo') -} - -function parseStop(details) { - return dayjs.tz(details.time_end, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo') -} - -function parseItems(content, channel) { - const $ = cheerio.load(content) - - return $(`img[alt="${channel.site_id}"]`).first().nextUntil('img').toArray() -} +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') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +const API_ENDPOINT = 'https://www.nuevosiglo.com.uy/programacion/getGrilla' + +module.exports = { + site: 'nuevosiglo.com.uy', + days: 2, + url({ date }) { + return `${API_ENDPOINT}?fecha=${date.format('YYYY/MM/DD')}` + }, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + async parser({ content, channel }) { + const programs = [] + const items = parseItems(content, channel) + for (let item of items) { + const $item = cheerio.load(item) + const programId = parseProgramId($item) + const details = await loadProgramDetails(programId) + if (!details) continue + programs.push({ + title: details.main_title, + description: details.short_argument, + image: parseImage(details), + actors: parseActors(details), + rating: parseRating(details), + date: details.year, + start: parseStart(details), + stop: parseStop(details) + }) + } + + return programs + }, + async channels() { + const data = await axios + .get(`${API_ENDPOINT}?fecha=${dayjs().format('YYYY/MM/DD')}`) + .then(r => r.data) + .catch(console.error) + const $ = cheerio.load(data) + + return $('img') + .map(function () { + return { + lang: 'es', + site_id: $(this).attr('alt').replace(/&/gi, '&'), + name: $(this).attr('alt') + } + }) + .get() + } +} + +function parseProgramId($item) { + return $item('*').data('schedule') +} + +function loadProgramDetails(programId) { + return axios + .get(`https://www.nuevosiglo.com.uy/Programacion/getScheduleXId/${programId}`) + .then(r => r.data) + .catch(console.log) +} + +function parseRating(details) { + return details.parental_rating + ? { + system: 'MPAA', + value: details.parental_rating + } + : null +} + +function parseActors(details) { + return details.actors.split(', ') +} + +function parseImage(details) { + return details.image ? `https://img-ns.s3.amazonaws.com/grid_data/${details.image}` : null +} + +function parseStart(details) { + return dayjs.tz(details.time_start, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo') +} + +function parseStop(details) { + return dayjs.tz(details.time_end, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo') +} + +function parseItems(content, channel) { + const $ = cheerio.load(content) + + return $(`img[alt="${channel.site_id}"]`).first().nextUntil('img').toArray() +} diff --git a/sites/nuevosiglo.com.uy/nuevosiglo.com.uy.test.js b/sites/nuevosiglo.com.uy/nuevosiglo.com.uy.test.js index fbd2bf02..bb4721af 100644 --- a/sites/nuevosiglo.com.uy/nuevosiglo.com.uy.test.js +++ b/sites/nuevosiglo.com.uy/nuevosiglo.com.uy.test.js @@ -1,97 +1,97 @@ -const { parser, url } = require('./nuevosiglo.com.uy.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('2023-02-10', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'HBO', - xmltv_id: 'HBOLatinAmerica.us' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://www.nuevosiglo.com.uy/programacion/getGrilla?fecha=2023/02/10' - ) -}) - -it('can parse response', async () => { - axios.get.mockImplementation(url => { - if (url === 'https://www.nuevosiglo.com.uy/Programacion/getScheduleXId/133769227') { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) - }) - } else if (url === 'https://www.nuevosiglo.com.uy/Programacion/getScheduleXId/133769239') { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = await parser({ content, channel }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-02-10T01:11:00.000Z', - stop: '2023-02-10T03:46:00.000Z', - title: 'Jurassic World: Dominion', - description: - 'Años después de la destrucción de Isla Nublar, los dinosaurios viven y cazan junto a los humanos. Este equilibrio determinará, si los humanos seguirán siendo los depredadores máximos en un planeta que comparten con las criaturas temibles.', - image: 'https://img-ns.s3.amazonaws.com/grid_data/23354476.jpg', - date: '2022', - rating: { - system: 'MPAA', - value: 'PG-13' - }, - actors: ['Jeff Goldblum', 'Sam Neill', 'Bryce Dallas Howard'] - }) - - expect(results[1]).toMatchObject({ - start: '2023-02-11T02:06:00.000Z', - stop: '2023-02-11T04:16:00.000Z', - title: 'Black Adam', - description: - 'Black Adam es liberado de su tumba casi cinco mil años después de haber sido encarcelado y recibir sus poderes de los antiguos dioses. Ahora está listo para desatar su forma única de justicia en el mundo.', - image: 'https://img-ns.s3.amazonaws.com/grid_data/24638423.jpg', - date: '2022', - rating: { - system: 'MPAA', - value: 'PG-13' - }, - actors: [ - 'Aldis Hodge', - 'Dwayne Johnson', - 'Noah Centineo', - 'Sarah Shahi', - 'Marwan Kenzari', - 'Pierce Brosnan', - 'Quintessa Swindell', - 'Mohammed Amer', - 'Bodhi Sabongui', - 'James Cusati-Moyer' - ] - }) -}) - -it('can handle empty guide', async () => { - const results = await parser({ - channel, - content: '' - }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./nuevosiglo.com.uy.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('2023-02-10', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'HBO', + xmltv_id: 'HBOLatinAmerica.us' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://www.nuevosiglo.com.uy/programacion/getGrilla?fecha=2023/02/10' + ) +}) + +it('can parse response', async () => { + axios.get.mockImplementation(url => { + if (url === 'https://www.nuevosiglo.com.uy/Programacion/getScheduleXId/133769227') { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) + }) + } else if (url === 'https://www.nuevosiglo.com.uy/Programacion/getScheduleXId/133769239') { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = await parser({ content, channel }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-02-10T01:11:00.000Z', + stop: '2023-02-10T03:46:00.000Z', + title: 'Jurassic World: Dominion', + description: + 'Años después de la destrucción de Isla Nublar, los dinosaurios viven y cazan junto a los humanos. Este equilibrio determinará, si los humanos seguirán siendo los depredadores máximos en un planeta que comparten con las criaturas temibles.', + image: 'https://img-ns.s3.amazonaws.com/grid_data/23354476.jpg', + date: '2022', + rating: { + system: 'MPAA', + value: 'PG-13' + }, + actors: ['Jeff Goldblum', 'Sam Neill', 'Bryce Dallas Howard'] + }) + + expect(results[1]).toMatchObject({ + start: '2023-02-11T02:06:00.000Z', + stop: '2023-02-11T04:16:00.000Z', + title: 'Black Adam', + description: + 'Black Adam es liberado de su tumba casi cinco mil años después de haber sido encarcelado y recibir sus poderes de los antiguos dioses. Ahora está listo para desatar su forma única de justicia en el mundo.', + image: 'https://img-ns.s3.amazonaws.com/grid_data/24638423.jpg', + date: '2022', + rating: { + system: 'MPAA', + value: 'PG-13' + }, + actors: [ + 'Aldis Hodge', + 'Dwayne Johnson', + 'Noah Centineo', + 'Sarah Shahi', + 'Marwan Kenzari', + 'Pierce Brosnan', + 'Quintessa Swindell', + 'Mohammed Amer', + 'Bodhi Sabongui', + 'James Cusati-Moyer' + ] + }) +}) + +it('can handle empty guide', async () => { + const results = await parser({ + channel, + content: '' + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/nzxmltv.com/nzxmltv.com.config.js b/sites/nzxmltv.com/nzxmltv.com.config.js index 43029c5b..488cdb86 100644 --- a/sites/nzxmltv.com/nzxmltv.com.config.js +++ b/sites/nzxmltv.com/nzxmltv.com.config.js @@ -1,81 +1,81 @@ -const parser = require('epg-parser') - -module.exports = { - site: 'nzxmltv.com', - days: 2, - request: { - cache: { - ttl: 3600000 // 1 hour - }, - maxContentLength: 104857600 // 100 MB - }, - url({ channel }) { - const [path] = channel.site_id.split('#') - - return `https://nzxmltv.com/${path}.xml` - }, - parser({ content, channel, date }) { - const programs = [] - parseItems(content, channel, date).forEach(item => { - const program = { - title: item.title?.[0]?.value, - description: item.desc?.[0]?.value, - icon: item.icon?.[0]?.src, - start: item.start, - stop: item.stop - } - if (item.episodeNum) { - item.episodeNum.forEach(ep => { - if (ep.system === 'xmltv_ns') { - const [season, episode] = ep.value.split('.') - program.season = parseInt(season) + 1 - program.episode = parseInt(episode) + 1 - return true - } - }) - } - programs.push(program) - }) - - return programs - }, - async channels({ provider }) { - const axios = require('axios') - const cheerio = require('cheerio') - - const providers = { - freeview: 'xmltv/guide', - sky: 'sky/guide', - redbull: 'iptv/redbull', - pluto: 'iptv/plutotv' - } - - const channels = [] - const path = providers[provider] - const xml = await axios - .get(`https://nzxmltv.com/${path}.xml`) - .then(r => r.data) - .catch(console.error) - - const $ = cheerio.load(xml) - $('tv channel').each((i, el) => { - const disp = $(el).find('display-name') - const channelId = $(el).attr('id') - - channels.push({ - lang: disp.attr('lang').substr(0, 2), - site_id: `${path}#${channelId}`, - name: disp.text().trim() - }) - }) - - return channels - } -} - -function parseItems(content, channel, date) { - const { programs } = parser.parse(content) - const [, channelId] = channel.site_id.split('#') - - return programs.filter(p => p.channel === channelId && date.isSame(p.start, 'day')) -} +const parser = require('epg-parser') + +module.exports = { + site: 'nzxmltv.com', + days: 2, + request: { + cache: { + ttl: 3600000 // 1 hour + }, + maxContentLength: 104857600 // 100 MB + }, + url({ channel }) { + const [path] = channel.site_id.split('#') + + return `https://nzxmltv.com/${path}.xml` + }, + parser({ content, channel, date }) { + const programs = [] + parseItems(content, channel, date).forEach(item => { + const program = { + title: item.title?.[0]?.value, + description: item.desc?.[0]?.value, + icon: item.icon?.[0]?.src, + start: item.start, + stop: item.stop + } + if (item.episodeNum) { + item.episodeNum.forEach(ep => { + if (ep.system === 'xmltv_ns') { + const [season, episode] = ep.value.split('.') + program.season = parseInt(season) + 1 + program.episode = parseInt(episode) + 1 + return true + } + }) + } + programs.push(program) + }) + + return programs + }, + async channels({ provider }) { + const axios = require('axios') + const cheerio = require('cheerio') + + const providers = { + freeview: 'xmltv/guide', + sky: 'sky/guide', + redbull: 'iptv/redbull', + pluto: 'iptv/plutotv' + } + + const channels = [] + const path = providers[provider] + const xml = await axios + .get(`https://nzxmltv.com/${path}.xml`) + .then(r => r.data) + .catch(console.error) + + const $ = cheerio.load(xml) + $('tv channel').each((i, el) => { + const disp = $(el).find('display-name') + const channelId = $(el).attr('id') + + channels.push({ + lang: disp.attr('lang').substr(0, 2), + site_id: `${path}#${channelId}`, + name: disp.text().trim() + }) + }) + + return channels + } +} + +function parseItems(content, channel, date) { + const { programs } = parser.parse(content) + const [, channelId] = channel.site_id.split('#') + + return programs.filter(p => p.channel === channelId && date.isSame(p.start, 'day')) +} diff --git a/sites/nzxmltv.com/nzxmltv.com.test.js b/sites/nzxmltv.com/nzxmltv.com.test.js index e6ef97a8..f1af8d89 100644 --- a/sites/nzxmltv.com/nzxmltv.com.test.js +++ b/sites/nzxmltv.com/nzxmltv.com.test.js @@ -1,40 +1,40 @@ -const { parser, url } = require('./nzxmltv.com.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('2023-11-21').startOf('d') -const channel = { - site_id: 'xmltv/guide#1', - xmltv_id: 'TVNZ1.nz' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://nzxmltv.com/xmltv/guide.xml') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml')) - const results = parser({ content, channel, date }) - - expect(results[0]).toMatchObject({ - start: '2023-11-21T10:30:00.000Z', - stop: '2023-11-21T11:25:00.000Z', - title: 'Sunday', - description: - 'On Sunday, an unmissable show with stories about divorce, weight loss, and the incomprehensible devastation of Gaza.', - season: 2023, - episode: 37, - icon: 'https://www.thetvdb.com/banners/posters/5dbebff2986f2.jpg' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ content: '', channel, date }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./nzxmltv.com.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('2023-11-21').startOf('d') +const channel = { + site_id: 'xmltv/guide#1', + xmltv_id: 'TVNZ1.nz' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://nzxmltv.com/xmltv/guide.xml') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml')) + const results = parser({ content, channel, date }) + + expect(results[0]).toMatchObject({ + start: '2023-11-21T10:30:00.000Z', + stop: '2023-11-21T11:25:00.000Z', + title: 'Sunday', + description: + 'On Sunday, an unmissable show with stories about divorce, weight loss, and the incomprehensible devastation of Gaza.', + season: 2023, + episode: 37, + icon: 'https://www.thetvdb.com/banners/posters/5dbebff2986f2.jpg' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ content: '', channel, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/ontvtonight.com/__data__/content.html b/sites/ontvtonight.com/__data__/content.html new file mode 100644 index 00000000..00f26fb4 --- /dev/null +++ b/sites/ontvtonight.com/__data__/content.html @@ -0,0 +1 @@ +
    7TWO
    12:10 am
    What A Carry On
    12:50 am
    Bones
    The Devil In The Details
    10:50 pm
    Inspector Morse: The Remorseful Day
    \ No newline at end of file diff --git a/sites/ontvtonight.com/__data__/no_content.html b/sites/ontvtonight.com/__data__/no_content.html new file mode 100644 index 00000000..6fedfd4c --- /dev/null +++ b/sites/ontvtonight.com/__data__/no_content.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sites/ontvtonight.com/ontvtonight.com.config.js b/sites/ontvtonight.com/ontvtonight.com.config.js index 20b6efd0..e4003ad1 100644 --- a/sites/ontvtonight.com/ontvtonight.com.config.js +++ b/sites/ontvtonight.com/ontvtonight.com.config.js @@ -1,179 +1,178 @@ -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: 'ontvtonight.com', - days: 2, - url: function ({ date, channel }) { - const [region, id] = channel.site_id.split('#') - let url = 'https://www.ontvtonight.com' - if (region && region !== 'us') url += `/${region}` - url += `/guide/listings/channel/${id}.html?dt=${date.format('YYYY-MM-DD')}` - - return url - }, - parser: function ({ content, date, channel }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date, channel) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(1, 'h') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - start, - stop - }) - }) - - return programs - }, - async channels({ country }) { - const axios = require('axios') - const _ = require('lodash') - - const providers = { - au: ['o', 'a'], - ca: [ - 'Y464014423', - '-464014503', - '-464014594', - '-464014738', - 'X3153330286', - 'X464014503', - 'X464013696', - 'X464014594', - 'X464014738', - 'X464014470', - 'X464013514', - 'X1210684931', - 'T3153330286', - 'T464014503', - 'T1810267316', - 'T1210684931' - ], - us: [ - 'Y341768590', - 'Y1693286984', - 'Y8833268284', - '-341767428', - '-341769166', - '-341769884', - '-3679985536', - '-341766967', - 'X4100694897', - 'X341767428', - 'X341768182', - 'X341767434', - 'X341768272', - 'X341769884', - 'X3679985536', - 'X3679984937', - 'X341764975', - 'X3679985052', - 'X341766967', - 'K4805071612', - 'K5039655414' - ] - } - const regions = { - au: [ - 1, 2, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 17, 18, 29, 28, 27, 26, 25, 23, 22, - 21, 20, 19, 24, 30, 31, 32, 33, 34, 35, 36, 39, 38, 37, 40, 41, 42, 43, 44, 45, 46, 47, 48, - 49, 50, 51, 52, 53 - ], - ca: [null], - us: [null] - } - const zipcodes = { - au: [null], - ca: ['M5G1P5', 'H3B1X8', 'V6Z2H7', 'T2P3E6', 'T5J2Z2', 'K1P1B1'], - us: [10199, 90052, 60607, 77201, 85026, 19104, 78284, 92199, 75260] - } - - const channels = [] - for (let provider of providers[country]) { - for (let zipcode of zipcodes[country]) { - for (let region of regions[country]) { - let url = 'https://www.ontvtonight.com' - if (country === 'us') url += '/guide/schedule' - else url += `/${country}/guide/schedule` - const data = await axios - .post(url, null, { - params: { - provider, - region, - zipcode, - TVperiod: 'Night', - date: dayjs().format('YYYY-MM-DD'), - st: 0, - is_mobile: 1 - } - }) - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(data) - $('.channelname').each((i, el) => { - let name = $(el).find('center > a:eq(1)').text() - name = name.replace(/--/gi, '-') - const url = $(el).find('center > a:eq(1)').attr('href') - if (!url) return - const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/) - - channels.push({ - lang: 'en', - name, - site_id: `${country}#${number}/${slug}` - }) - }) - } - } - } - - return _.uniqBy(channels, 'site_id') - } -} - -function parseStart($item, date, channel) { - const timezones = { - au: 'Australia/Sydney', - ca: 'America/Toronto', - us: 'America/New_York' - } - const [region] = channel.site_id.split('#') - const timeString = $item('td:nth-child(1) > h5').text().trim() - const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` - - return dayjs.tz(dateString, 'YYYY-MM-DD H:mm a', timezones[region]) -} - -function parseTitle($item) { - return $item('td:nth-child(2) > h5').text().trim() -} - -function parseDescription($item) { - return $item('td:nth-child(2) > h6').text().trim() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#content > div > div > div > table > tbody > tr').toArray() -} +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 uniqBy = require('lodash.uniqby') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'ontvtonight.com', + days: 2, + url: function ({ date, channel }) { + const [region, id] = channel.site_id.split('#') + let url = 'https://www.ontvtonight.com' + if (region && region !== 'us') url += `/${region}` + url += `/guide/listings/channel/${id}.html?dt=${date.format('YYYY-MM-DD')}` + + return url + }, + parser: function ({ content, date, channel }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date, channel) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(1, 'h') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + start, + stop + }) + }) + + return programs + }, + async channels({ country }) { + const providers = { + au: ['o', 'a'], + ca: [ + 'Y464014423', + '-464014503', + '-464014594', + '-464014738', + 'X3153330286', + 'X464014503', + 'X464013696', + 'X464014594', + 'X464014738', + 'X464014470', + 'X464013514', + 'X1210684931', + 'T3153330286', + 'T464014503', + 'T1810267316', + 'T1210684931' + ], + us: [ + 'Y341768590', + 'Y1693286984', + 'Y8833268284', + '-341767428', + '-341769166', + '-341769884', + '-3679985536', + '-341766967', + 'X4100694897', + 'X341767428', + 'X341768182', + 'X341767434', + 'X341768272', + 'X341769884', + 'X3679985536', + 'X3679984937', + 'X341764975', + 'X3679985052', + 'X341766967', + 'K4805071612', + 'K5039655414' + ] + } + const regions = { + au: [ + 1, 2, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 17, 18, 29, 28, 27, 26, 25, 23, 22, + 21, 20, 19, 24, 30, 31, 32, 33, 34, 35, 36, 39, 38, 37, 40, 41, 42, 43, 44, 45, 46, 47, 48, + 49, 50, 51, 52, 53 + ], + ca: [null], + us: [null] + } + const zipcodes = { + au: [null], + ca: ['M5G1P5', 'H3B1X8', 'V6Z2H7', 'T2P3E6', 'T5J2Z2', 'K1P1B1'], + us: [10199, 90052, 60607, 77201, 85026, 19104, 78284, 92199, 75260] + } + + const channels = [] + for (let provider of providers[country]) { + for (let zipcode of zipcodes[country]) { + for (let region of regions[country]) { + let url = 'https://www.ontvtonight.com' + if (country === 'us') url += '/guide/schedule' + else url += `/${country}/guide/schedule` + const data = await axios + .post(url, null, { + params: { + provider, + region, + zipcode, + TVperiod: 'Night', + date: dayjs().format('YYYY-MM-DD'), + st: 0, + is_mobile: 1 + } + }) + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(data) + $('.channelname').each((i, el) => { + let name = $(el).find('center > a:eq(1)').text() + name = name.replace(/--/gi, '-') + const url = $(el).find('center > a:eq(1)').attr('href') + if (!url) return + const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/) + + channels.push({ + lang: 'en', + name, + site_id: `${country}#${number}/${slug}` + }) + }) + } + } + } + + return uniqBy(channels, 'site_id') + } +} + +function parseStart($item, date, channel) { + const timezones = { + au: 'Australia/Sydney', + ca: 'America/Toronto', + us: 'America/New_York' + } + const [region] = channel.site_id.split('#') + const timeString = $item('td:nth-child(1) > h5').text().trim() + const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` + + return dayjs.tz(dateString, 'YYYY-MM-DD H:mm a', timezones[region]) +} + +function parseTitle($item) { + return $item('td:nth-child(2) > h5').text().trim() +} + +function parseDescription($item) { + return $item('td:nth-child(2) > h6').text().trim() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('#content > div > div > div > table > tbody > tr').toArray() +} diff --git a/sites/ontvtonight.com/ontvtonight.com.test.js b/sites/ontvtonight.com/ontvtonight.com.test.js index 8ad6b96b..a40b980e 100644 --- a/sites/ontvtonight.com/ontvtonight.com.test.js +++ b/sites/ontvtonight.com/ontvtonight.com.test.js @@ -1,56 +1,57 @@ -const { parser, url } = require('./ontvtonight.com.config.js') -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('2021-11-25', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'au#1692/7two', - xmltv_id: '7two.au' -} -const content = - '
    7TWO
    12:10 am
    What A Carry On
    12:50 am
    Bones
    The Devil In The Details
    10:50 pm
    Inspector Morse: The Remorseful Day
    ' - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.ontvtonight.com/au/guide/listings/channel/1692/7two.html?dt=2021-11-25' - ) -}) - -it('can parse response', () => { - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-24T13:10:00.000Z', - stop: '2021-11-24T13:50:00.000Z', - title: 'What A Carry On' - }, - { - start: '2021-11-24T13:50:00.000Z', - stop: '2021-11-25T11:50:00.000Z', - title: 'Bones', - description: 'The Devil In The Details' - }, - { - start: '2021-11-25T11:50:00.000Z', - stop: '2021-11-25T12:50:00.000Z', - title: 'Inspector Morse: The Remorseful Day' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./ontvtonight.com.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('2021-11-25', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'au#1692/7two', + xmltv_id: '7two.au' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.ontvtonight.com/au/guide/listings/channel/1692/7two.html?dt=2021-11-25' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-24T13:10:00.000Z', + stop: '2021-11-24T13:50:00.000Z', + title: 'What A Carry On' + }, + { + start: '2021-11-24T13:50:00.000Z', + stop: '2021-11-25T11:50:00.000Z', + title: 'Bones', + description: 'The Devil In The Details' + }, + { + start: '2021-11-25T11:50:00.000Z', + stop: '2021-11-25T12:50:00.000Z', + title: 'Inspector Morse: The Remorseful Day' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/opto.sic.pt/opto.sic.pt.config.js b/sites/opto.sic.pt/opto.sic.pt.config.js index 8c14ec50..d6b64cf8 100644 --- a/sites/opto.sic.pt/opto.sic.pt.config.js +++ b/sites/opto.sic.pt/opto.sic.pt.config.js @@ -1,53 +1,53 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'opto.sic.pt', - days: 2, - url({ date, channel }) { - const startDate = date.unix() - const endDate = date.add(1, 'd').unix() - - return `https://opto.sic.pt/api/v1/content/epg?startDate=${startDate}&endDate=${endDate}&channels=${channel.site_id}` - }, - parser({ content }) { - let programs = [] - let items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - episode: item.episode_number || null, - season: item.season_number || null, - start: dayjs.unix(item.start_time), - stop: dayjs.unix(item.end_time) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://opto.sic.pt/api/v1/content/channel') - .then(r => r.data) - .catch(console.error) - - return data.map(channel => { - return { - lang: 'pt', - site_id: channel.id, - name: channel.name - } - }) - } -} - -function parseItems(content) { - try { - const data = JSON.parse(content) - if (!Array.isArray(data)) return [] - - return data - } catch { - return [] - } -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'opto.sic.pt', + days: 2, + url({ date, channel }) { + const startDate = date.unix() + const endDate = date.add(1, 'd').unix() + + return `https://opto.sic.pt/api/v1/content/epg?startDate=${startDate}&endDate=${endDate}&channels=${channel.site_id}` + }, + parser({ content }) { + let programs = [] + let items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + episode: item.episode_number || null, + season: item.season_number || null, + start: dayjs.unix(item.start_time), + stop: dayjs.unix(item.end_time) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://opto.sic.pt/api/v1/content/channel') + .then(r => r.data) + .catch(console.error) + + return data.map(channel => { + return { + lang: 'pt', + site_id: channel.id, + name: channel.name + } + }) + } +} + +function parseItems(content) { + try { + const data = JSON.parse(content) + if (!Array.isArray(data)) return [] + + return data + } catch { + return [] + } +} diff --git a/sites/opto.sic.pt/opto.sic.pt.test.js b/sites/opto.sic.pt/opto.sic.pt.test.js index 7d718f5f..c9d696a5 100644 --- a/sites/opto.sic.pt/opto.sic.pt.test.js +++ b/sites/opto.sic.pt/opto.sic.pt.test.js @@ -1,52 +1,52 @@ -const { parser, url } = require('./opto.sic.pt.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-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '38719848-2a57-42e3-8640-63a9aa39f107', - xmltv_id: 'SICNoticias.pt' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://opto.sic.pt/api/v1/content/epg?startDate=1737072000&endDate=1737158400&channels=38719848-2a57-42e3-8640-63a9aa39f107' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') - let results = parser({ content }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(17) - expect(results[0]).toMatchObject({ - start: '2025-01-17T00:00:00.000Z', - stop: '2025-01-17T01:45:00.000Z', - title: 'JORNAL DA MEIA-NOITE', - episode: 16, - season: null - }) - expect(results[16]).toMatchObject({ - start: '2025-01-17T23:00:00.000Z', - stop: '2025-01-18T00:00:00.000Z', - title: 'EXPRESSO DA MEIA-NOITE', - episode: 2, - season: null - }) -}) - -it('can handle empty guide', () => { - const results = parser({ content: '' }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./opto.sic.pt.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-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '38719848-2a57-42e3-8640-63a9aa39f107', + xmltv_id: 'SICNoticias.pt' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://opto.sic.pt/api/v1/content/epg?startDate=1737072000&endDate=1737158400&channels=38719848-2a57-42e3-8640-63a9aa39f107' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') + let results = parser({ content }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(17) + expect(results[0]).toMatchObject({ + start: '2025-01-17T00:00:00.000Z', + stop: '2025-01-17T01:45:00.000Z', + title: 'JORNAL DA MEIA-NOITE', + episode: 16, + season: null + }) + expect(results[16]).toMatchObject({ + start: '2025-01-17T23:00:00.000Z', + stop: '2025-01-18T00:00:00.000Z', + title: 'EXPRESSO DA MEIA-NOITE', + episode: 2, + season: null + }) +}) + +it('can handle empty guide', () => { + const results = parser({ content: '' }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/orangetv.orange.es/orangetv.orange.es.config.js b/sites/orangetv.orange.es/orangetv.orange.es.config.js index 0988fb62..4c3bdc30 100644 --- a/sites/orangetv.orange.es/orangetv.orange.es.config.js +++ b/sites/orangetv.orange.es/orangetv.orange.es.config.js @@ -1,99 +1,99 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const doFetch = require('@ntlab/sfetch') -const debug = require('debug')('site:orangetv.orange.es') - -dayjs.extend(utc) - -doFetch.setDebugger(debug) - -const API_PROGRAM_ENDPOINT = 'https://epg.orangetv.orange.es/epg/Smartphone_Android/1_PRO' -const API_CHANNEL_ENDPOINT = - 'https://pc.orangetv.orange.es/pc/api/rtv/v1/GetChannelList?bouquet_id=1&model_external_id=PC&filter_unsupported_channels=false&client=json' -const API_IMAGE_ENDPOINT = 'https://pc.orangetv.orange.es/pc/api/rtv/v1/images' - -module.exports = { - site: 'orangetv.orange.es', - days: 2, - request: { - cache: { - ttl: 24 * 60 * 60 * 1000 // 1 day - } - }, - url({ date, segment = 1 }) { - return `${API_PROGRAM_ENDPOINT}/${date.format('YYYYMMDD')}_8h_${segment}.json` - }, - async parser({ content, channel, date }) { - const programs = [] - const items = parseItems(content, channel) - if (items.length) { - const queues = [ - module.exports.url({ date, segment: 2 }), - module.exports.url({ date, segment: 3 }) - ] - await doFetch(queues, (url, res) => { - items.push(...parseItems(res, channel)) - }) - programs.push( - ...items.map(item => { - return { - title: item.name, - sub_title: item.seriesName, - description: item.description, - category: parseGenres(item), - season: item.seriesSeason ? parseInt(item.seriesSeason) : null, - episode: item.episodeId ? parseInt(item.episodeId) : null, - icon: parseIcon(item), - start: dayjs.utc(item.startDate), - stop: dayjs.utc(item.endDate) - } - }) - ) - } - - return programs - }, - async channels() { - const axios = require('axios') - const data = await axios - .get(API_CHANNEL_ENDPOINT) - .then(r => r.data) - .catch(console.error) - - return data.response.map(item => { - return { - lang: 'es', - name: item.name, - site_id: item.externalChannelId - } - }) - } -} - -function parseIcon(item) { - if (item.attachments.length) { - const cover = item.attachments.find(i => i.name.match(/cover/i)) - if (cover) { - return `${API_IMAGE_ENDPOINT}${cover.value}` - } - } -} - -function parseGenres(item) { - return item.genres.map(i => i.name) -} - -function parseItems(content, channel) { - const result = [] - const json = - typeof content === 'string' ? JSON.parse(content) : Array.isArray(content) ? content : [] - if (Array.isArray(json)) { - json - .filter(i => i.channelExternalId === channel.site_id) - .forEach(i => { - result.push(...i.programs) - }) - } - - return result -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const doFetch = require('@ntlab/sfetch') +const debug = require('debug')('site:orangetv.orange.es') + +dayjs.extend(utc) + +doFetch.setDebugger(debug) + +const API_PROGRAM_ENDPOINT = 'https://epg.orangetv.orange.es/epg/Smartphone_Android/1_PRO' +const API_CHANNEL_ENDPOINT = + 'https://pc.orangetv.orange.es/pc/api/rtv/v1/GetChannelList?bouquet_id=1&model_external_id=PC&filter_unsupported_channels=false&client=json' +const API_IMAGE_ENDPOINT = 'https://pc.orangetv.orange.es/pc/api/rtv/v1/images' + +module.exports = { + site: 'orangetv.orange.es', + days: 2, + request: { + cache: { + ttl: 24 * 60 * 60 * 1000 // 1 day + } + }, + url({ date, segment = 1 }) { + return `${API_PROGRAM_ENDPOINT}/${date.format('YYYYMMDD')}_8h_${segment}.json` + }, + async parser({ content, channel, date }) { + const programs = [] + const items = parseItems(content, channel) + if (items.length) { + const queues = [ + module.exports.url({ date, segment: 2 }), + module.exports.url({ date, segment: 3 }) + ] + await doFetch(queues, (url, res) => { + items.push(...parseItems(res, channel)) + }) + programs.push( + ...items.map(item => { + return { + title: item.name, + sub_title: item.seriesName, + description: item.description, + category: parseGenres(item), + season: item.seriesSeason ? parseInt(item.seriesSeason) : null, + episode: item.episodeId ? parseInt(item.episodeId) : null, + icon: parseIcon(item), + start: dayjs.utc(item.startDate), + stop: dayjs.utc(item.endDate) + } + }) + ) + } + + return programs + }, + async channels() { + const axios = require('axios') + const data = await axios + .get(API_CHANNEL_ENDPOINT) + .then(r => r.data) + .catch(console.error) + + return data.response.map(item => { + return { + lang: 'es', + name: item.name, + site_id: item.externalChannelId + } + }) + } +} + +function parseIcon(item) { + if (item.attachments.length) { + const cover = item.attachments.find(i => i.name.match(/cover/i)) + if (cover) { + return `${API_IMAGE_ENDPOINT}${cover.value}` + } + } +} + +function parseGenres(item) { + return item.genres.map(i => i.name) +} + +function parseItems(content, channel) { + const result = [] + const json = + typeof content === 'string' ? JSON.parse(content) : Array.isArray(content) ? content : [] + if (Array.isArray(json)) { + json + .filter(i => i.channelExternalId === channel.site_id) + .forEach(i => { + result.push(...i.programs) + }) + } + + return result +} diff --git a/sites/orangetv.orange.es/orangetv.orange.es.test.js b/sites/orangetv.orange.es/orangetv.orange.es.test.js index 7e92021c..981dd827 100644 --- a/sites/orangetv.orange.es/orangetv.orange.es.test.js +++ b/sites/orangetv.orange.es/orangetv.orange.es.test.js @@ -1,85 +1,85 @@ -const { parser, url } = require('./orangetv.orange.es.config.js') -const path = require('path') -const fs = require('fs') -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-01-12', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1010', - xmltv_id: 'La1.es' -} - -axios.get.mockImplementation(url => { - const result = {} - const urls = { - 'https://epg.orangetv.orange.es/epg/Smartphone_Android/1_PRO/20250112_8h_1.json': - 'data1.json', - 'https://epg.orangetv.orange.es/epg/Smartphone_Android/1_PRO/20250112_8h_2.json': - 'data2.json', - 'https://epg.orangetv.orange.es/epg/Smartphone_Android/1_PRO/20250112_8h_3.json': - 'data3.json', - } - if (urls[url] !== undefined) { - result.data = fs.readFileSync(path.join(__dirname, '__data__', urls[url])).toString() - if (!urls[url].startsWith('data1')) { - result.data = JSON.parse(result.data) - } - } - - return Promise.resolve(result) -}) - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://epg.orangetv.orange.es/epg/Smartphone_Android/1_PRO/20250112_8h_1.json' - ) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.join(__dirname, '__data__', 'data1.json')).toString() - const results = (await parser({ content, channel, date })).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(18) - expect(results[0]).toMatchObject({ - start: '2025-01-11T22:55:00.000Z', - stop: '2025-01-12T00:40:00.000Z', - title: 'Una joven prometedora', - description: - 'Cassie tenía un brillante futuro por delante. Sin embargo, un incidente provocó que no pudiese cumplir sus sueños. Con el paso del tiempo, tendrá la oportunidad de enmendar los errores del pasado.', - category: ['Cine', 'Drama', 'Suspense'], - icon: 'https://pc.orangetv.orange.es/pc/api/rtv/v1/images/epg/COVER/COVER_2247567.jpg' - }) - expect(results[17]).toMatchObject({ - start: '2025-01-12T21:05:00.000Z', - stop: '2025-01-12T23:05:00.000Z', - title: 'Bake Off: Famosos al horno - T2, E01: Bake Off: Famosos al horno', - sub_title: 'Bake Off: Famosos al horno', - description: - 'Nervios y emoción en el debut de los 14 pasteleros de la nueva temporada de Bake off Famosos al horno. En el primer programa hornearán unas galletas dedicadas a sus mascotas y una tradicional tarta de queso.', - category: ['Programa', 'Reality'], - icon: 'https://pc.orangetv.orange.es/pc/api/rtv/v1/images/epg/COVER/COVER_3520028.jpg', - season: 2, - episode: 1 - }) -}) - -it('can handle empty guide', async () => { - const result = await parser({ - date, - channel, - content: '{}' - }) - expect(result).toMatchObject({}) -}) +const { parser, url } = require('./orangetv.orange.es.config.js') +const path = require('path') +const fs = require('fs') +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-01-12', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1010', + xmltv_id: 'La1.es' +} + +axios.get.mockImplementation(url => { + const result = {} + const urls = { + 'https://epg.orangetv.orange.es/epg/Smartphone_Android/1_PRO/20250112_8h_1.json': + 'data1.json', + 'https://epg.orangetv.orange.es/epg/Smartphone_Android/1_PRO/20250112_8h_2.json': + 'data2.json', + 'https://epg.orangetv.orange.es/epg/Smartphone_Android/1_PRO/20250112_8h_3.json': + 'data3.json', + } + if (urls[url] !== undefined) { + result.data = fs.readFileSync(path.join(__dirname, '__data__', urls[url])).toString() + if (!urls[url].startsWith('data1')) { + result.data = JSON.parse(result.data) + } + } + + return Promise.resolve(result) +}) + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://epg.orangetv.orange.es/epg/Smartphone_Android/1_PRO/20250112_8h_1.json' + ) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.join(__dirname, '__data__', 'data1.json')).toString() + const results = (await parser({ content, channel, date })).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(18) + expect(results[0]).toMatchObject({ + start: '2025-01-11T22:55:00.000Z', + stop: '2025-01-12T00:40:00.000Z', + title: 'Una joven prometedora', + description: + 'Cassie tenía un brillante futuro por delante. Sin embargo, un incidente provocó que no pudiese cumplir sus sueños. Con el paso del tiempo, tendrá la oportunidad de enmendar los errores del pasado.', + category: ['Cine', 'Drama', 'Suspense'], + icon: 'https://pc.orangetv.orange.es/pc/api/rtv/v1/images/epg/COVER/COVER_2247567.jpg' + }) + expect(results[17]).toMatchObject({ + start: '2025-01-12T21:05:00.000Z', + stop: '2025-01-12T23:05:00.000Z', + title: 'Bake Off: Famosos al horno - T2, E01: Bake Off: Famosos al horno', + sub_title: 'Bake Off: Famosos al horno', + description: + 'Nervios y emoción en el debut de los 14 pasteleros de la nueva temporada de Bake off Famosos al horno. En el primer programa hornearán unas galletas dedicadas a sus mascotas y una tradicional tarta de queso.', + category: ['Programa', 'Reality'], + icon: 'https://pc.orangetv.orange.es/pc/api/rtv/v1/images/epg/COVER/COVER_3520028.jpg', + season: 2, + episode: 1 + }) +}) + +it('can handle empty guide', async () => { + const result = await parser({ + date, + channel, + content: '{}' + }) + expect(result).toMatchObject({}) +}) diff --git a/sites/osn.com/osn.com.config.js b/sites/osn.com/osn.com.config.js index ece2bf72..c2b6bd77 100644 --- a/sites/osn.com/osn.com.config.js +++ b/sites/osn.com/osn.com.config.js @@ -1,73 +1,73 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -const packages = { - 'OSNTV CONNECT': 3720, - 'OSNTV PRIME': 3733, - ALFA: 1281, - 'OSN PINOY PLUS EXTRA': 3519 -} -const country = 'AE' -const tz = 'Asia/Dubai' - -module.exports = { - site: 'osn.com', - days: 2, - url({ channel, date }) { - return `https://www.osn.com/api/TVScheduleWebService.asmx/time?dt=${encodeURIComponent( - date.format('MM/DD/YYYY') - )}&co=${country}&ch=${channel.site_id}&mo=false&hr=0` - }, - request: { - headers({ channel }) { - return { - Referer: `https://www.osn.com/${channel.lang}-${country.toLowerCase()}/watch/tv-schedule` - } - } - }, - parser({ content, channel }) { - const programs = [] - const items = JSON.parse(content) || [] - if (Array.isArray(items)) { - for (const item of items) { - const title = channel.lang === 'ar' ? item.Arab_Title : item.Title - const start = dayjs.tz(item.StartDateTime, 'DD MMM YYYY, HH:mm', tz) - const duration = parseInt(item.TotalDivWidth / 4.8) - const stop = start.add(duration, 'm') - programs.push({ title, start, stop }) - } - } - - return programs - }, - async channels({ lang = 'ar' }) { - const result = {} - const axios = require('axios') - for (const pkg of Object.values(packages)) { - const channels = await axios - .get( - `https://www.osn.com/api/tvchannels.ashx?culture=en-US&packageId=${pkg}&country=${country}` - ) - .then(response => response.data) - .catch(console.error) - - if (Array.isArray(channels)) { - for (const ch of channels) { - if (result[ch.channelCode] === undefined) { - result[ch.channelCode] = { - lang, - site_id: ch.channelCode, - name: ch.channeltitle - } - } - } - } - } - - return Object.values(result) - } -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +const packages = { + 'OSNTV CONNECT': 3720, + 'OSNTV PRIME': 3733, + ALFA: 1281, + 'OSN PINOY PLUS EXTRA': 3519 +} +const country = 'AE' +const tz = 'Asia/Dubai' + +module.exports = { + site: 'osn.com', + days: 2, + url({ channel, date }) { + return `https://www.osn.com/api/TVScheduleWebService.asmx/time?dt=${encodeURIComponent( + date.format('MM/DD/YYYY') + )}&co=${country}&ch=${channel.site_id}&mo=false&hr=0` + }, + request: { + headers({ channel }) { + return { + Referer: `https://www.osn.com/${channel.lang}-${country.toLowerCase()}/watch/tv-schedule` + } + } + }, + parser({ content, channel }) { + const programs = [] + const items = JSON.parse(content) || [] + if (Array.isArray(items)) { + for (const item of items) { + const title = channel.lang === 'ar' ? item.Arab_Title : item.Title + const start = dayjs.tz(item.StartDateTime, 'DD MMM YYYY, HH:mm', tz) + const duration = parseInt(item.TotalDivWidth / 4.8) + const stop = start.add(duration, 'm') + programs.push({ title, start, stop }) + } + } + + return programs + }, + async channels({ lang = 'ar' }) { + const result = {} + const axios = require('axios') + for (const pkg of Object.values(packages)) { + const channels = await axios + .get( + `https://www.osn.com/api/tvchannels.ashx?culture=en-US&packageId=${pkg}&country=${country}` + ) + .then(response => response.data) + .catch(console.error) + + if (Array.isArray(channels)) { + for (const ch of channels) { + if (result[ch.channelCode] === undefined) { + result[ch.channelCode] = { + lang, + site_id: ch.channelCode, + name: ch.channeltitle + } + } + } + } + } + + return Object.values(result) + } +} diff --git a/sites/osn.com/osn.com.test.js b/sites/osn.com/osn.com.test.js index fb2fcfbc..c10c9d86 100644 --- a/sites/osn.com/osn.com.test.js +++ b/sites/osn.com/osn.com.test.js @@ -1,61 +1,61 @@ -const { parser, url, request } = require('./osn.com.config') -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('2024-11-27', 'YYYY-MM-DD').startOf('d') -const channelAR = { site_id: 'FTF', xmltv_id: 'Fatafeat.ae', lang: 'ar' } -const channelEN = { site_id: 'FTF', xmltv_id: 'Fatafeat.ae', lang: 'en' } -const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.json')) - -it('can generate valid request headers', () => { - const result = request.headers({ channel: channelAR, date }) - expect(result).toMatchObject({ - Referer: 'https://www.osn.com/ar-ae/watch/tv-schedule' - }) -}) - -it('can generate valid url', () => { - const result = url({ channel: channelAR, date }) - expect(result).toBe( - 'https://www.osn.com/api/TVScheduleWebService.asmx/time?dt=11%2F27%2F2024&co=AE&ch=FTF&mo=false&hr=0' - ) -}) - -it('can parse response (ar)', () => { - const result = parser({ date, channel: channelAR, content }).map(a => { - a.start = a.start.toJSON() - a.stop = a.stop.toJSON() - return a - }) - expect(result.length).toBe(29) - expect(result[1]).toMatchObject({ - start: '2024-11-26T20:50:00.000Z', - stop: '2024-11-26T21:45:00.000Z', - title: 'بيت الحلويات: الحلقة 3' - }) -}) - -it('can parse response (en)', () => { - const result = parser({ date, channel: channelEN, content }).map(a => { - a.start = a.start.toJSON() - a.stop = a.stop.toJSON() - return a - }) - expect(result.length).toBe(29) - expect(result[1]).toMatchObject({ - start: '2024-11-26T20:50:00.000Z', - stop: '2024-11-26T21:45:00.000Z', - title: 'House Of Desserts: Episode 3' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ date, channel: channelAR, content: '[]' }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./osn.com.config') +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('2024-11-27', 'YYYY-MM-DD').startOf('d') +const channelAR = { site_id: 'FTF', xmltv_id: 'Fatafeat.ae', lang: 'ar' } +const channelEN = { site_id: 'FTF', xmltv_id: 'Fatafeat.ae', lang: 'en' } +const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.json')) + +it('can generate valid request headers', () => { + const result = request.headers({ channel: channelAR, date }) + expect(result).toMatchObject({ + Referer: 'https://www.osn.com/ar-ae/watch/tv-schedule' + }) +}) + +it('can generate valid url', () => { + const result = url({ channel: channelAR, date }) + expect(result).toBe( + 'https://www.osn.com/api/TVScheduleWebService.asmx/time?dt=11%2F27%2F2024&co=AE&ch=FTF&mo=false&hr=0' + ) +}) + +it('can parse response (ar)', () => { + const result = parser({ date, channel: channelAR, content }).map(a => { + a.start = a.start.toJSON() + a.stop = a.stop.toJSON() + return a + }) + expect(result.length).toBe(29) + expect(result[1]).toMatchObject({ + start: '2024-11-26T20:50:00.000Z', + stop: '2024-11-26T21:45:00.000Z', + title: 'بيت الحلويات: الحلقة 3' + }) +}) + +it('can parse response (en)', () => { + const result = parser({ date, channel: channelEN, content }).map(a => { + a.start = a.start.toJSON() + a.stop = a.stop.toJSON() + return a + }) + expect(result.length).toBe(29) + expect(result[1]).toMatchObject({ + start: '2024-11-26T20:50:00.000Z', + stop: '2024-11-26T21:45:00.000Z', + title: 'House Of Desserts: Episode 3' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ date, channel: channelAR, content: '[]' }) + expect(result).toMatchObject([]) +}) diff --git a/sites/pbsguam.org/__data__/content.html b/sites/pbsguam.org/__data__/content.html new file mode 100644 index 00000000..e946e965 --- /dev/null +++ b/sites/pbsguam.org/__data__/content.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/sites/pbsguam.org/__data__/no_content.html b/sites/pbsguam.org/__data__/no_content.html new file mode 100644 index 00000000..8ef664e2 --- /dev/null +++ b/sites/pbsguam.org/__data__/no_content.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sites/pbsguam.org/pbsguam.org.config.js b/sites/pbsguam.org/pbsguam.org.config.js index 504778df..caa3cf49 100644 --- a/sites/pbsguam.org/pbsguam.org.config.js +++ b/sites/pbsguam.org/pbsguam.org.config.js @@ -1,41 +1,41 @@ -const dayjs = require('dayjs') -const isBetween = require('dayjs/plugin/isBetween') - -dayjs.extend(isBetween) - -module.exports = { - site: 'pbsguam.org', - days: 2, - url: 'https://pbsguam.org/calendar/', - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content, date) - items.forEach(item => { - programs.push({ - title: item.title, - start: dayjs(item.start), - stop: dayjs(item.end) - }) - }) - - return programs - } -} - -function parseItems(content, date) { - const [, json] = content.match(/EventsSchedule_1 = (.*);/i) || [null, ''] - let data - try { - data = JSON.parse(json) - } catch { - return [] - } - - if (!data || !Array.isArray(data.feed)) return [] - - return data.feed.filter( - i => - dayjs(i.start).isBetween(date, date.add(1, 'd')) || - dayjs(i.end).isBetween(date, date.add(1, 'd')) - ) -} +const dayjs = require('dayjs') +const isBetween = require('dayjs/plugin/isBetween') + +dayjs.extend(isBetween) + +module.exports = { + site: 'pbsguam.org', + days: 2, + url: 'https://pbsguam.org/calendar/', + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content, date) + items.forEach(item => { + programs.push({ + title: item.title, + start: dayjs(item.start), + stop: dayjs(item.end) + }) + }) + + return programs + } +} + +function parseItems(content, date) { + const [, json] = content.match(/EventsSchedule_1 = (.*);/i) || [null, ''] + let data + try { + data = JSON.parse(json) + } catch { + return [] + } + + if (!data || !Array.isArray(data.feed)) return [] + + return data.feed.filter( + i => + dayjs(i.start).isBetween(date, date.add(1, 'd')) || + dayjs(i.end).isBetween(date, date.add(1, 'd')) + ) +} diff --git a/sites/pbsguam.org/pbsguam.org.test.js b/sites/pbsguam.org/pbsguam.org.test.js index 70a16680..dd6984fa 100644 --- a/sites/pbsguam.org/pbsguam.org.test.js +++ b/sites/pbsguam.org/pbsguam.org.test.js @@ -1,46 +1,44 @@ -const { parser, url } = require('./pbsguam.org.config.js') -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('2021-11-25', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '#', - xmltv_id: 'KGTF.us' -} - -it('can generate valid url', () => { - expect(url).toBe('https://pbsguam.org/calendar/') -}) - -it('can parse response', () => { - const content = ` ` - const result = parser({ date, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-25T08:30:00.000Z', - stop: '2021-11-25T09:00:00.000Z', - title: 'Xavier Riddle and the Secret Museum' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: ' ' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./pbsguam.org.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('2021-11-25', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '#', + xmltv_id: 'KGTF.us' +} + +it('can generate valid url', () => { + expect(url).toBe('https://pbsguam.org/calendar/') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + const result = parser({ date, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-25T08:30:00.000Z', + stop: '2021-11-25T09:00:00.000Z', + title: 'Xavier Riddle and the Secret Museum' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/pickx.be/pickx.be.config.js b/sites/pickx.be/pickx.be.config.js index 0f692f6c..a4011b33 100644 --- a/sites/pickx.be/pickx.be.config.js +++ b/sites/pickx.be/pickx.be.config.js @@ -1,131 +1,131 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -let apiVersion - -module.exports = { - site: 'pickx.be', - days: 2, - async url({ channel, date }) { - if (!apiVersion) { - await fetchApiVersion() - } - - return `https://px-epg.azureedge.net/airings/${apiVersion}/${date.format( - 'YYYY-MM-DD' - )}/channel/${channel.site_id}?timezone=Europe%2FBrussels` - }, - request: { - headers: { - Origin: 'https://www.pickx.be', - Referer: 'https://www.pickx.be/' - } - }, - parser({ channel, content }) { - const programs = [] - if (content) { - const items = JSON.parse(content) - items.forEach(item => { - programs.push({ - title: item.program.title, - sub_title: item.program.episodeTitle, - description: item.program.description, - category: item.program.translatedCategory?.[channel.lang] - ? item.program.translatedCategory[channel.lang] - : item.program.category.split('.')[1], - image: item.program.posterFileName - ? `https://experience-cache.proximustv.be/posterserver/poster/EPG/w-166_h-110/${item.program.posterFileName}` - : null, - season: item.program.seasonNumber, - episode: item.program.episodeNumber, - actors: item.program.actors, - director: item.program.director ? [item.program.director] : null, - start: dayjs(item.programScheduleStart), - stop: dayjs(item.programScheduleEnd) - }) - }) - } - - return programs - }, - async channels() { - let channels = [] - - const query = { - operationName: 'getChannels', - variables: { - language: 'fr', - queryParams: {}, - id: '0', - params: { - shouldReadFromCache: true - } - }, - query: `query getChannels($language: String!, $queryParams: ChannelQueryParams, $id: String, $params: ChannelParams) { - channels(language: $language, queryParams: $queryParams, id: $id, params: $params) { - id - name - language - radio - } - }` - } - - const data = await axios - .post('https://api.proximusmwc.be/tiams/v3/graphql', query) - .then(r => r.data) - .catch(console.error) - - data.data.channels.forEach(channel => { - let lang = channel.language || 'fr' - if (channel.language === 'ger') lang = 'de' - - channels.push({ - lang, - site_id: channel.id, - name: channel.name - }) - }) - - return channels - } -} - -async function fetchApiVersion() { - const hashUrl = 'https://www.pickx.be/nl/televisie/tv-gids' - const hashData = await axios - .get(hashUrl) - .then(r => { - const re = /"hashes":\["(.*)"\]/ - const match = r.data.match(re) - if (match && match[1]) { - return match[1] - } else { - throw new Error('React app version hash not found') - } - }) - .catch(console.error) - - const versionUrl = `https://www.pickx.be/api/s-${hashData}` - const response = await axios.get(versionUrl, { - headers: { - Origin: 'https://www.pickx.be', - Referer: 'https://www.pickx.be/' - } - }) - - return new Promise((resolve, reject) => { - try { - if (response.status === 200) { - apiVersion = response.data.version - resolve() - } else { - console.error(`Failed to fetch API version. Status: ${response.status}`) - reject(`Failed to fetch API version. Status: ${response.status}`) - } - } catch (error) { - console.error('Error during fetchApiVersion:', error) - reject(error) - } - }) -} +const axios = require('axios') +const dayjs = require('dayjs') + +let apiVersion + +module.exports = { + site: 'pickx.be', + days: 2, + async url({ channel, date }) { + if (!apiVersion) { + await fetchApiVersion() + } + + return `https://px-epg.azureedge.net/airings/${apiVersion}/${date.format( + 'YYYY-MM-DD' + )}/channel/${channel.site_id}?timezone=Europe%2FBrussels` + }, + request: { + headers: { + Origin: 'https://www.pickx.be', + Referer: 'https://www.pickx.be/' + } + }, + parser({ channel, content }) { + const programs = [] + if (content) { + const items = JSON.parse(content) + items.forEach(item => { + programs.push({ + title: item.program.title, + sub_title: item.program.episodeTitle, + description: item.program.description, + category: item.program.translatedCategory?.[channel.lang] + ? item.program.translatedCategory[channel.lang] + : item.program.category.split('.')[1], + image: item.program.posterFileName + ? `https://experience-cache.proximustv.be/posterserver/poster/EPG/w-166_h-110/${item.program.posterFileName}` + : null, + season: item.program.seasonNumber, + episode: item.program.episodeNumber, + actors: item.program.actors, + director: item.program.director ? [item.program.director] : null, + start: dayjs(item.programScheduleStart), + stop: dayjs(item.programScheduleEnd) + }) + }) + } + + return programs + }, + async channels() { + let channels = [] + + const query = { + operationName: 'getChannels', + variables: { + language: 'fr', + queryParams: {}, + id: '0', + params: { + shouldReadFromCache: true + } + }, + query: `query getChannels($language: String!, $queryParams: ChannelQueryParams, $id: String, $params: ChannelParams) { + channels(language: $language, queryParams: $queryParams, id: $id, params: $params) { + id + name + language + radio + } + }` + } + + const data = await axios + .post('https://api.proximusmwc.be/tiams/v3/graphql', query) + .then(r => r.data) + .catch(console.error) + + data.data.channels.forEach(channel => { + let lang = channel.language || 'fr' + if (channel.language === 'ger') lang = 'de' + + channels.push({ + lang, + site_id: channel.id, + name: channel.name + }) + }) + + return channels + } +} + +async function fetchApiVersion() { + const hashUrl = 'https://www.pickx.be/nl/televisie/tv-gids' + const hashData = await axios + .get(hashUrl) + .then(r => { + const re = /"hashes":\["(.*)"\]/ + const match = r.data.match(re) + if (match && match[1]) { + return match[1] + } else { + throw new Error('React app version hash not found') + } + }) + .catch(console.error) + + const versionUrl = `https://www.pickx.be/api/s-${hashData}` + const response = await axios.get(versionUrl, { + headers: { + Origin: 'https://www.pickx.be', + Referer: 'https://www.pickx.be/' + } + }) + + return new Promise((resolve, reject) => { + try { + if (response.status === 200) { + apiVersion = response.data.version + resolve() + } else { + console.error(`Failed to fetch API version. Status: ${response.status}`) + reject(`Failed to fetch API version. Status: ${response.status}`) + } + } catch (error) { + console.error('Error during fetchApiVersion:', error) + reject(error) + } + }) +} diff --git a/sites/pickx.be/pickx.be.test.js b/sites/pickx.be/pickx.be.test.js index ee11e679..3dd95ee8 100644 --- a/sites/pickx.be/pickx.be.test.js +++ b/sites/pickx.be/pickx.be.test.js @@ -1,83 +1,83 @@ -const { parser, url, request } = require('./pickx.be.config.js') -const axios = require('axios') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -jest.mock('axios') - -axios.get.mockImplementation((url, data) => { - if (url === 'https://www.pickx.be/nl/televisie/tv-gids') { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/hash.html'), 'utf8') - }) - } else if ( - url === - 'https://www.pickx.be/api/s-375ce5e452cf964b4158545d9ddf26cc97d6411f0998a2fa7ed5922c88d5bdc4' && - JSON.stringify(data) === - JSON.stringify({ - headers: { - Origin: 'https://www.pickx.be', - Referer: 'https://www.pickx.be/' - } - }) - ) { - return Promise.resolve({ - status: 200, - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/version.json'))) - }) - } else { - return Promise.resolve({ - data: '' - }) - } -}) - -const date = dayjs.utc('2023-12-13').startOf('d') -const channel = { - lang: 'fr', - site_id: 'UID0118' -} - -it('can generate valid url', async () => { - expect(await url({ channel, date })).toBe( - 'https://px-epg.azureedge.net/airings/21738594888692v.4.2/2023-12-13/channel/UID0118?timezone=Europe%2FBrussels' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - Origin: 'https://www.pickx.be', - Referer: 'https://www.pickx.be/' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/data.json')) - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result[0]).toMatchObject({ - start: '2023-12-12T23:55:00.000Z', - stop: '2023-12-13T00:15:00.000Z', - title: 'Le 22h30', - description: 'Le journal de vivre ici.', - category: 'Info', - image: - 'https://experience-cache.proximustv.be/posterserver/poster/EPG/w-166_h-110/250_250_4B990CC58066A7B2A660AFA0BDDE5C41.jpg' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./pickx.be.config.js') +const axios = require('axios') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +jest.mock('axios') + +axios.get.mockImplementation((url, data) => { + if (url === 'https://www.pickx.be/nl/televisie/tv-gids') { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/hash.html'), 'utf8') + }) + } else if ( + url === + 'https://www.pickx.be/api/s-375ce5e452cf964b4158545d9ddf26cc97d6411f0998a2fa7ed5922c88d5bdc4' && + JSON.stringify(data) === + JSON.stringify({ + headers: { + Origin: 'https://www.pickx.be', + Referer: 'https://www.pickx.be/' + } + }) + ) { + return Promise.resolve({ + status: 200, + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/version.json'))) + }) + } else { + return Promise.resolve({ + data: '' + }) + } +}) + +const date = dayjs.utc('2023-12-13').startOf('d') +const channel = { + lang: 'fr', + site_id: 'UID0118' +} + +it('can generate valid url', async () => { + expect(await url({ channel, date })).toBe( + 'https://px-epg.azureedge.net/airings/21738594888692v.4.2/2023-12-13/channel/UID0118?timezone=Europe%2FBrussels' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + Origin: 'https://www.pickx.be', + Referer: 'https://www.pickx.be/' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/data.json')) + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result[0]).toMatchObject({ + start: '2023-12-12T23:55:00.000Z', + stop: '2023-12-13T00:15:00.000Z', + title: 'Le 22h30', + description: 'Le journal de vivre ici.', + category: 'Info', + image: + 'https://experience-cache.proximustv.be/posterserver/poster/EPG/w-166_h-110/250_250_4B990CC58066A7B2A660AFA0BDDE5C41.jpg' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/player.ee.co.uk/player.ee.co.uk.config.js b/sites/player.ee.co.uk/player.ee.co.uk.config.js index 6914b017..21258e98 100644 --- a/sites/player.ee.co.uk/player.ee.co.uk.config.js +++ b/sites/player.ee.co.uk/player.ee.co.uk.config.js @@ -1,104 +1,104 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'player.ee.co.uk', - days: 2, - url({ date, channel, hour = 0 }) { - return `https://api.youview.tv/metadata/linear/v2/schedule/by-servicelocator?serviceLocator=${encodeURIComponent( - channel.site_id - )}&interval=${date.format('YYYY-MM-DD')}T${hour.toString().padStart(2, '0')}Z/PT12H` - }, - request: { - headers: { - Referer: 'https://player.ee.co.uk/' - } - }, - async parser({ content, channel, date }) { - const programs = [] - if (content) { - const schedule = JSON.parse(content) - // fetch next 12 hours schedule - const { url, request } = module.exports - const nextSchedule = await axios - .get(url({ channel, date, hour: 12 }), { headers: request.headers }) - .then(response => response.data) - .catch(console.error) - - if (schedule?.items) { - // merge schedules - if (nextSchedule?.items) { - schedule.items.push(...nextSchedule.items) - } - schedule.items.forEach(item => { - let season, episode - const start = dayjs.utc(item.publishedStartTime) - const stop = start.add(item.publishedDuration, 's') - const description = item.synopsis - if (description) { - const matches = description.trim().match(/\(?S(\d+)[/\s]Ep(\d+)\)?/) - if (matches) { - if (matches[1]) { - season = parseInt(matches[1]) - } - if (matches[2]) { - episode = parseInt(matches[2]) - } - } - } - programs.push({ - title: item.title, - description, - season, - episode, - start, - stop - }) - }) - } - } - - return programs - }, - async channels() { - const token = - 'eyJkaXNjb3ZlcnlVc2VyR3JvdXBzIjpbIkFMTFVTRVJTIiwiYWxsIiwiaHR0cDovL3JlZmRhd' + - 'GEueW91dmlldy5jb20vbXBlZzdjcy9Zb3VWaWV3QXBwbGljYXRpb25QbGF5ZXJDUy8yMDIxLT' + - 'A5LTEwI2FuZHJvaWRfcnVudGltZS1wcm9maWxlMSIsInRhZzpidC5jb20sMjAxOC0wNy0xMTp' + - '1c2VyZ3JvdXAjR0JSLWJ0X25vd1RWX211bHRpY2FzdCIsInRhZzpidC5jb20sMjAyMS0xMC0y' + - 'NTp1c2VyZ3JvdXAjR0JSLWJ0X2V1cm9zcG9ydCJdLCJyZWdpb25zIjpbIkFMTFJFR0lPTlMiL' + - 'CJHQlIiLCJHQlItRU5HIiwiR0JSLUVORy1sb25kb24iLCJhbGwiXSwic3Vic2V0IjoiMy41Lj' + - 'EvYW5kcm9pZF9ydW50aW1lLXByb2ZpbGUxL0JST0FEQ0FTVF9JUC9HQlItYnRfYnJvYWRiYW5' + - 'kIiwic3Vic2V0cyI6WyIvLy8iLCIvL0JST0FEQ0FTVF9JUC8iLCIzLjUvLy8iXX0=' - const extensions = [ - 'LinearCategoriesExtension', - 'LogicalChannelNumberExtension', - 'BTSubscriptionCodesExtension' - ] - const result = await axios - .get('https://api.youview.tv/metadata/linear/v2/linear-services', { - params: { - contentTargetingToken: token, - extensions: extensions.join(',') - }, - headers: module.exports.request.headers - }) - .then(response => response.data) - .catch(console.error) - - return ( - result?.items - .filter(channel => channel.contentTypes.indexOf('tv') >= 0) - .map(channel => { - return { - lang: 'en', - site_id: channel.serviceLocator, - name: channel.fullName - } - }) || [] - ) - } -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'player.ee.co.uk', + days: 2, + url({ date, channel, hour = 0 }) { + return `https://api.youview.tv/metadata/linear/v2/schedule/by-servicelocator?serviceLocator=${encodeURIComponent( + channel.site_id + )}&interval=${date.format('YYYY-MM-DD')}T${hour.toString().padStart(2, '0')}Z/PT12H` + }, + request: { + headers: { + Referer: 'https://player.ee.co.uk/' + } + }, + async parser({ content, channel, date }) { + const programs = [] + if (content) { + const schedule = JSON.parse(content) + // fetch next 12 hours schedule + const { url, request } = module.exports + const nextSchedule = await axios + .get(url({ channel, date, hour: 12 }), { headers: request.headers }) + .then(response => response.data) + .catch(console.error) + + if (schedule?.items) { + // merge schedules + if (nextSchedule?.items) { + schedule.items.push(...nextSchedule.items) + } + schedule.items.forEach(item => { + let season, episode + const start = dayjs.utc(item.publishedStartTime) + const stop = start.add(item.publishedDuration, 's') + const description = item.synopsis + if (description) { + const matches = description.trim().match(/\(?S(\d+)[/\s]Ep(\d+)\)?/) + if (matches) { + if (matches[1]) { + season = parseInt(matches[1]) + } + if (matches[2]) { + episode = parseInt(matches[2]) + } + } + } + programs.push({ + title: item.title, + description, + season, + episode, + start, + stop + }) + }) + } + } + + return programs + }, + async channels() { + const token = + 'eyJkaXNjb3ZlcnlVc2VyR3JvdXBzIjpbIkFMTFVTRVJTIiwiYWxsIiwiaHR0cDovL3JlZmRhd' + + 'GEueW91dmlldy5jb20vbXBlZzdjcy9Zb3VWaWV3QXBwbGljYXRpb25QbGF5ZXJDUy8yMDIxLT' + + 'A5LTEwI2FuZHJvaWRfcnVudGltZS1wcm9maWxlMSIsInRhZzpidC5jb20sMjAxOC0wNy0xMTp' + + '1c2VyZ3JvdXAjR0JSLWJ0X25vd1RWX211bHRpY2FzdCIsInRhZzpidC5jb20sMjAyMS0xMC0y' + + 'NTp1c2VyZ3JvdXAjR0JSLWJ0X2V1cm9zcG9ydCJdLCJyZWdpb25zIjpbIkFMTFJFR0lPTlMiL' + + 'CJHQlIiLCJHQlItRU5HIiwiR0JSLUVORy1sb25kb24iLCJhbGwiXSwic3Vic2V0IjoiMy41Lj' + + 'EvYW5kcm9pZF9ydW50aW1lLXByb2ZpbGUxL0JST0FEQ0FTVF9JUC9HQlItYnRfYnJvYWRiYW5' + + 'kIiwic3Vic2V0cyI6WyIvLy8iLCIvL0JST0FEQ0FTVF9JUC8iLCIzLjUvLy8iXX0=' + const extensions = [ + 'LinearCategoriesExtension', + 'LogicalChannelNumberExtension', + 'BTSubscriptionCodesExtension' + ] + const result = await axios + .get('https://api.youview.tv/metadata/linear/v2/linear-services', { + params: { + contentTargetingToken: token, + extensions: extensions.join(',') + }, + headers: module.exports.request.headers + }) + .then(response => response.data) + .catch(console.error) + + return ( + result?.items + .filter(channel => channel.contentTypes.indexOf('tv') >= 0) + .map(channel => { + return { + lang: 'en', + site_id: channel.serviceLocator, + name: channel.fullName + } + }) || [] + ) + } +} diff --git a/sites/player.ee.co.uk/player.ee.co.uk.test.js b/sites/player.ee.co.uk/player.ee.co.uk.test.js index 30424e74..99619111 100644 --- a/sites/player.ee.co.uk/player.ee.co.uk.test.js +++ b/sites/player.ee.co.uk/player.ee.co.uk.test.js @@ -1,74 +1,74 @@ -const { parser, url } = require('./player.ee.co.uk.config.js') -const fs = require('fs') -const path = require('path') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2023-12-13').startOf('d') -const channel = { - site_id: 'dvb://233a..6d60', - xmltv_id: 'HGTV.uk' -} - -axios.get.mockImplementation(url => { - if ( - url === - 'https://api.youview.tv/metadata/linear/v2/schedule/by-servicelocator?serviceLocator=dvb%3A%2F%2F233a..6d60&interval=2023-12-13T12Z/PT12H' - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/data1.json'))) - }) - } - - return Promise.resolve({ data: '' }) -}) - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://api.youview.tv/metadata/linear/v2/schedule/by-servicelocator?serviceLocator=dvb%3A%2F%2F233a..6d60&interval=2023-12-13T00Z/PT12H' - ) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/data.json')) - const result = (await parser({ content, channel, date })).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - title: 'Bargain Mansions', - description: - 'Tamara and her dad help a recent widow who loves to cook for her family design her dream kitchen, perfect for entertaining and large gatherings. S4/Ep1', - season: 4, - episode: 1, - start: '2023-12-13T13:00:00.000Z', - stop: '2023-12-13T14:00:00.000Z' - }, - { - title: 'Flip Or Flop', - description: - 'Tarek and Christina are contacted by a cash strapped flipper who needs to unload a project house. S2/Ep2', - season: 2, - episode: 2, - start: '2023-12-13T14:00:00.000Z', - stop: '2023-12-13T14:30:00.000Z' - } - ]) -}) - -it('can handle empty guide', async () => { - const result = await parser({ - channel, - date, - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./player.ee.co.uk.config.js') +const fs = require('fs') +const path = require('path') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2023-12-13').startOf('d') +const channel = { + site_id: 'dvb://233a..6d60', + xmltv_id: 'HGTV.uk' +} + +axios.get.mockImplementation(url => { + if ( + url === + 'https://api.youview.tv/metadata/linear/v2/schedule/by-servicelocator?serviceLocator=dvb%3A%2F%2F233a..6d60&interval=2023-12-13T12Z/PT12H' + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/data1.json'))) + }) + } + + return Promise.resolve({ data: '' }) +}) + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://api.youview.tv/metadata/linear/v2/schedule/by-servicelocator?serviceLocator=dvb%3A%2F%2F233a..6d60&interval=2023-12-13T00Z/PT12H' + ) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/data.json')) + const result = (await parser({ content, channel, date })).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + title: 'Bargain Mansions', + description: + 'Tamara and her dad help a recent widow who loves to cook for her family design her dream kitchen, perfect for entertaining and large gatherings. S4/Ep1', + season: 4, + episode: 1, + start: '2023-12-13T13:00:00.000Z', + stop: '2023-12-13T14:00:00.000Z' + }, + { + title: 'Flip Or Flop', + description: + 'Tarek and Christina are contacted by a cash strapped flipper who needs to unload a project house. S2/Ep2', + season: 2, + episode: 2, + start: '2023-12-13T14:00:00.000Z', + stop: '2023-12-13T14:30:00.000Z' + } + ]) +}) + +it('can handle empty guide', async () => { + const result = await parser({ + channel, + date, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/playtv.unifi.com.my/playtv.unifi.com.my.config.js b/sites/playtv.unifi.com.my/playtv.unifi.com.my.config.js index d426eeb5..b3e533e2 100644 --- a/sites/playtv.unifi.com.my/playtv.unifi.com.my.config.js +++ b/sites/playtv.unifi.com.my/playtv.unifi.com.my.config.js @@ -1,85 +1,85 @@ -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: 'playtv.unifi.com.my', - days: 2, - url: 'https://unifi.com.my/tv/api/tv', - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - }, - method: 'POST', - headers: { - 'x-requested-with': 'XMLHttpRequest' - }, - data({ date }) { - const params = new URLSearchParams() - params.append('date', date.format('YYYY-MM-DD')) - return params - } - }, - parser({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const start = parseStart(item, date) - const stop = start.add(item.minute, 'minute') - programs.push({ - title: item.name, - start, - stop - }) - }) - return programs - }, - async channels() { - const axios = require('axios') - const data = await axios - .post( - 'https://playtv.unifi.com.my:7053/VSP/V3/QueryAllChannel', - { isReturnAllMedia: '0' }, - { - params: { - userFilter: '-1880777955', - from: 'inMSAAccess' - } - } - ) - .then(r => r.data) - .catch(console.log) - - return data.channelDetails.map(item => { - return { - lang: 'en', - site_id: item.ID, - name: item.name - } - }) - } -} - -function parseItems(content, channel) { - try { - const [, string] = content.match(/initializeClient(.*)$/) - const data = JSON.parse(string) - if (!data) return [] - if (!Array.isArray(data)) return [] - - const channelData = data.find(i => i.id == channel.site_id) - return channelData.items && Array.isArray(channelData.items) ? channelData.items : [] - } catch { - return [] - } -} - -function parseStart(item, date) { - const time = `${date.format('YYYY-MM-DD')} ${item.start_time}` - return dayjs.tz(time, 'YYYY-MM-DD H:mma', 'Asia/Kuala_Lumpur') -} +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: 'playtv.unifi.com.my', + days: 2, + url: 'https://unifi.com.my/tv/api/tv', + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + }, + method: 'POST', + headers: { + 'x-requested-with': 'XMLHttpRequest' + }, + data({ date }) { + const params = new URLSearchParams() + params.append('date', date.format('YYYY-MM-DD')) + return params + } + }, + parser({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const start = parseStart(item, date) + const stop = start.add(item.minute, 'minute') + programs.push({ + title: item.name, + start, + stop + }) + }) + return programs + }, + async channels() { + const axios = require('axios') + const data = await axios + .post( + 'https://playtv.unifi.com.my:7053/VSP/V3/QueryAllChannel', + { isReturnAllMedia: '0' }, + { + params: { + userFilter: '-1880777955', + from: 'inMSAAccess' + } + } + ) + .then(r => r.data) + .catch(console.log) + + return data.channelDetails.map(item => { + return { + lang: 'en', + site_id: item.ID, + name: item.name + } + }) + } +} + +function parseItems(content, channel) { + try { + const [, string] = content.match(/initializeClient(.*)$/) + const data = JSON.parse(string) + if (!data) return [] + if (!Array.isArray(data)) return [] + + const channelData = data.find(i => i.id == channel.site_id) + return channelData.items && Array.isArray(channelData.items) ? channelData.items : [] + } catch { + return [] + } +} + +function parseStart(item, date) { + const time = `${date.format('YYYY-MM-DD')} ${item.start_time}` + return dayjs.tz(time, 'YYYY-MM-DD H:mma', 'Asia/Kuala_Lumpur') +} diff --git a/sites/playtv.unifi.com.my/playtv.unifi.com.my.test.js b/sites/playtv.unifi.com.my/playtv.unifi.com.my.test.js index 5b1fca9f..2fc223f0 100644 --- a/sites/playtv.unifi.com.my/playtv.unifi.com.my.test.js +++ b/sites/playtv.unifi.com.my/playtv.unifi.com.my.test.js @@ -1,55 +1,55 @@ -const { parser, url, request } = require('./playtv.unifi.com.my.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('2023-01-13', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '20000009', - xmltv_id: 'TV1.my' -} - -it('can generate valid url', () => { - expect(url).toBe('https://unifi.com.my/tv/api/tv') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'x-requested-with': 'XMLHttpRequest' - }) -}) - -it('can generate valid request data', () => { - const data = request.data({ date }) - - expect(data.get('date')).toBe('2023-01-13') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') - const results = parser({ content, date, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - title: 'Berita Tengah Malam', - start: '2023-01-12T16:00:00.000Z', - stop: '2023-01-12T16:30:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ content: '', channel }) - - expect(results).toMatchObject([]) -}) +const { parser, url, request } = require('./playtv.unifi.com.my.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('2023-01-13', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '20000009', + xmltv_id: 'TV1.my' +} + +it('can generate valid url', () => { + expect(url).toBe('https://unifi.com.my/tv/api/tv') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'x-requested-with': 'XMLHttpRequest' + }) +}) + +it('can generate valid request data', () => { + const data = request.data({ date }) + + expect(data.get('date')).toBe('2023-01-13') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') + const results = parser({ content, date, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + title: 'Berita Tengah Malam', + start: '2023-01-12T16:00:00.000Z', + stop: '2023-01-12T16:30:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ content: '', channel }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/plex.tv/plex.tv.config.js b/sites/plex.tv/plex.tv.config.js index bb7eb56a..c2f33b12 100644 --- a/sites/plex.tv/plex.tv.config.js +++ b/sites/plex.tv/plex.tv.config.js @@ -1,91 +1,91 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const API_ENDPOINT = 'https://epg.provider.plex.tv' - -module.exports = { - site: 'plex.tv', - days: 2, - request: { - headers: { - 'x-plex-provider-version': '5.1' - } - }, - url: function ({ channel, date }) { - const [, channelGridKey] = channel.site_id.split('-') - - return `${API_ENDPOINT}/grid?channelGridKey=${channelGridKey}&date=${date.format('YYYY-MM-DD')}` - }, - parser({ content }) { - const programs = [] - const items = parseItems(content) - for (let item of items) { - programs.push({ - title: item.title, - description: item.summary, - categories: parseCategories(item), - image: item.art, - start: parseStart(item), - stop: parseStop(item) - }) - } - - return programs - }, - async channels({ token }) { - const data = await axios - .get(`${API_ENDPOINT}/lineups/plex/channels?X-Plex-Token=${token}`) - .then(r => r.data) - .catch(console.error) - - return data.MediaContainer.Channel.map(c => { - return { - lang: 'en', - site_id: c.id, - name: c.title - } - }) - } -} - -function parseCategories(item) { - return Array.isArray(item.Genre) ? item.Genre.map(g => g.tag) : [] -} - -function parseStart(item) { - return item.beginsAt ? dayjs.unix(item.beginsAt) : null -} - -function parseStop(item) { - return item.endsAt ? dayjs.unix(item.endsAt) : null -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !data.MediaContainer || !Array.isArray(data.MediaContainer.Metadata)) return [] - const metadata = data.MediaContainer.Metadata - const items = [] - metadata.forEach(item => { - let segments = [] - item.Media.sort(byTime).forEach(media => { - let prevSegment = segments[segments.length - 1] - if (prevSegment && prevSegment.endsAt === media.beginsAt) { - prevSegment.endsAt = media.endsAt - } else { - segments.push(media) - } - }) - - segments.forEach(segment => { - items.push({ ...item, segments, beginsAt: segment.beginsAt, endsAt: segment.endsAt }) - }) - }) - - return items.sort(byTime) - - function byTime(a, b) { - if (a.beginsAt > b.beginsAt) return 1 - if (a.beginsAt < b.beginsAt) return -1 - return 0 - } -} +const axios = require('axios') +const dayjs = require('dayjs') + +const API_ENDPOINT = 'https://epg.provider.plex.tv' + +module.exports = { + site: 'plex.tv', + days: 2, + request: { + headers: { + 'x-plex-provider-version': '5.1' + } + }, + url: function ({ channel, date }) { + const [, channelGridKey] = channel.site_id.split('-') + + return `${API_ENDPOINT}/grid?channelGridKey=${channelGridKey}&date=${date.format('YYYY-MM-DD')}` + }, + parser({ content }) { + const programs = [] + const items = parseItems(content) + for (let item of items) { + programs.push({ + title: item.title, + description: item.summary, + categories: parseCategories(item), + image: item.art, + start: parseStart(item), + stop: parseStop(item) + }) + } + + return programs + }, + async channels({ token }) { + const data = await axios + .get(`${API_ENDPOINT}/lineups/plex/channels?X-Plex-Token=${token}`) + .then(r => r.data) + .catch(console.error) + + return data.MediaContainer.Channel.map(c => { + return { + lang: 'en', + site_id: c.id, + name: c.title + } + }) + } +} + +function parseCategories(item) { + return Array.isArray(item.Genre) ? item.Genre.map(g => g.tag) : [] +} + +function parseStart(item) { + return item.beginsAt ? dayjs.unix(item.beginsAt) : null +} + +function parseStop(item) { + return item.endsAt ? dayjs.unix(item.endsAt) : null +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !data.MediaContainer || !Array.isArray(data.MediaContainer.Metadata)) return [] + const metadata = data.MediaContainer.Metadata + const items = [] + metadata.forEach(item => { + let segments = [] + item.Media.sort(byTime).forEach(media => { + let prevSegment = segments[segments.length - 1] + if (prevSegment && prevSegment.endsAt === media.beginsAt) { + prevSegment.endsAt = media.endsAt + } else { + segments.push(media) + } + }) + + segments.forEach(segment => { + items.push({ ...item, segments, beginsAt: segment.beginsAt, endsAt: segment.endsAt }) + }) + }) + + return items.sort(byTime) + + function byTime(a, b) { + if (a.beginsAt > b.beginsAt) return 1 + if (a.beginsAt < b.beginsAt) return -1 + return 0 + } +} diff --git a/sites/plex.tv/plex.tv.test.js b/sites/plex.tv/plex.tv.test.js index a189c691..6cb2e74b 100644 --- a/sites/plex.tv/plex.tv.test.js +++ b/sites/plex.tv/plex.tv.test.js @@ -1,56 +1,56 @@ -const { parser, url, request } = require('./plex.tv.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) - -jest.mock('axios') - -const date = dayjs.utc('2023-02-05', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '5e20b730f2f8d5003d739db7-5eea605674085f0040ddc7a6', - xmltv_id: 'DarkMatterTV.us' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://epg.provider.plex.tv/grid?channelGridKey=5eea605674085f0040ddc7a6&date=2023-02-05' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'x-plex-provider-version': '5.1' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - // expect(results.length).toBe(15) - expect(results[0]).toMatchObject({ - start: '2023-02-04T23:31:14.000Z', - stop: '2023-02-05T01:10:45.000Z', - title: 'Violet & Daisy', - description: - 'Two teenage assassins accept what they think will be a quick-and-easy job, until an unexpected target throws them off their plan.', - image: 'https://provider-static.plex.tv/epg/images/ott_channels/arts/darkmatter-tv-about.jpg', - categories: ['Movies'] - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const results = parser({ content }) - - expect(results).toMatchObject([]) -}) +const { parser, url, request } = require('./plex.tv.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) + +jest.mock('axios') + +const date = dayjs.utc('2023-02-05', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '5e20b730f2f8d5003d739db7-5eea605674085f0040ddc7a6', + xmltv_id: 'DarkMatterTV.us' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://epg.provider.plex.tv/grid?channelGridKey=5eea605674085f0040ddc7a6&date=2023-02-05' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'x-plex-provider-version': '5.1' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + // expect(results.length).toBe(15) + expect(results[0]).toMatchObject({ + start: '2023-02-04T23:31:14.000Z', + stop: '2023-02-05T01:10:45.000Z', + title: 'Violet & Daisy', + description: + 'Two teenage assassins accept what they think will be a quick-and-easy job, until an unexpected target throws them off their plan.', + image: 'https://provider-static.plex.tv/epg/images/ott_channels/arts/darkmatter-tv-about.jpg', + categories: ['Movies'] + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const results = parser({ content }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/pluto.tv/pluto.tv.config.js b/sites/pluto.tv/pluto.tv.config.js index 294e130e..d4001a47 100644 --- a/sites/pluto.tv/pluto.tv.config.js +++ b/sites/pluto.tv/pluto.tv.config.js @@ -1,49 +1,49 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'pluto.tv', - days: 3, - - url: function ({ date, channel }) { - const channelId = channel.site_id - - const localTimezone = dayjs.tz.guess() - - const startTime = dayjs(date).tz(localTimezone).startOf('day').toISOString() - const endTime = dayjs(date).tz(localTimezone).add(this.days, 'day').endOf('day').toISOString() - - const generatedUrl = `https://api.pluto.tv/v2/channels/${channelId}?start=${startTime}&stop=${endTime}` - return generatedUrl - }, - - parser: function ({ content }) { - const data = JSON.parse(content) - const programs = [] - - if (data.timelines) { - data.timelines.forEach(item => { - programs.push({ - title: item.title, - subTitle: item.episode?.name || '', - description: item.episode?.description || '', - episode: item.episode?.number || '', - season: item.episode?.season || '', - actors: item.episode?.clip?.actors || [], - categories: [item.episode?.genre, item.episode?.subGenre].filter(Boolean), - rating: item.episode?.rating || '', - date: item.episode?.clip?.originalReleaseDate || '', - icon: item.episode?.series?.tile?.path || '', - start: item.start, - stop: item.stop - }) - }) - } - - return programs - } -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'pluto.tv', + days: 3, + + url: function ({ date, channel }) { + const channelId = channel.site_id + + const localTimezone = dayjs.tz.guess() + + const startTime = dayjs(date).tz(localTimezone).startOf('day').toISOString() + const endTime = dayjs(date).tz(localTimezone).add(this.days, 'day').endOf('day').toISOString() + + const generatedUrl = `https://api.pluto.tv/v2/channels/${channelId}?start=${startTime}&stop=${endTime}` + return generatedUrl + }, + + parser: function ({ content }) { + const data = JSON.parse(content) + const programs = [] + + if (data.timelines) { + data.timelines.forEach(item => { + programs.push({ + title: item.title, + subTitle: item.episode?.name || '', + description: item.episode?.description || '', + episode: item.episode?.number || '', + season: item.episode?.season || '', + actors: item.episode?.clip?.actors || [], + categories: [item.episode?.genre, item.episode?.subGenre].filter(Boolean), + rating: item.episode?.rating || '', + date: item.episode?.clip?.originalReleaseDate || '', + icon: item.episode?.series?.tile?.path || '', + start: item.start, + stop: item.stop + }) + }) + } + + return programs + } +} diff --git a/sites/pluto.tv/pluto.tv.test.js b/sites/pluto.tv/pluto.tv.test.js index 37ec1a81..9fd9308d 100644 --- a/sites/pluto.tv/pluto.tv.test.js +++ b/sites/pluto.tv/pluto.tv.test.js @@ -1,59 +1,59 @@ -const config = require('./pluto.tv.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('2024-12-28', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '5ee92e72fb286e0007285fec', - xmltv_id: 'Naruto' -} - -it('can generate valid url', () => { - const url = config.url({ date, channel }) - expect(url).toBe( - 'https://api.pluto.tv/v2/channels/5ee92e72fb286e0007285fec?start=2024-12-27T12:00:00.000Z&stop=2024-12-31T11:59:59.999Z' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') - const results = config.parser({ content }).map(p => { - p.start = dayjs(p.start).toJSON() - p.stop = dayjs(p.stop).toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2024-12-28T00:21:00.000Z', - stop: '2024-12-28T00:48:00.000Z', - title: 'Naruto: El Tercer Hokage, Eternamente', - description: - 'Gaara y Naruto continúan combatiendo con todas sus fuerzas. Decidido a proteger a Sakura, Naruto ataca a Gaara una y otra vez.', - subTitle: 'El Tercer Hokage, Eternamente', - episode: 80, - season: 2, - actors: [ - 'Isabel Martion (Naruto Uzumaki)', - 'Christine Byrd (Sakura Haruno)', - 'Victor Ugarte (Sasuke Uchiha)', - 'Alfonso Obreg (Kakashi Hatake)' - ], - categories: ['Anime', 'Anime Action & Adventure'], - rating: 'TV-14', - date: '2004-04-21T00:00:00.000Z', - icon: 'https://images.pluto.tv/series/5e73b850e40c9f001a0a9fb4/tile.jpg?fill=blur&fit=fill&fm=jpg&h=660&q=75&w=660' - }) -}) - -it('can handle empty guide', () => { - const results = config.parser({ - content: '[]' - }) - expect(results).toMatchObject([]) -}) +const config = require('./pluto.tv.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('2024-12-28', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '5ee92e72fb286e0007285fec', + xmltv_id: 'Naruto' +} + +it('can generate valid url', () => { + const url = config.url({ date, channel }) + expect(url).toBe( + 'https://api.pluto.tv/v2/channels/5ee92e72fb286e0007285fec?start=2024-12-27T12:00:00.000Z&stop=2024-12-31T11:59:59.999Z' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') + const results = config.parser({ content }).map(p => { + p.start = dayjs(p.start).toJSON() + p.stop = dayjs(p.stop).toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2024-12-28T00:21:00.000Z', + stop: '2024-12-28T00:48:00.000Z', + title: 'Naruto: El Tercer Hokage, Eternamente', + description: + 'Gaara y Naruto continúan combatiendo con todas sus fuerzas. Decidido a proteger a Sakura, Naruto ataca a Gaara una y otra vez.', + subTitle: 'El Tercer Hokage, Eternamente', + episode: 80, + season: 2, + actors: [ + 'Isabel Martion (Naruto Uzumaki)', + 'Christine Byrd (Sakura Haruno)', + 'Victor Ugarte (Sasuke Uchiha)', + 'Alfonso Obreg (Kakashi Hatake)' + ], + categories: ['Anime', 'Anime Action & Adventure'], + rating: 'TV-14', + date: '2004-04-21T00:00:00.000Z', + icon: 'https://images.pluto.tv/series/5e73b850e40c9f001a0a9fb4/tile.jpg?fill=blur&fit=fill&fm=jpg&h=660&q=75&w=660' + }) +}) + +it('can handle empty guide', () => { + const results = config.parser({ + content: '[]' + }) + expect(results).toMatchObject([]) +}) diff --git a/sites/programacion-tv.elpais.com/programacion-tv.elpais.com.config.js b/sites/programacion-tv.elpais.com/programacion-tv.elpais.com.config.js index 2ac993f7..7ff4b728 100644 --- a/sites/programacion-tv.elpais.com/programacion-tv.elpais.com.config.js +++ b/sites/programacion-tv.elpais.com/programacion-tv.elpais.com.config.js @@ -1,105 +1,105 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'programacion-tv.elpais.com', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: function ({ date }) { - return `https://programacion-tv.elpais.com/data/parrilla_${date.format('DDMMYYYY')}.json` - }, - parser: async function ({ content, channel }) { - const programs = [] - const items = parseItems(content, channel) - if (!items.length) return programs - const programsData = await loadProgramsData(channel) - items.forEach(item => { - const start = parseStart(item) - const stop = parseStop(item) - const details = programsData.find(p => p.id_programa === item.id_programa) || {} - programs.push({ - title: item.title, - sub_title: details.episode_title, - description: details.episode_description || item.description, - category: parseCategory(details), - image: parseImage(details), - director: parseList(details.director), - actors: parseList(details.actors), - writer: parseList(details.script), - producer: parseList(details.producer), - presenter: parseList(details.presented_by), - composer: parseList(details.music), - guest: parseList(details.guest_actors), - season: parseNumber(details.season), - episode: parseNumber(details.episode), - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://programacion-tv.elpais.com/data/canales.json') - .then(r => r.data) - .catch(console.log) - - return Object.values(data).map(item => ({ - lang: 'es', - site_id: item.id, - name: item.nombre - })) - } -} - -function parseNumber(str) { - return typeof str === 'string' ? parseInt(str) : null -} - -function parseList(str) { - return typeof str === 'string' ? str.split(', ') : [] -} - -function parseImage(details) { - const url = new URL(details.image, 'https://programacion-tv.elpais.com/') - - return url.href -} - -function parseCategory(details) { - return [details.txt_genre, details.txt_subgenre].filter(Boolean).join('/') -} - -async function loadProgramsData(channel) { - return await axios - .get(`https://programacion-tv.elpais.com/data/programas/${channel.site_id}.json`) - .then(r => r.data) - .catch(console.log) -} - -function parseStart(item) { - return dayjs.tz(item.iniDate, 'YYYY-MM-DD HH:mm:ss', 'Europe/Madrid') -} - -function parseStop(item) { - return dayjs.tz(item.endDate, 'YYYY-MM-DD HH:mm:ss', 'Europe/Madrid') -} - -function parseItems(content, channel) { - if (!content) return [] - const data = JSON.parse(content) - const channelData = data.find(i => i.idCanal === channel.site_id) - if (!channelData || !Array.isArray(channelData.programas)) return [] - - return channelData.programas -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'programacion-tv.elpais.com', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: function ({ date }) { + return `https://programacion-tv.elpais.com/data/parrilla_${date.format('DDMMYYYY')}.json` + }, + parser: async function ({ content, channel }) { + const programs = [] + const items = parseItems(content, channel) + if (!items.length) return programs + const programsData = await loadProgramsData(channel) + items.forEach(item => { + const start = parseStart(item) + const stop = parseStop(item) + const details = programsData.find(p => p.id_programa === item.id_programa) || {} + programs.push({ + title: item.title, + sub_title: details.episode_title, + description: details.episode_description || item.description, + category: parseCategory(details), + image: parseImage(details), + director: parseList(details.director), + actors: parseList(details.actors), + writer: parseList(details.script), + producer: parseList(details.producer), + presenter: parseList(details.presented_by), + composer: parseList(details.music), + guest: parseList(details.guest_actors), + season: parseNumber(details.season), + episode: parseNumber(details.episode), + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://programacion-tv.elpais.com/data/canales.json') + .then(r => r.data) + .catch(console.log) + + return Object.values(data).map(item => ({ + lang: 'es', + site_id: item.id, + name: item.nombre + })) + } +} + +function parseNumber(str) { + return typeof str === 'string' ? parseInt(str) : null +} + +function parseList(str) { + return typeof str === 'string' ? str.split(', ') : [] +} + +function parseImage(details) { + const url = new URL(details.image, 'https://programacion-tv.elpais.com/') + + return url.href +} + +function parseCategory(details) { + return [details.txt_genre, details.txt_subgenre].filter(Boolean).join('/') +} + +async function loadProgramsData(channel) { + return await axios + .get(`https://programacion-tv.elpais.com/data/programas/${channel.site_id}.json`) + .then(r => r.data) + .catch(console.log) +} + +function parseStart(item) { + return dayjs.tz(item.iniDate, 'YYYY-MM-DD HH:mm:ss', 'Europe/Madrid') +} + +function parseStop(item) { + return dayjs.tz(item.endDate, 'YYYY-MM-DD HH:mm:ss', 'Europe/Madrid') +} + +function parseItems(content, channel) { + if (!content) return [] + const data = JSON.parse(content) + const channelData = data.find(i => i.idCanal === channel.site_id) + if (!channelData || !Array.isArray(channelData.programas)) return [] + + return channelData.programas +} diff --git a/sites/programacion-tv.elpais.com/programacion-tv.elpais.com.test.js b/sites/programacion-tv.elpais.com/programacion-tv.elpais.com.test.js index 1cfff4f5..543df9dc 100644 --- a/sites/programacion-tv.elpais.com/programacion-tv.elpais.com.test.js +++ b/sites/programacion-tv.elpais.com/programacion-tv.elpais.com.test.js @@ -1,71 +1,71 @@ -const { parser, url } = require('./programacion-tv.elpais.com.config.js') -const axios = require('axios') -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) - -jest.mock('axios') - -const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '3', - xmltv_id: 'La1.es' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://programacion-tv.elpais.com/data/parrilla_04102022.json') -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - axios.get.mockImplementation(url => { - if (url === 'https://programacion-tv.elpais.com/data/programas/3.json') { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/programs.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, channel }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results).toMatchObject([ - { - start: '2022-10-03T23:30:00.000Z', - stop: '2022-10-04T00:25:00.000Z', - title: 'Comerse el mundo', - sub_title: 'París', - description: - 'El chef Peña viaja hasta París, una de las capitales mundiales de la alta gastronomía. Allí visitará un viñedo muy especial en pleno corazón de la ciudad, probará los famosos caracoles, hará un queso y conocerá a chefs que llegaron a la capital gala para cumplir sus sueños y los consiguieron.', - director: ['Sergio Martín', 'Victor Arribas'], - presenter: ['Javier Peña'], - writer: ['Filippo Gravino', 'Guido Iuculano', 'Michele Pellegrini'], - actors: ['Pietro Sermonti', 'Maya Sansa', 'Ana Caterina Morariu'], - guest: ['Tobia de Angelis', 'Benedetta Porcaroli', 'Roberto Nocchi'], - producer: ['Javier Redondo'], - composer: ['Paco Musulén'], - category: 'Ocio-Cultura/Cocina', - season: 1, - episode: 23, - image: 'https://programacion-tv.elpais.com/imagenes/programas/2099957.jpg' - } - ]) -}) - -it('can handle empty guide', async () => { - const result = await parser({ - content: '', - channel - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./programacion-tv.elpais.com.config.js') +const axios = require('axios') +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) + +jest.mock('axios') + +const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '3', + xmltv_id: 'La1.es' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://programacion-tv.elpais.com/data/parrilla_04102022.json') +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + axios.get.mockImplementation(url => { + if (url === 'https://programacion-tv.elpais.com/data/programas/3.json') { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/programs.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, channel }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results).toMatchObject([ + { + start: '2022-10-03T23:30:00.000Z', + stop: '2022-10-04T00:25:00.000Z', + title: 'Comerse el mundo', + sub_title: 'París', + description: + 'El chef Peña viaja hasta París, una de las capitales mundiales de la alta gastronomía. Allí visitará un viñedo muy especial en pleno corazón de la ciudad, probará los famosos caracoles, hará un queso y conocerá a chefs que llegaron a la capital gala para cumplir sus sueños y los consiguieron.', + director: ['Sergio Martín', 'Victor Arribas'], + presenter: ['Javier Peña'], + writer: ['Filippo Gravino', 'Guido Iuculano', 'Michele Pellegrini'], + actors: ['Pietro Sermonti', 'Maya Sansa', 'Ana Caterina Morariu'], + guest: ['Tobia de Angelis', 'Benedetta Porcaroli', 'Roberto Nocchi'], + producer: ['Javier Redondo'], + composer: ['Paco Musulén'], + category: 'Ocio-Cultura/Cocina', + season: 1, + episode: 23, + image: 'https://programacion-tv.elpais.com/imagenes/programas/2099957.jpg' + } + ]) +}) + +it('can handle empty guide', async () => { + const result = await parser({ + content: '', + channel + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/programacion.tcc.com.uy/programacion.tcc.com.uy.config.js b/sites/programacion.tcc.com.uy/programacion.tcc.com.uy.config.js index 5a42c02b..b82be339 100644 --- a/sites/programacion.tcc.com.uy/programacion.tcc.com.uy.config.js +++ b/sites/programacion.tcc.com.uy/programacion.tcc.com.uy.config.js @@ -1,102 +1,102 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const API_ENDPOINT = 'https://www.tccvivo.com.uy/api/v1/navigation_filter/1575/filter/' - -module.exports = { - site: 'programacion.tcc.com.uy', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - }, - maxContentLength: 10 * 1024 * 1024 // 30Mb - }, - url: function ({ date }) { - return `${API_ENDPOINT}?cable_operator=1&emission_start=${date.format( - 'YYYY-MM-DDTHH:mm:ss[Z]' - )}&emission_end=${date.add(1, 'd').format('YYYY-MM-DDTHH:mm:ss[Z]')}&format=json` - }, - parser({ content, channel }) { - let programs = [] - let items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: parseTitle(item), - description: parseDescription(item), - categories: parseCategories(item), - date: item.year, - season: item.season_number, - episode: item.episode_number, - image: parseImage(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get( - `${API_ENDPOINT}?cable_operator=1&emission_start=${dayjs().format( - 'YYYY-MM-DDTHH:mm:ss[Z]' - )}&emission_end=${dayjs().format('YYYY-MM-DDTHH:mm:ss[Z]')}&format=json` - ) - .then(r => r.data) - .catch(console.error) - - return data.results.map(c => { - return { - lang: 'es', - site_id: c.id, - name: c.name.replace(/^\[.*\]\s/, '') - } - }) - } -} - -function parseTitle(item) { - const localized = item.localized.find(i => i.language === 'es') - - return localized ? localized.title : item.original_title -} - -function parseDescription(item) { - const localized = item.localized.find(i => i.language === 'es') - - return localized ? localized.description : null -} - -function parseCategories(item) { - return item.genres - .map(g => { - const localized = g.localized.find(i => i.language === 'es') - - return localized ? localized.name : null - }) - .filter(Boolean) -} - -function parseImage(item) { - const uri = item.images[0] ? item.images[0].image_media.file : null - - return uri ? `https:${uri}` : null -} - -function parseStart(item) { - return dayjs(item.emission_start) -} - -function parseStop(item) { - return dayjs(item.emission_end) -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.results)) return [] - const channelData = data.results.find(c => c.id == channel.site_id) - if (!channelData || !Array.isArray(channelData.events)) return [] - - return channelData.events -} +const axios = require('axios') +const dayjs = require('dayjs') + +const API_ENDPOINT = 'https://www.tccvivo.com.uy/api/v1/navigation_filter/1575/filter/' + +module.exports = { + site: 'programacion.tcc.com.uy', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + }, + maxContentLength: 10 * 1024 * 1024 // 30Mb + }, + url: function ({ date }) { + return `${API_ENDPOINT}?cable_operator=1&emission_start=${date.format( + 'YYYY-MM-DDTHH:mm:ss[Z]' + )}&emission_end=${date.add(1, 'd').format('YYYY-MM-DDTHH:mm:ss[Z]')}&format=json` + }, + parser({ content, channel }) { + let programs = [] + let items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: parseTitle(item), + description: parseDescription(item), + categories: parseCategories(item), + date: item.year, + season: item.season_number, + episode: item.episode_number, + image: parseImage(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get( + `${API_ENDPOINT}?cable_operator=1&emission_start=${dayjs().format( + 'YYYY-MM-DDTHH:mm:ss[Z]' + )}&emission_end=${dayjs().format('YYYY-MM-DDTHH:mm:ss[Z]')}&format=json` + ) + .then(r => r.data) + .catch(console.error) + + return data.results.map(c => { + return { + lang: 'es', + site_id: c.id, + name: c.name.replace(/^\[.*\]\s/, '') + } + }) + } +} + +function parseTitle(item) { + const localized = item.localized.find(i => i.language === 'es') + + return localized ? localized.title : item.original_title +} + +function parseDescription(item) { + const localized = item.localized.find(i => i.language === 'es') + + return localized ? localized.description : null +} + +function parseCategories(item) { + return item.genres + .map(g => { + const localized = g.localized.find(i => i.language === 'es') + + return localized ? localized.name : null + }) + .filter(Boolean) +} + +function parseImage(item) { + const uri = item.images[0] ? item.images[0].image_media.file : null + + return uri ? `https:${uri}` : null +} + +function parseStart(item) { + return dayjs(item.emission_start) +} + +function parseStop(item) { + return dayjs(item.emission_end) +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.results)) return [] + const channelData = data.results.find(c => c.id == channel.site_id) + if (!channelData || !Array.isArray(channelData.events)) return [] + + return channelData.events +} diff --git a/sites/programacion.tcc.com.uy/programacion.tcc.com.uy.test.js b/sites/programacion.tcc.com.uy/programacion.tcc.com.uy.test.js index f2600af1..02ce2dc9 100644 --- a/sites/programacion.tcc.com.uy/programacion.tcc.com.uy.test.js +++ b/sites/programacion.tcc.com.uy/programacion.tcc.com.uy.test.js @@ -1,76 +1,76 @@ -const { parser, url } = require('./programacion.tcc.com.uy.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('2023-02-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '212', - xmltv_id: 'MultiPremier.mx' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://www.tccvivo.com.uy/api/v1/navigation_filter/1575/filter/?cable_operator=1&emission_start=2023-02-11T00:00:00Z&emission_end=2023-02-12T00:00:00Z&format=json' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-02-10T22:45:00.000Z', - stop: '2023-02-11T00:30:00.000Z', - title: 'Meurtres à... - Temp. 3 - Episodio 3', - date: 2016, - season: 3, - episode: 3, - categories: [], - image: 'https://zpapi.zetatv.com.uy/media/images/2b45d2675389f2e4f7f6fe0655ccc968.jpg', - description: - 'Cada episodio relata un lugar y una historia diferente pero siguiendo la línea de una investigación basada en una leyenda la cual es guiada por una pareja. Estos dos personajes no son necesariamente ambos policías, pero se ven obligados a colaborar a pesar de los primeros informes difíciles.' - }) - expect(results[1]).toMatchObject({ - start: '2023-02-11T00:30:00.000Z', - stop: '2023-02-11T03:00:00.000Z', - title: 'Grandes esperanzas', - date: 1998, - season: null, - episode: null, - categories: ['Drama'], - image: 'https://zpapi.zetatv.com.uy/media/images/8cab42d88691edaa8a4001b91f809d91.jpg', - description: - 'Basada en la novela de Charles Dickens, cuenta la historia del pintor Finn que persigue obsesionado a su amor de la niñez, la bella y rica Estella. Gracias a un misterioso benefactor, Finn es enviado a Nueva York, donde se reúne con la hermosa y fría joven.' - }) - expect(results[3]).toMatchObject({ - start: '2023-02-11T05:35:00.000Z', - stop: '2023-02-11T07:45:00.000Z', - title: 'Los niños están bien', - date: 2010, - season: null, - episode: null, - categories: ['Comedia', 'Drama'], - image: 'https://zpapi.zetatv.com.uy/media/images/51684d91ed33cb9b0c1863b7a9b097e9.jpg', - description: - 'Una pareja de lesbianas conciben a un niño y una niña por inseminacion artificial. Al paso del tiempo, los chicos deciden conocer a su verdadero padre a espaldas de sus madres. Tras localizarlo intentan integrar toda una familia. Podran lograrlo?.' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')), - channel - }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./programacion.tcc.com.uy.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('2023-02-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '212', + xmltv_id: 'MultiPremier.mx' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://www.tccvivo.com.uy/api/v1/navigation_filter/1575/filter/?cable_operator=1&emission_start=2023-02-11T00:00:00Z&emission_end=2023-02-12T00:00:00Z&format=json' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-02-10T22:45:00.000Z', + stop: '2023-02-11T00:30:00.000Z', + title: 'Meurtres à... - Temp. 3 - Episodio 3', + date: 2016, + season: 3, + episode: 3, + categories: [], + image: 'https://zpapi.zetatv.com.uy/media/images/2b45d2675389f2e4f7f6fe0655ccc968.jpg', + description: + 'Cada episodio relata un lugar y una historia diferente pero siguiendo la línea de una investigación basada en una leyenda la cual es guiada por una pareja. Estos dos personajes no son necesariamente ambos policías, pero se ven obligados a colaborar a pesar de los primeros informes difíciles.' + }) + expect(results[1]).toMatchObject({ + start: '2023-02-11T00:30:00.000Z', + stop: '2023-02-11T03:00:00.000Z', + title: 'Grandes esperanzas', + date: 1998, + season: null, + episode: null, + categories: ['Drama'], + image: 'https://zpapi.zetatv.com.uy/media/images/8cab42d88691edaa8a4001b91f809d91.jpg', + description: + 'Basada en la novela de Charles Dickens, cuenta la historia del pintor Finn que persigue obsesionado a su amor de la niñez, la bella y rica Estella. Gracias a un misterioso benefactor, Finn es enviado a Nueva York, donde se reúne con la hermosa y fría joven.' + }) + expect(results[3]).toMatchObject({ + start: '2023-02-11T05:35:00.000Z', + stop: '2023-02-11T07:45:00.000Z', + title: 'Los niños están bien', + date: 2010, + season: null, + episode: null, + categories: ['Comedia', 'Drama'], + image: 'https://zpapi.zetatv.com.uy/media/images/51684d91ed33cb9b0c1863b7a9b097e9.jpg', + description: + 'Una pareja de lesbianas conciben a un niño y una niña por inseminacion artificial. Al paso del tiempo, los chicos deciden conocer a su verdadero padre a espaldas de sus madres. Tras localizarlo intentan integrar toda una familia. Podran lograrlo?.' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')), + channel + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/programetv.ro/__data__/content.html b/sites/programetv.ro/__data__/content.html new file mode 100644 index 00000000..0eba125b --- /dev/null +++ b/sites/programetv.ro/__data__/content.html @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/sites/programetv.ro/__data__/no_content.html b/sites/programetv.ro/__data__/no_content.html new file mode 100644 index 00000000..4589c8b2 --- /dev/null +++ b/sites/programetv.ro/__data__/no_content.html @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/sites/programetv.ro/programetv.ro.config.js b/sites/programetv.ro/programetv.ro.config.js index 4a14f0e5..86f34f58 100644 --- a/sites/programetv.ro/programetv.ro.config.js +++ b/sites/programetv.ro/programetv.ro.config.js @@ -1,95 +1,95 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'programetv.ro', - days: 2, - url: function ({ date, channel }) { - const daysOfWeek = { - 0: 'duminica', - 1: 'luni', - 2: 'marti', - 3: 'miercuri', - 4: 'joi', - 5: 'vineri', - 6: 'sambata' - } - const day = date.day() - - return `https://www.programetv.ro/program-tv/${channel.site_id}/${daysOfWeek[day]}/` - }, - parser: function ({ content }) { - let programs = [] - const data = parseContent(content) - if (!data || !data.shows) return programs - const items = data.shows - items.forEach(item => { - programs.push({ - title: item.title, - sub_title: item.titleOriginal, - description: item.desc || item.obs, - category: item.categories, - season: item.season || null, - episode: item.episode || null, - start: parseStart(item), - stop: parseStop(item), - url: item.url || null, - date: item.date, - rating: parseRating(item), - directors: parseDirector(item), - actors: parseActor(item), - icon: item.icon - }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - const data = await axios - .get('https://www.programetv.ro/api/station/index/') - .then(r => r.data) - .catch(console.log) - - return data.map(item => { - return { - lang: 'ro', - site_id: item.slug, - name: item.displayName - } - }) - } -} - -function parseStart(item) { - return dayjs(item.start).toJSON() -} - -function parseStop(item) { - return dayjs(item.stop).toJSON() -} - -function parseContent(content) { - const [, data] = content.match(/var pageData = ({.+});\n/) || [null, null] - - return data ? JSON.parse(data) : {} -} - -function parseDirector(item) { - return item.credits && item.credits.director ? item.credits.director : null -} - -function parseActor(item) { - return item.credits && item.credits.actor ? item.credits.actor : null -} - -function parseRating(item) { - return item.rating - ? { - system: 'CNC', - value: item.rating.toUpperCase() - } - : null -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'programetv.ro', + days: 2, + url: function ({ date, channel }) { + const daysOfWeek = { + 0: 'duminica', + 1: 'luni', + 2: 'marti', + 3: 'miercuri', + 4: 'joi', + 5: 'vineri', + 6: 'sambata' + } + const day = date.day() + + return `https://www.programetv.ro/program-tv/${channel.site_id}/${daysOfWeek[day]}/` + }, + parser: function ({ content }) { + let programs = [] + const data = parseContent(content) + if (!data || !data.shows) return programs + const items = data.shows + items.forEach(item => { + programs.push({ + title: item.title, + sub_title: item.titleOriginal, + description: item.desc || item.obs, + category: item.categories, + season: item.season || null, + episode: item.episode || null, + start: parseStart(item), + stop: parseStop(item), + url: item.url || null, + date: item.date, + rating: parseRating(item), + directors: parseDirector(item), + actors: parseActor(item), + icon: item.icon + }) + }) + + return programs + }, + async channels() { + const axios = require('axios') + const data = await axios + .get('https://www.programetv.ro/api/station/index/') + .then(r => r.data) + .catch(console.log) + + return data.map(item => { + return { + lang: 'ro', + site_id: item.slug, + name: item.displayName + } + }) + } +} + +function parseStart(item) { + return dayjs(item.start).toJSON() +} + +function parseStop(item) { + return dayjs(item.stop).toJSON() +} + +function parseContent(content) { + const [, data] = content.match(/var pageData = ({.+?});/) || [null, null] + + return data ? JSON.parse(data) : {} +} + +function parseDirector(item) { + return item.credits && item.credits.director ? item.credits.director : null +} + +function parseActor(item) { + return item.credits && item.credits.actor ? item.credits.actor : null +} + +function parseRating(item) { + return item.rating + ? { + system: 'CNC', + value: item.rating.toUpperCase() + } + : null +} diff --git a/sites/programetv.ro/programetv.ro.test.js b/sites/programetv.ro/programetv.ro.test.js index 03be7955..a5ef779a 100644 --- a/sites/programetv.ro/programetv.ro.test.js +++ b/sites/programetv.ro/programetv.ro.test.js @@ -1,68 +1,42 @@ -const { parser, url } = require('./programetv.ro.config.js') -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('2021-10-24', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'pro-tv', xmltv_id: 'ProTV.ro' } -const content = ` - - - - - -` - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe('https://www.programetv.ro/program-tv/pro-tv/duminica/') -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: '2021-11-07T05:00:00.000Z', - stop: '2021-11-07T07:59:59.000Z', - title: 'Ştirile Pro Tv', - description: - 'În fiecare zi, cele mai importante evenimente, transmisiuni LIVE, analize, anchete şi reportaje sunt la Ştirile ProTV.', - category: ['Ştiri'], - icon: 'https://www.programetv.ro/img/shows/84/54/stirile-pro-tv.png?key=Z2lfZnVial90cmFyZXZwLzAwLzAwLzA1LzE4MzgxMnktMTIwazE3MC1hLW40NTk4MW9zLmNhdA==' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: ` - - - - - - -` - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./programetv.ro.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('2021-10-24', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'pro-tv', xmltv_id: 'ProTV.ro' } + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe('https://www.programetv.ro/program-tv/pro-tv/duminica/') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: '2021-11-07T05:00:00.000Z', + stop: '2021-11-07T07:59:59.000Z', + title: 'Ştirile Pro Tv', + description: + 'În fiecare zi, cele mai importante evenimente, transmisiuni LIVE, analize, anchete şi reportaje sunt la Ştirile ProTV.', + category: ['Ştiri'], + icon: 'https://www.programetv.ro/img/shows/84/54/stirile-pro-tv.png?key=Z2lfZnVial90cmFyZXZwLzAwLzAwLzA1LzE4MzgxMnktMTIwazE3MC1hLW40NTk4MW9zLmNhdA==' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + + }) + expect(result).toMatchObject([]) +}) 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/programme-tv.net/programme-tv.net.test.js b/sites/programme-tv.net/programme-tv.net.test.js index 3f59862d..a3c2ddde 100644 --- a/sites/programme-tv.net/programme-tv.net.test.js +++ b/sites/programme-tv.net/programme-tv.net.test.js @@ -1,63 +1,63 @@ -const { parser, url, request } = require('./programme-tv.net.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('2023-11-27', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'tf1-19', - xmltv_id: 'TF1.fr' -} - -it('can generate valid url', () => { - expect(request.headers).toMatchObject({ - cookie: 'authId=b7154156fe4fb8acdb6f38e1207c6231' - }) -}) - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://www.programme-tv.net/programme/chaine/2023-11-27/programme-tf1-19.html' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - 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: '2023-11-27T00:05:00.000Z', - stop: '2023-11-27T05:30:00.000Z', - title: 'Programmes de la nuit', - category: 'Autre', - image: - 'https://www.programme-tv.net/imgre/fit/~2~program~978eb86d5b99cee0.jpg/960x540/quality/80/programmes-de-la-nuit.jpg' - }) - - expect(results[27]).toMatchObject({ - start: '2023-11-27T22:50:00.000Z', - stop: '2023-11-27T23:45:00.000Z', - title: 'Coup de foudre chez le Père Noël', - category: 'Téléfilm', - image: - 'https://www.programme-tv.net/imgre/fit/~2~program~5a4e78779c4a3fac.jpg/960x540/quality/80/coup-de-foudre-chez-le-pere-noel.jpg' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: '', - date - }) - - expect(results).toMatchObject([]) -}) +const { parser, url, request } = require('./programme-tv.net.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('2023-11-27', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'tf1-19', + xmltv_id: 'TF1.fr' +} + +it('can generate valid url', () => { + expect(request.headers).toMatchObject({ + cookie: 'authId=b7154156fe4fb8acdb6f38e1207c6231' + }) +}) + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://www.programme-tv.net/programme/chaine/2023-11-27/programme-tf1-19.html' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + 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: '2023-11-27T00:05:00.000Z', + stop: '2023-11-27T05:30:00.000Z', + title: 'Programmes de la nuit', + category: 'Autre', + image: + 'https://www.programme-tv.net/imgre/fit/~2~program~978eb86d5b99cee0.jpg/960x540/quality/80/programmes-de-la-nuit.jpg' + }) + + expect(results[27]).toMatchObject({ + start: '2023-11-27T22:50:00.000Z', + stop: '2023-11-27T23:45:00.000Z', + title: 'Coup de foudre chez le Père Noël', + category: 'Téléfilm', + image: + 'https://www.programme-tv.net/imgre/fit/~2~program~5a4e78779c4a3fac.jpg/960x540/quality/80/coup-de-foudre-chez-le-pere-noel.jpg' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: '', + date + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/programme-tv.vini.pf/__data__/content.json b/sites/programme-tv.vini.pf/__data__/content.json new file mode 100644 index 00000000..41e8bacc --- /dev/null +++ b/sites/programme-tv.vini.pf/__data__/content.json @@ -0,0 +1 @@ +{"programmes":[{"nid":"8857261","src":"https://programme-tv.vini.pf/sites/default/files/img-icones/192.png","alt":"","title":"","url":"/tf1","programmes":[{"nidP":"24162436","categorieP":"Magazine","categorieTIDP":"1033","episodeP":"-1","titreP":"Reportages découverte","heureP":"13:50","timestampDeb":1637452200,"timestampFin":1637457000,"altP":"","titleP":"","legendeP":"La coloc ne connaît pas la crise","desc":"Pour faire face à la crise du logement, aux loyers toujours plus élevés, à la solitude ou pour les gardes d'enfants, les colocations ont le vent en poupe, Pour mieux comprendre ce nouveau phénomène, une équipe a partagé le quotidien de quatre foyers : une retraitée qui héberge des étudiants, des mamans solos, enceintes, qui partagent un appartement associatif, trois générations de la même famille sur un domaine viticole et une étudiante qui intègre une colocation XXL.","srcP":"https://programme-tv.vini.pf/sites/default/files/img-icones/52ada51ed86b7e7bc11eaee83ff2192785989d77.jpg","urlP":"/reportages-decouverte-20112021-1350","width":58.333333333333,"active":false,"progression":0,"test":0,"nowphp":1637509179},{"nidP":"24162437","categorieP":"Magazine","categorieTIDP":"1033","episodeP":"-1","titreP":"Les docs du week-end","heureP":"15:10","timestampDeb":1637457000,"timestampFin":1637461800,"altP":"","titleP":"","legendeP":"Que sont-ils devenus ? L'incroyable destin des stars des émissions de télécrochet","desc":"Un documentaire français réalisé en 2019, Cindy Sander, Myriam Abel, Mario, Michal ou encore Magali Vaé ont fait les grandes heures des premières émissions de télécrochet modernes, dans les années 2000, Des années après leur passage, que reste-t-il de leur notoriété ? Comment ces candidats ont-ils vécu leur soudaine médiatisation ? Quels rapports entretenaient-ils avec les autres participants et les membres du jury, souvent intransigeants ?","srcP":"https://programme-tv.vini.pf/sites/default/files/img-icones/6e64cfbc55c1f4cbd11e3011401403d4dc08c6d2.jpg","urlP":"/les-docs-du-week-end-20112021-1510","width":41.666666666667,"active":false,"progression":0,"test":0,"nowphp":1637509179}]}]} \ No newline at end of file diff --git a/sites/programme-tv.vini.pf/__data__/content_1.json b/sites/programme-tv.vini.pf/__data__/content_1.json new file mode 100644 index 00000000..b8c311d0 --- /dev/null +++ b/sites/programme-tv.vini.pf/__data__/content_1.json @@ -0,0 +1 @@ +{"programmes":[{"nid":"8857261","src":"https://programme-tv.vini.pf/sites/default/files/img-icones/192.png","alt":"","title":"","url":"/tf1","programmes":[{"nidP":"24162437","categorieP":"Magazine","categorieTIDP":"1033","episodeP":"-1","titreP":"Les docs du week-end","heureP":"15:10","timestampDeb":1637457000,"timestampFin":1637461800,"altP":"","titleP":"","legendeP":"Que sont-ils devenus ? L'incroyable destin des stars des émissions de télécrochet","desc":"Un documentaire français réalisé en 2019, Cindy Sander, Myriam Abel, Mario, Michal ou encore Magali Vaé ont fait les grandes heures des premières émissions de télécrochet modernes, dans les années 2000, Des années après leur passage, que reste-t-il de leur notoriété ? Comment ces candidats ont-ils vécu leur soudaine médiatisation ? Quels rapports entretenaient-ils avec les autres participants et les membres du jury, souvent intransigeants ?","srcP":"https://programme-tv.vini.pf/sites/default/files/img-icones/6e64cfbc55c1f4cbd11e3011401403d4dc08c6d2.jpg","urlP":"/les-docs-du-week-end-20112021-1510","width":25,"active":false,"progression":0,"test":0,"nowphp":1637509998},{"nidP":"24162438","categorieP":"Magazine","categorieTIDP":"1033","episodeP":"-1","titreP":"50mn Inside","heureP":"16:30","timestampDeb":1637461800,"timestampFin":1637466300,"altP":"","titleP":"","legendeP":"L'actu","desc":"50'INSIDE, c'est toute l'actualité des stars résumée, chaque samedi, Le rendez-vous glamour pour retrouver toujours,,","srcP":"https://programme-tv.vini.pf/sites/default/files/img-icones/3d7e252312dacb5fb7a1a786fa0022ca1be15499.jpg","urlP":"/50mn-inside-20112021-1630","width":62.5,"active":false,"progression":0,"test":0,"nowphp":1637509998}]}]} \ No newline at end of file diff --git a/sites/programme-tv.vini.pf/__data__/content_2.json b/sites/programme-tv.vini.pf/__data__/content_2.json new file mode 100644 index 00000000..874680b9 --- /dev/null +++ b/sites/programme-tv.vini.pf/__data__/content_2.json @@ -0,0 +1 @@ +{"programmes":[{"nid":"8857261","src":"https://programme-tv.vini.pf/sites/default/files/img-icones/192.png","alt":"","title":"","url":"/tf1","programmes":[]}]} \ No newline at end of file diff --git a/sites/programme-tv.vini.pf/__data__/no_content.json b/sites/programme-tv.vini.pf/__data__/no_content.json new file mode 100644 index 00000000..874680b9 --- /dev/null +++ b/sites/programme-tv.vini.pf/__data__/no_content.json @@ -0,0 +1 @@ +{"programmes":[{"nid":"8857261","src":"https://programme-tv.vini.pf/sites/default/files/img-icones/192.png","alt":"","title":"","url":"/tf1","programmes":[]}]} \ No newline at end of file diff --git a/sites/programme-tv.vini.pf/programme-tv.vini.pf.config.js b/sites/programme-tv.vini.pf/programme-tv.vini.pf.config.js index 1e697583..1d9de342 100644 --- a/sites/programme-tv.vini.pf/programme-tv.vini.pf.config.js +++ b/sites/programme-tv.vini.pf/programme-tv.vini.pf.config.js @@ -1,90 +1,90 @@ -const dayjs = require('dayjs') -const axios = require('axios') - -const apiUrl = 'https://programme-tv.vini.pf/programmesJSON' - -module.exports = { - site: 'programme-tv.vini.pf', - days: 2, - url: apiUrl, - request: { - method: 'POST', - data({ date }) { - return { - dateDebut: `${date.subtract(10, 'h').format('YYYY-MM-DDTHH:mm:ss')}-10:00` - } - } - }, - parser: async function ({ content, channel, date }) { - const programs = [] - const items = parseItems(content, channel) - if (items.length) { - for (let hours of [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22]) { - const nextContent = await loadNextItems(date, hours) - const nextItems = parseItems(nextContent, channel) - for (let item of nextItems) { - if (!items.find(i => i.nidP === item.nidP)) { - items.push(item) - } - } - } - } - - items.forEach(item => { - programs.push({ - title: item.titreP, - description: item.desc, - category: item.categorieP, - image: item.srcP, - start: dayjs.unix(item.timestampDeb), - stop: dayjs.unix(item.timestampFin) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .post('https://programme-tv.vini.pf/programmesJSON') - .then(r => r.data) - .catch(console.log) - - return data.programmes.map(item => { - const site_id = item.url.replace('/', '') - const name = site_id.replace(/-/gi, ' ') - - return { - lang: 'fr', - site_id, - name - } - }) - } -} - -async function loadNextItems(date, hours) { - date = date.add(hours, 'h') - - return axios - .post( - apiUrl, - { - dateDebut: `${date.subtract(10, 'h').format('YYYY-MM-DDTHH:mm:ss')}-10:00` - }, - { - responseType: 'arraybuffer' - } - ) - .then(res => res.data.toString()) - .catch(console.log) -} - -function parseItems(content, channel) { - if (!content) return [] - const data = JSON.parse(content) - if (!data || !Array.isArray(data.programmes)) return [] - const channelData = data.programmes.find(i => i.url === `/${channel.site_id}`) - if (!channelData) return [] - - return channelData.programmes || [] -} +const dayjs = require('dayjs') +const axios = require('axios') + +const apiUrl = 'https://programme-tv.vini.pf/programmesJSON' + +module.exports = { + site: 'programme-tv.vini.pf', + days: 2, + url: apiUrl, + request: { + method: 'POST', + data({ date }) { + return { + dateDebut: `${date.subtract(10, 'h').format('YYYY-MM-DDTHH:mm:ss')}-10:00` + } + } + }, + parser: async function ({ content, channel, date }) { + const programs = [] + const items = parseItems(content, channel) + if (items.length) { + for (let hours of [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22]) { + const nextContent = await loadNextItems(date, hours) + const nextItems = parseItems(nextContent, channel) + for (let item of nextItems) { + if (!items.find(i => i.nidP === item.nidP)) { + items.push(item) + } + } + } + } + + items.forEach(item => { + programs.push({ + title: item.titreP, + description: item.desc, + category: item.categorieP, + image: item.srcP, + start: dayjs.unix(item.timestampDeb), + stop: dayjs.unix(item.timestampFin) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .post('https://programme-tv.vini.pf/programmesJSON') + .then(r => r.data) + .catch(console.log) + + return data.programmes.map(item => { + const site_id = item.url.replace('/', '') + const name = site_id.replace(/-/gi, ' ') + + return { + lang: 'fr', + site_id, + name + } + }) + } +} + +async function loadNextItems(date, hours) { + date = date.add(hours, 'h') + + return axios + .post( + apiUrl, + { + dateDebut: `${date.subtract(10, 'h').format('YYYY-MM-DDTHH:mm:ss')}-10:00` + }, + { + responseType: 'arraybuffer' + } + ) + .then(res => res.data.toString()) + .catch(console.log) +} + +function parseItems(content, channel) { + if (!content) return [] + const data = JSON.parse(content) + if (!data || !Array.isArray(data.programmes)) return [] + const channelData = data.programmes.find(i => i.url === `/${channel.site_id}`) + if (!channelData) return [] + + return channelData.programmes || [] +} diff --git a/sites/programme-tv.vini.pf/programme-tv.vini.pf.test.js b/sites/programme-tv.vini.pf/programme-tv.vini.pf.test.js index 013682c9..1dba5bcd 100644 --- a/sites/programme-tv.vini.pf/programme-tv.vini.pf.test.js +++ b/sites/programme-tv.vini.pf/programme-tv.vini.pf.test.js @@ -1,110 +1,107 @@ -const { parser, url, request } = require('./programme-tv.vini.pf.config.js') -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('2021-11-21', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'tf1', - xmltv_id: 'TF1.fr' -} - -it('can generate valid url', () => { - expect(url).toBe('https://programme-tv.vini.pf/programmesJSON') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request data', () => { - expect(request.data({ date })).toMatchObject({ dateDebut: '2021-11-20T14:00:00-10:00' }) -}) - -it('can parse response', done => { - axios.post.mockImplementation((url, data) => { - if (data.dateDebut === '2021-11-20T16:00:00-10:00') { - return Promise.resolve({ - data: Buffer.from( - '{"programmes":[{"nid":"8857261","src":"https://programme-tv.vini.pf/sites/default/files/img-icones/192.png","alt":"","title":"","url":"/tf1","programmes":[{"nidP":"24162437","categorieP":"Magazine","categorieTIDP":"1033","episodeP":"-1","titreP":"Les docs du week-end","heureP":"15:10","timestampDeb":1637457000,"timestampFin":1637461800,"altP":"","titleP":"","legendeP":"Que sont-ils devenus ? L\'incroyable destin des stars des émissions de télécrochet","desc":"Un documentaire français réalisé en 2019, Cindy Sander, Myriam Abel, Mario, Michal ou encore Magali Vaé ont fait les grandes heures des premières émissions de télécrochet modernes, dans les années 2000, Des années après leur passage, que reste-t-il de leur notoriété ? Comment ces candidats ont-ils vécu leur soudaine médiatisation ? Quels rapports entretenaient-ils avec les autres participants et les membres du jury, souvent intransigeants ?","srcP":"https://programme-tv.vini.pf/sites/default/files/img-icones/6e64cfbc55c1f4cbd11e3011401403d4dc08c6d2.jpg","urlP":"/les-docs-du-week-end-20112021-1510","width":25,"active":false,"progression":0,"test":0,"nowphp":1637509998},{"nidP":"24162438","categorieP":"Magazine","categorieTIDP":"1033","episodeP":"-1","titreP":"50mn Inside","heureP":"16:30","timestampDeb":1637461800,"timestampFin":1637466300,"altP":"","titleP":"","legendeP":"L\'actu","desc":"50\'INSIDE, c\'est toute l\'actualité des stars résumée, chaque samedi, Le rendez-vous glamour pour retrouver toujours,,","srcP":"https://programme-tv.vini.pf/sites/default/files/img-icones/3d7e252312dacb5fb7a1a786fa0022ca1be15499.jpg","urlP":"/50mn-inside-20112021-1630","width":62.5,"active":false,"progression":0,"test":0,"nowphp":1637509998}]}]}' - ) - }) - } else { - return Promise.resolve({ - data: Buffer.from( - '{"programmes":[{"nid":"8857261","src":"https://programme-tv.vini.pf/sites/default/files/img-icones/192.png","alt":"","title":"","url":"/tf1","programmes":[]}]}' - ) - }) - } - }) - - const content = - '{"programmes":[{"nid":"8857261","src":"https://programme-tv.vini.pf/sites/default/files/img-icones/192.png","alt":"","title":"","url":"/tf1","programmes":[{"nidP":"24162436","categorieP":"Magazine","categorieTIDP":"1033","episodeP":"-1","titreP":"Reportages découverte","heureP":"13:50","timestampDeb":1637452200,"timestampFin":1637457000,"altP":"","titleP":"","legendeP":"La coloc ne connaît pas la crise","desc":"Pour faire face à la crise du logement, aux loyers toujours plus élevés, à la solitude ou pour les gardes d\'enfants, les colocations ont le vent en poupe, Pour mieux comprendre ce nouveau phénomène, une équipe a partagé le quotidien de quatre foyers : une retraitée qui héberge des étudiants, des mamans solos, enceintes, qui partagent un appartement associatif, trois générations de la même famille sur un domaine viticole et une étudiante qui intègre une colocation XXL.","srcP":"https://programme-tv.vini.pf/sites/default/files/img-icones/52ada51ed86b7e7bc11eaee83ff2192785989d77.jpg","urlP":"/reportages-decouverte-20112021-1350","width":58.333333333333,"active":false,"progression":0,"test":0,"nowphp":1637509179},{"nidP":"24162437","categorieP":"Magazine","categorieTIDP":"1033","episodeP":"-1","titreP":"Les docs du week-end","heureP":"15:10","timestampDeb":1637457000,"timestampFin":1637461800,"altP":"","titleP":"","legendeP":"Que sont-ils devenus ? L\'incroyable destin des stars des émissions de télécrochet","desc":"Un documentaire français réalisé en 2019, Cindy Sander, Myriam Abel, Mario, Michal ou encore Magali Vaé ont fait les grandes heures des premières émissions de télécrochet modernes, dans les années 2000, Des années après leur passage, que reste-t-il de leur notoriété ? Comment ces candidats ont-ils vécu leur soudaine médiatisation ? Quels rapports entretenaient-ils avec les autres participants et les membres du jury, souvent intransigeants ?","srcP":"https://programme-tv.vini.pf/sites/default/files/img-icones/6e64cfbc55c1f4cbd11e3011401403d4dc08c6d2.jpg","urlP":"/les-docs-du-week-end-20112021-1510","width":41.666666666667,"active":false,"progression":0,"test":0,"nowphp":1637509179}]}]}' - - parser({ content, channel, date }) - .then(result => { - result = result.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(result).toMatchObject([ - { - start: '2021-11-20T23:50:00.000Z', - stop: '2021-11-21T01:10:00.000Z', - title: 'Reportages découverte', - category: 'Magazine', - description: - "Pour faire face à la crise du logement, aux loyers toujours plus élevés, à la solitude ou pour les gardes d'enfants, les colocations ont le vent en poupe, Pour mieux comprendre ce nouveau phénomène, une équipe a partagé le quotidien de quatre foyers : une retraitée qui héberge des étudiants, des mamans solos, enceintes, qui partagent un appartement associatif, trois générations de la même famille sur un domaine viticole et une étudiante qui intègre une colocation XXL.", - image: - 'https://programme-tv.vini.pf/sites/default/files/img-icones/52ada51ed86b7e7bc11eaee83ff2192785989d77.jpg' - }, - { - start: '2021-11-21T01:10:00.000Z', - stop: '2021-11-21T02:30:00.000Z', - title: 'Les docs du week-end', - category: 'Magazine', - description: - 'Un documentaire français réalisé en 2019, Cindy Sander, Myriam Abel, Mario, Michal ou encore Magali Vaé ont fait les grandes heures des premières émissions de télécrochet modernes, dans les années 2000, Des années après leur passage, que reste-t-il de leur notoriété ? Comment ces candidats ont-ils vécu leur soudaine médiatisation ? Quels rapports entretenaient-ils avec les autres participants et les membres du jury, souvent intransigeants ?', - image: - 'https://programme-tv.vini.pf/sites/default/files/img-icones/6e64cfbc55c1f4cbd11e3011401403d4dc08c6d2.jpg' - }, - { - start: '2021-11-21T02:30:00.000Z', - stop: '2021-11-21T03:45:00.000Z', - title: '50mn Inside', - category: 'Magazine', - description: - "50'INSIDE, c'est toute l'actualité des stars résumée, chaque samedi, Le rendez-vous glamour pour retrouver toujours,,", - image: - 'https://programme-tv.vini.pf/sites/default/files/img-icones/3d7e252312dacb5fb7a1a786fa0022ca1be15499.jpg' - } - ]) - done() - }) - .catch(err => { - done(err) - }) -}) - -it('can handle empty guide', done => { - parser({ - date, - channel, - content: - '{"programmes":[{"nid":"8857261","src":"https://programme-tv.vini.pf/sites/default/files/img-icones/192.png","alt":"","title":"","url":"/tf1","programmes":[]}]}' - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(err => { - done(err) - }) -}) +const { parser, url, request } = require('./programme-tv.vini.pf.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('2021-11-21', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'tf1', + xmltv_id: 'TF1.fr' +} + +it('can generate valid url', () => { + expect(url).toBe('https://programme-tv.vini.pf/programmesJSON') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request data', () => { + expect(request.data({ date })).toMatchObject({ dateDebut: '2021-11-20T14:00:00-10:00' }) +}) + +it('can parse response', done => { + axios.post.mockImplementation((url, data) => { + if (data.dateDebut === '2021-11-20T16:00:00-10:00') { + return Promise.resolve({ + data: Buffer.from(fs.readFileSync(path.resolve(__dirname, '__data__/content_1.json'))) + }) + } else { + return Promise.resolve({ + data: Buffer.from(fs.readFileSync(path.resolve(__dirname, '__data__/content_2.json'))) + }) + } +}) + + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + + parser({ content, channel, date }) + .then(result => { + result = result.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(result).toMatchObject([ + { + start: '2021-11-20T23:50:00.000Z', + stop: '2021-11-21T01:10:00.000Z', + title: 'Reportages découverte', + category: 'Magazine', + description: + "Pour faire face à la crise du logement, aux loyers toujours plus élevés, à la solitude ou pour les gardes d'enfants, les colocations ont le vent en poupe, Pour mieux comprendre ce nouveau phénomène, une équipe a partagé le quotidien de quatre foyers : une retraitée qui héberge des étudiants, des mamans solos, enceintes, qui partagent un appartement associatif, trois générations de la même famille sur un domaine viticole et une étudiante qui intègre une colocation XXL.", + image: + 'https://programme-tv.vini.pf/sites/default/files/img-icones/52ada51ed86b7e7bc11eaee83ff2192785989d77.jpg' + }, + { + start: '2021-11-21T01:10:00.000Z', + stop: '2021-11-21T02:30:00.000Z', + title: 'Les docs du week-end', + category: 'Magazine', + description: + 'Un documentaire français réalisé en 2019, Cindy Sander, Myriam Abel, Mario, Michal ou encore Magali Vaé ont fait les grandes heures des premières émissions de télécrochet modernes, dans les années 2000, Des années après leur passage, que reste-t-il de leur notoriété ? Comment ces candidats ont-ils vécu leur soudaine médiatisation ? Quels rapports entretenaient-ils avec les autres participants et les membres du jury, souvent intransigeants ?', + image: + 'https://programme-tv.vini.pf/sites/default/files/img-icones/6e64cfbc55c1f4cbd11e3011401403d4dc08c6d2.jpg' + }, + { + start: '2021-11-21T02:30:00.000Z', + stop: '2021-11-21T03:45:00.000Z', + title: '50mn Inside', + category: 'Magazine', + description: + "50'INSIDE, c'est toute l'actualité des stars résumée, chaque samedi, Le rendez-vous glamour pour retrouver toujours,,", + image: + 'https://programme-tv.vini.pf/sites/default/files/img-icones/3d7e252312dacb5fb7a1a786fa0022ca1be15499.jpg' + } + ]) + done() + }) + .catch(err => { + done(err) + }) +}) + +it('can handle empty guide', done => { + parser({ + date, + channel, + content: + '' + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(err => { + done(err) + }) +}) diff --git a/sites/programme.tvb.com/programme.tvb.com.config.js b/sites/programme.tvb.com/programme.tvb.com.config.js index 87c3580e..2b1e5bc5 100644 --- a/sites/programme.tvb.com/programme.tvb.com.config.js +++ b/sites/programme.tvb.com/programme.tvb.com.config.js @@ -1,92 +1,92 @@ -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) - -const tz = 'Asia/Hong_Kong' - -module.exports = { - site: 'programme.tvb.com', - days: 2, - url({ channel, date, time = null }) { - return `https://programme.tvb.com/api/schedule?input_date=${date.format( - 'YYYYMMDD' - )}&network_code=${channel.site_id}&_t=${time ? time : parseInt(Date.now() / 1000)}` - }, - parser({ content, channel, date }) { - const programs = [] - const data = content ? JSON.parse(content) : {} - if (Array.isArray(data.data?.list)) { - for (const d of data.data.list) { - if (Array.isArray(d.schedules)) { - const schedules = d.schedules.filter(s => s.network_code === channel.site_id) - schedules.forEach((s, i) => { - const start = dayjs.tz(s.event_datetime, 'YYYY-MM-DD HH:mm:ss', tz) - let stop - if (i < schedules.length - 1) { - stop = dayjs.tz(schedules[i + 1].event_datetime, 'YYYY-MM-DD HH:mm:ss', tz) - } else { - stop = date.add(1, 'd') - } - programs.push({ - title: channel.lang === 'en' ? s.en_programme_title : s.programme_title, - description: channel.lang === 'en' ? s.en_synopsis : s.synopsis, - start, - stop - }) - }) - } - } - } - - return programs - }, - async channels({ lang = 'en' }) { - const channels = [] - const axios = require('axios') - const base = 'https://programme.tvb.com' - const queues = [base] - while (true) { - if (queues.length) { - const url = queues.shift() - const content = await axios - .get(url) - .then(response => response.data) - .catch(console.error) - if (content) { - const assets = content.match(/assets\/index\.([a-z0-9]+)\.js/g) - if (assets) { - queues.push(...assets.map(a => base + '/' + a)) - } else { - const metadata = content.match(/e=(\[(.*?)\])/) - if (metadata) { - const infos = eval(metadata[1]) - if (Array.isArray(infos)) { - infos - .filter(a => a.code.length) - .map(a => { - channels.push({ - lang, - site_id: a.code, - name: lang === 'en' ? a.nameEn : a.name - }) - }) - break - } - } - } - if (queues.length) { - continue - } - } - } - break - } - - return channels - } -} +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) + +const tz = 'Asia/Hong_Kong' + +module.exports = { + site: 'programme.tvb.com', + days: 2, + url({ channel, date, time = null }) { + return `https://programme.tvb.com/api/schedule?input_date=${date.format( + 'YYYYMMDD' + )}&network_code=${channel.site_id}&_t=${time ? time : parseInt(Date.now() / 1000)}` + }, + parser({ content, channel, date }) { + const programs = [] + const data = content ? JSON.parse(content) : {} + if (Array.isArray(data.data?.list)) { + for (const d of data.data.list) { + if (Array.isArray(d.schedules)) { + const schedules = d.schedules.filter(s => s.network_code === channel.site_id) + schedules.forEach((s, i) => { + const start = dayjs.tz(s.event_datetime, 'YYYY-MM-DD HH:mm:ss', tz) + let stop + if (i < schedules.length - 1) { + stop = dayjs.tz(schedules[i + 1].event_datetime, 'YYYY-MM-DD HH:mm:ss', tz) + } else { + stop = date.add(1, 'd') + } + programs.push({ + title: channel.lang === 'en' ? s.en_programme_title : s.programme_title, + description: channel.lang === 'en' ? s.en_synopsis : s.synopsis, + start, + stop + }) + }) + } + } + } + + return programs + }, + async channels({ lang = 'en' }) { + const channels = [] + const axios = require('axios') + const base = 'https://programme.tvb.com' + const queues = [base] + while (true) { + if (queues.length) { + const url = queues.shift() + const content = await axios + .get(url) + .then(response => response.data) + .catch(console.error) + if (content) { + const assets = content.match(/assets\/index\.([a-z0-9]+)\.js/g) + if (assets) { + queues.push(...assets.map(a => base + '/' + a)) + } else { + const metadata = content.match(/e=(\[(.*?)\])/) + if (metadata) { + const infos = eval(metadata[1]) + if (Array.isArray(infos)) { + infos + .filter(a => a.code.length) + .map(a => { + channels.push({ + lang, + site_id: a.code, + name: lang === 'en' ? a.nameEn : a.name + }) + }) + break + } + } + } + if (queues.length) { + continue + } + } + } + break + } + + return channels + } +} diff --git a/sites/programme.tvb.com/programme.tvb.com.test.js b/sites/programme.tvb.com/programme.tvb.com.test.js index 558f4600..ce8fda7e 100644 --- a/sites/programme.tvb.com/programme.tvb.com.test.js +++ b/sites/programme.tvb.com/programme.tvb.com.test.js @@ -1,64 +1,64 @@ -const { parser, url } = require('./programme.tvb.com.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 content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) -const date = dayjs.utc('2024-12-06', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'J', - xmltv_id: 'Jade.hk', - lang: 'en' -} - -it('can generate valid url', () => { - const time = 1733491000 - expect(url({ channel, date, time })).toBe( - 'https://programme.tvb.com/api/schedule?input_date=20241206&network_code=J&_t=1733491000' - ) -}) - -it('can parse response (en)', () => { - const results = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(3) - expect(results[1]).toMatchObject({ - start: '2024-12-06T15:55:00.000Z', - stop: '2024-12-06T16:55:00.000Z', - title: 'Line Walker: Bull Fight#16[Can][PG]' - }) -}) - -it('can parse response (zh)', () => { - const results = parser({ content, channel: { ...channel, lang: 'zh' }, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(3) - expect(results[1]).toMatchObject({ - start: '2024-12-06T15:55:00.000Z', - stop: '2024-12-06T16:55:00.000Z', - title: '使徒行者3#16[粵][PG]', - description: - '文鼎從淑梅手上救走大聖爺兒子,大聖爺還恩於歡喜,答允支持九指強。崇聯社定下選舉日子,恰巧是韋傑出獄之日,頭目們顧念舊日恩義,紛紛轉投浩洋。浩洋帶亞希逛傢俬店,憧憬二人未來。亞希向家強承認愛上浩洋,要求退出臥底任務。作榮與歡喜暗中會面,將國際犯罪組織「永恆幫」情報交給他。阿火遭家強出賣,到沐足店搶錢。家強逮住阿火,惟被合星誤會而受拘捕。家強把正植遺下的頸鏈和學生證交還,合星意識到家強已知悉正植身世。' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '', - date - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./programme.tvb.com.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 content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) +const date = dayjs.utc('2024-12-06', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'J', + xmltv_id: 'Jade.hk', + lang: 'en' +} + +it('can generate valid url', () => { + const time = 1733491000 + expect(url({ channel, date, time })).toBe( + 'https://programme.tvb.com/api/schedule?input_date=20241206&network_code=J&_t=1733491000' + ) +}) + +it('can parse response (en)', () => { + const results = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(3) + expect(results[1]).toMatchObject({ + start: '2024-12-06T15:55:00.000Z', + stop: '2024-12-06T16:55:00.000Z', + title: 'Line Walker: Bull Fight#16[Can][PG]' + }) +}) + +it('can parse response (zh)', () => { + const results = parser({ content, channel: { ...channel, lang: 'zh' }, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(3) + expect(results[1]).toMatchObject({ + start: '2024-12-06T15:55:00.000Z', + stop: '2024-12-06T16:55:00.000Z', + title: '使徒行者3#16[粵][PG]', + description: + '文鼎從淑梅手上救走大聖爺兒子,大聖爺還恩於歡喜,答允支持九指強。崇聯社定下選舉日子,恰巧是韋傑出獄之日,頭目們顧念舊日恩義,紛紛轉投浩洋。浩洋帶亞希逛傢俬店,憧憬二人未來。亞希向家強承認愛上浩洋,要求退出臥底任務。作榮與歡喜暗中會面,將國際犯罪組織「永恆幫」情報交給他。阿火遭家強出賣,到沐足店搶錢。家強逮住阿火,惟被合星誤會而受拘捕。家強把正植遺下的頸鏈和學生證交還,合星意識到家強已知悉正植身世。' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '', + date + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/programtv.onet.pl/__data__/content.html b/sites/programtv.onet.pl/__data__/content.html new file mode 100644 index 00000000..fa2d325b --- /dev/null +++ b/sites/programtv.onet.pl/__data__/content.html @@ -0,0 +1 @@ +
    13th Street
    • 03:20
      Law & Order, odc. 15: Letzte Worte Krimiserie

      Bei einer Reality-TV-Show stirbt einer der Teilnehmer. Zunächst tappen Briscoe (Jerry Orbach) und Green (Jesse L....

    • 23:30
      Navy CIS, odc. 1: New Orleans Krimiserie

      Der Abgeordnete Dan McLane, ein ehemaliger Vorgesetzter von Gibbs, wird in New Orleans ermordet. In den 90er Jahren...

    • 01:00
      Navy CIS: L.A, odc. 13: High Society Krimiserie

      Die Zahl der Drogentoten ist gestiegen. Das Team des NCIS glaubt, dass sich Terroristen durch den zunehmenden...

    \ No newline at end of file diff --git a/sites/programtv.onet.pl/__data__/no_content.html b/sites/programtv.onet.pl/__data__/no_content.html new file mode 100644 index 00000000..6fedfd4c --- /dev/null +++ b/sites/programtv.onet.pl/__data__/no_content.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sites/programtv.onet.pl/programtv.onet.pl.config.js b/sites/programtv.onet.pl/programtv.onet.pl.config.js index 64ff12df..ecb95773 100644 --- a/sites/programtv.onet.pl/programtv.onet.pl.config.js +++ b/sites/programtv.onet.pl/programtv.onet.pl.config.js @@ -1,89 +1,89 @@ -const cheerio = require('cheerio') -const { DateTime } = require('luxon') - -module.exports = { - delay: 5000, - site: 'programtv.onet.pl', - days: 2, - url: function ({ date, channel }) { - const currDate = DateTime.now().toUTC().startOf('day') - const day = date.diff(currDate, 'd') - - return `https://programtv.onet.pl/program-tv/${channel.site_id}?dzien=${day}` - }, - parser: function ({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ hours: 1 }) - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - category: parseCategory($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - const data = await axios - .get('https://programtv.onet.pl/stacje') - .then(r => r.data) - .catch(console.log) - - let channels = [] - - const $ = cheerio.load(data) - $('ul.channelList a').each((i, el) => { - const name = $(el).text() - const url = $(el).attr('href') - const [, site_id] = url.match(/^\/program-tv\/(.*)$/i) - - channels.push({ - lang: 'pl', - site_id, - name - }) - }) - - return channels - } -} - -function parseStart($item, date) { - const timeString = $item('.hours > .hour').text() - const dateString = `${date.format('MM/DD/YYYY')} ${timeString}` - - return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH:mm', { zone: 'Europe/Warsaw' }).toUTC() -} - -function parseCategory($item) { - return $item('.titles > .type').text() -} - -function parseDescription($item) { - return $item('.titles > p').text().trim() -} - -function parseTitle($item) { - return $item('.titles > a').text().trim() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#channelTV > section > div.emissions > ul > li').toArray() -} +const cheerio = require('cheerio') +const { DateTime } = require('luxon') + +module.exports = { + delay: 5000, + site: 'programtv.onet.pl', + days: 2, + url: function ({ date, channel }) { + const currDate = DateTime.now().toUTC().startOf('day') + const day = date.diff(currDate, 'd') + + return `https://programtv.onet.pl/program-tv/${channel.site_id}?dzien=${day}` + }, + parser: function ({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ hours: 1 }) + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + category: parseCategory($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const axios = require('axios') + const data = await axios + .get('https://programtv.onet.pl/stacje') + .then(r => r.data) + .catch(console.log) + + let channels = [] + + const $ = cheerio.load(data) + $('ul.channelList a').each((i, el) => { + const name = $(el).text() + const url = $(el).attr('href') + const [, site_id] = url.match(/^\/program-tv\/(.*)$/i) + + channels.push({ + lang: 'pl', + site_id, + name + }) + }) + + return channels + } +} + +function parseStart($item, date) { + const timeString = $item('.hours > .hour').text() + const dateString = `${date.format('MM/DD/YYYY')} ${timeString}` + + return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH:mm', { zone: 'Europe/Warsaw' }).toUTC() +} + +function parseCategory($item) { + return $item('.titles > .type').text() +} + +function parseDescription($item) { + return $item('.titles > p').text().trim() +} + +function parseTitle($item) { + return $item('.titles > a').text().trim() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('#channelTV > section > div.emissions > ul > li').toArray() +} diff --git a/sites/programtv.onet.pl/programtv.onet.pl.test.js b/sites/programtv.onet.pl/programtv.onet.pl.test.js index 68f3a34a..03bbd481 100644 --- a/sites/programtv.onet.pl/programtv.onet.pl.test.js +++ b/sites/programtv.onet.pl/programtv.onet.pl.test.js @@ -1,75 +1,76 @@ -const MockDate = require('mockdate') -const { parser, url } = require('./programtv.onet.pl.config.js') -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('2021-11-24', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '13th-street-250', - xmltv_id: '13thStreet.de' -} -const content = - '
    13th Street
    • 03:20
      Law & Order, odc. 15: Letzte Worte Krimiserie

      Bei einer Reality-TV-Show stirbt einer der Teilnehmer. Zunächst tappen Briscoe (Jerry Orbach) und Green (Jesse L....

    • 23:30
      Navy CIS, odc. 1: New Orleans Krimiserie

      Der Abgeordnete Dan McLane, ein ehemaliger Vorgesetzter von Gibbs, wird in New Orleans ermordet. In den 90er Jahren...

    • 01:00
      Navy CIS: L.A, odc. 13: High Society Krimiserie

      Die Zahl der Drogentoten ist gestiegen. Das Team des NCIS glaubt, dass sich Terroristen durch den zunehmenden...

    ' - -it('can generate valid url', () => { - MockDate.set(dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d')) - expect(url({ channel, date })).toBe( - 'https://programtv.onet.pl/program-tv/13th-street-250?dzien=0' - ) - MockDate.reset() -}) - -it('can generate valid url for next day', () => { - MockDate.set(dayjs.utc('2021-11-23', 'YYYY-MM-DD').startOf('d')) - expect(url({ channel, date })).toBe( - 'https://programtv.onet.pl/program-tv/13th-street-250?dzien=1' - ) - MockDate.reset() -}) - -it('can parse response', () => { - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-24T02:20:00.000Z', - stop: '2021-11-24T22:30:00.000Z', - title: 'Law & Order, odc. 15: Letzte Worte', - category: 'Krimiserie', - description: - 'Bei einer Reality-TV-Show stirbt einer der Teilnehmer. Zunächst tappen Briscoe (Jerry Orbach) und Green (Jesse L....' - }, - { - start: '2021-11-24T22:30:00.000Z', - stop: '2021-11-25T00:00:00.000Z', - title: 'Navy CIS, odc. 1: New Orleans', - category: 'Krimiserie', - description: - 'Der Abgeordnete Dan McLane, ein ehemaliger Vorgesetzter von Gibbs, wird in New Orleans ermordet. In den 90er Jahren...' - }, - { - start: '2021-11-25T00:00:00.000Z', - stop: '2021-11-25T01:00:00.000Z', - title: 'Navy CIS: L.A, odc. 13: High Society', - category: 'Krimiserie', - description: - 'Die Zahl der Drogentoten ist gestiegen. Das Team des NCIS glaubt, dass sich Terroristen durch den zunehmenden...' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +const MockDate = require('mockdate') +const { parser, url } = require('./programtv.onet.pl.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('2021-11-24', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '13th-street-250', + xmltv_id: '13thStreet.de' +} + +it('can generate valid url', () => { + MockDate.set(dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d')) + expect(url({ channel, date })).toBe( + 'https://programtv.onet.pl/program-tv/13th-street-250?dzien=0' + ) + MockDate.reset() +}) + +it('can generate valid url for next day', () => { + MockDate.set(dayjs.utc('2021-11-23', 'YYYY-MM-DD').startOf('d')) + expect(url({ channel, date })).toBe( + 'https://programtv.onet.pl/program-tv/13th-street-250?dzien=1' + ) + MockDate.reset() +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-24T02:20:00.000Z', + stop: '2021-11-24T22:30:00.000Z', + title: 'Law & Order, odc. 15: Letzte Worte', + category: 'Krimiserie', + description: + 'Bei einer Reality-TV-Show stirbt einer der Teilnehmer. Zunächst tappen Briscoe (Jerry Orbach) und Green (Jesse L....' + }, + { + start: '2021-11-24T22:30:00.000Z', + stop: '2021-11-25T00:00:00.000Z', + title: 'Navy CIS, odc. 1: New Orleans', + category: 'Krimiserie', + description: + 'Der Abgeordnete Dan McLane, ein ehemaliger Vorgesetzter von Gibbs, wird in New Orleans ermordet. In den 90er Jahren...' + }, + { + start: '2021-11-25T00:00:00.000Z', + stop: '2021-11-25T01:00:00.000Z', + title: 'Navy CIS: L.A, odc. 13: High Society', + category: 'Krimiserie', + description: + 'Die Zahl der Drogentoten ist gestiegen. Das Team des NCIS glaubt, dass sich Terroristen durch den zunehmenden...' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/raiplay.it/__data__/content.json b/sites/raiplay.it/__data__/content.json new file mode 100644 index 00000000..95d30306 --- /dev/null +++ b/sites/raiplay.it/__data__/content.json @@ -0,0 +1 @@ +{ "id": "Page-e120a813-1b92-4057-a214-15943d95aa68", "title": "Pagina Palinsesto", "channel": "Rai 2", "date": "03-05-2022", "events": [ { "id": "ContentItem-2f81030d-803b-456a-9ea5-40233234fd9d", "name": "The Good Doctor S3E5 - La prima volta", "episode_title": "La prima volta", "episode": "5", "season": "3", "description": "Shaun affronta il suo primo intervento. Il caso si rivela complicato e, nonostante Shaun abbia un'idea geniale, sarà Andrews a portare a termine l'operazione.", "channel": "Rai 2", "date": "03/05/2022", "hour": "19:40", "duration": "00:50:00", "duration_in_minutes": "50 min", "path_id": "", "weblink": "", "event_weblink": "/dirette/rai2/The-Good-Doctor-S3E5---La-prima-volta-2f81030d-803b-456a-9ea5-40233234fd9d.html", "has_video": false, "image": "/dl/img/2020/03/09/1583748471860_dddddd.jpg", "playlist_id": "11430689", "program": { "name": "The Good Doctor", "path_id": "/programmi/thegooddoctor.json", "info_url": "/programmi/info/757edeac-6fff-4dea-afcd-0bcb39f9ea83.json", "weblink": "/programmi/thegooddoctor" } } ], "track_info": { "id": "", "domain": "raiplay", "platform": "[platform]", "media_type": "", "page_type": "", "editor": "raiplay", "year": "2019", "edit_year": "", "section": "guida tv", "sub_section": "rai 2", "content": "guida tv", "title": "", "channel": "", "date": "2019-09-08", "typology": "", "genres": [], "sub_genres": [], "program_title": "", "program_typology": "", "program_genres": [], "program_sub_genres": [], "edition": "", "season": "", "episode_number": "", "episode_title": "", "form": "", "listaDateMo": [], "dfp": {} }} \ No newline at end of file diff --git a/sites/raiplay.it/__data__/no_content.json b/sites/raiplay.it/__data__/no_content.json new file mode 100644 index 00000000..33ba57bf --- /dev/null +++ b/sites/raiplay.it/__data__/no_content.json @@ -0,0 +1 @@ +{"events":[],"total":0} \ No newline at end of file diff --git a/sites/raiplay.it/raiplay.it.config.js b/sites/raiplay.it/raiplay.it.config.js index 4030d21d..b910d8c7 100644 --- a/sites/raiplay.it/raiplay.it.config.js +++ b/sites/raiplay.it/raiplay.it.config.js @@ -1,79 +1,79 @@ -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(customParseFormat) -dayjs.extend(timezone) - -module.exports = { - site: 'raiplay.it', - days: 2, - url: function ({ date, channel }) { - return `https://www.raiplay.it/palinsesto/app/${channel.site_id}/${date.format( - 'DD-MM-YYYY' - )}.json` - }, - parser: function ({ content, date }) { - const programs = [] - const data = JSON.parse(content) - if (!data.events) return programs - - data.events.forEach(item => { - if (!item.name || !item.hour || !item.duration_in_minutes) return - const start = parseStart(item, date) - const duration = parseInt(item.duration_in_minutes) - const stop = start.add(duration, 'm') - - programs.push({ - title: item.name || item.program.name, - description: item.description, - season: parseSeason(item), - episode: parseEpisode(item), - sub_title: item['episode_title'] || null, - url: parseURL(item), - start, - stop, - image: parseImage(item) - }) - }) - - return programs - } -} - -function parseStart(item, date) { - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${item.hour}`, 'YYYY-MM-DD HH:mm', 'Europe/Rome') -} - -function parseImage(item) { - let cover = null - if (item.image) { - cover = `https://www.raiplay.it${item.image}` - } - return cover -} - -function parseURL(item) { - let url = null - if (item.weblink) { - url = `https://www.raiplay.it${item.weblink}` - } - if (item.event_weblink) { - url = `https://www.raiplay.it${item.event_weblink}` - } - return url -} - -function parseSeason(item) { - if (!item.season) return null - if (String(item.season).length > 2) return null - return item.season -} - -function parseEpisode(item) { - if (!item.episode) return null - if (String(item.episode).length > 3) return null - return item.episode -} +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(customParseFormat) +dayjs.extend(timezone) + +module.exports = { + site: 'raiplay.it', + days: 2, + url: function ({ date, channel }) { + return `https://www.raiplay.it/palinsesto/app/${channel.site_id}/${date.format( + 'DD-MM-YYYY' + )}.json` + }, + parser: function ({ content, date }) { + const programs = [] + const data = JSON.parse(content) + if (!data.events) return programs + + data.events.forEach(item => { + if (!item.name || !item.hour || !item.duration_in_minutes) return + const start = parseStart(item, date) + const duration = parseInt(item.duration_in_minutes) + const stop = start.add(duration, 'm') + + programs.push({ + title: item.name || item.program.name, + description: item.description, + season: parseSeason(item), + episode: parseEpisode(item), + sub_title: item['episode_title'] || null, + url: parseURL(item), + start, + stop, + image: parseImage(item) + }) + }) + + return programs + } +} + +function parseStart(item, date) { + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${item.hour}`, 'YYYY-MM-DD HH:mm', 'Europe/Rome') +} + +function parseImage(item) { + let cover = null + if (item.image) { + cover = `https://www.raiplay.it${item.image}` + } + return cover +} + +function parseURL(item) { + let url = null + if (item.weblink) { + url = `https://www.raiplay.it${item.weblink}` + } + if (item.event_weblink) { + url = `https://www.raiplay.it${item.event_weblink}` + } + return url +} + +function parseSeason(item) { + if (!item.season) return null + if (String(item.season).length > 2) return null + return item.season +} + +function parseEpisode(item) { + if (!item.episode) return null + if (String(item.episode).length > 3) return null + return item.episode +} diff --git a/sites/raiplay.it/raiplay.it.test.js b/sites/raiplay.it/raiplay.it.test.js index 057ca8bf..27bed4b2 100644 --- a/sites/raiplay.it/raiplay.it.test.js +++ b/sites/raiplay.it/raiplay.it.test.js @@ -1,51 +1,50 @@ -// npm run grab --- --site=raiplay.it - -const { parser, url } = require('./raiplay.it.config.js') -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('2022-05-03', 'YYYY-MM-DD') -const channel = { - site_id: 'rai-2', - xmltv_id: 'Rai2.it' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://www.raiplay.it/palinsesto/app/rai-2/03-05-2022.json') -}) - -it('can parse response', () => { - const content = - '{ "id": "Page-e120a813-1b92-4057-a214-15943d95aa68", "title": "Pagina Palinsesto", "channel": "Rai 2", "date": "03-05-2022", "events": [ { "id": "ContentItem-2f81030d-803b-456a-9ea5-40233234fd9d", "name": "The Good Doctor S3E5 - La prima volta", "episode_title": "La prima volta", "episode": "5", "season": "3", "description": "Shaun affronta il suo primo intervento. Il caso si rivela complicato e, nonostante Shaun abbia un\'idea geniale, sarà Andrews a portare a termine l\'operazione.", "channel": "Rai 2", "date": "03/05/2022", "hour": "19:40", "duration": "00:50:00", "duration_in_minutes": "50 min", "path_id": "", "weblink": "", "event_weblink": "/dirette/rai2/The-Good-Doctor-S3E5---La-prima-volta-2f81030d-803b-456a-9ea5-40233234fd9d.html", "has_video": false, "image": "/dl/img/2020/03/09/1583748471860_dddddd.jpg", "playlist_id": "11430689", "program": { "name": "The Good Doctor", "path_id": "/programmi/thegooddoctor.json", "info_url": "/programmi/info/757edeac-6fff-4dea-afcd-0bcb39f9ea83.json", "weblink": "/programmi/thegooddoctor" } } ], "track_info": { "id": "", "domain": "raiplay", "platform": "[platform]", "media_type": "", "page_type": "", "editor": "raiplay", "year": "2019", "edit_year": "", "section": "guida tv", "sub_section": "rai 2", "content": "guida tv", "title": "", "channel": "", "date": "2019-09-08", "typology": "", "genres": [], "sub_genres": [], "program_title": "", "program_typology": "", "program_genres": [], "program_sub_genres": [], "edition": "", "season": "", "episode_number": "", "episode_title": "", "form": "", "listaDateMo": [], "dfp": {} }}' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-05-03T17:40:00.000Z', - stop: '2022-05-03T18:30:00.000Z', - title: 'The Good Doctor S3E5 - La prima volta', - description: - "Shaun affronta il suo primo intervento. Il caso si rivela complicato e, nonostante Shaun abbia un'idea geniale, sarà Andrews a portare a termine l'operazione.", - season: '3', - episode: '5', - sub_title: 'La prima volta', - image: 'https://www.raiplay.it/dl/img/2020/03/09/1583748471860_dddddd.jpg', - url: 'https://www.raiplay.it/dirette/rai2/The-Good-Doctor-S3E5---La-prima-volta-2f81030d-803b-456a-9ea5-40233234fd9d.html' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '{"events":[],"total":0}' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./raiplay.it.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('2022-05-03', 'YYYY-MM-DD') +const channel = { + site_id: 'rai-2', + xmltv_id: 'Rai2.it' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://www.raiplay.it/palinsesto/app/rai-2/03-05-2022.json') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-05-03T17:40:00.000Z', + stop: '2022-05-03T18:30:00.000Z', + title: 'The Good Doctor S3E5 - La prima volta', + description: + "Shaun affronta il suo primo intervento. Il caso si rivela complicato e, nonostante Shaun abbia un'idea geniale, sarà Andrews a portare a termine l'operazione.", + season: '3', + episode: '5', + sub_title: 'La prima volta', + image: 'https://www.raiplay.it/dl/img/2020/03/09/1583748471860_dddddd.jpg', + url: 'https://www.raiplay.it/dirette/rai2/The-Good-Doctor-S3E5---La-prima-volta-2f81030d-803b-456a-9ea5-40233234fd9d.html' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/reportv.com.ar/reportv.com.ar.config.js b/sites/reportv.com.ar/reportv.com.ar.config.js index 51c89d23..0016fd8d 100644 --- a/sites/reportv.com.ar/reportv.com.ar.config.js +++ b/sites/reportv.com.ar/reportv.com.ar.config.js @@ -1,170 +1,170 @@ -require('dayjs/locale/es') -const axios = require('axios') -const dayjs = require('dayjs') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const _ = require('lodash') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'reportv.com.ar', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - }, - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - data({ channel, date }) { - const formData = new URLSearchParams() - formData.append('idSenial', channel.site_id) - formData.append('Alineacion', '2694') - formData.append('DiaDesde', date.format('YYYY/MM/DD')) - formData.append('HoraDesde', '00:00:00') - - return formData - } - }, - url: 'https://www.reportv.com.ar/buscador/ProgXSenial.php', - parser: async function ({ content, date }) { - let programs = [] - const items = parseItems(content, date) - for (let item of items) { - const $item = cheerio.load(item) - const start = parseStart($item, date) - const duration = parseDuration($item) - const stop = start.add(duration, 's') - const details = await loadProgramDetails($item) - programs.push({ - title: parseTitle($item), - category: parseCategory($item), - image: details.image, - description: details.description, - directors: details.directors, - actors: details.actors, - start, - stop - }) - } - - return programs - }, - async channels() { - const content = await axios - .get('https://www.reportv.com.ar/buscador/Buscador.php?aid=2694') - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(content) - const items = $('#tr_home_2 > td:nth-child(1) > select > option').toArray() - - return items.map(item => { - return { - lang: 'es', - site_id: $(item).attr('value'), - name: $(item).text() - } - }) - } -} - -async function loadProgramDetails($item) { - const onclick = $item('*').attr('onclick') - const regexp = /detallePrograma\((\d+),(\d+),(\d+),(\d+),'([^']+)'\);/g - const match = [...onclick.matchAll(regexp)] - const [, id, idc, id_alineacion, idp, title] = match[0] - if (!id || !idc || !id_alineacion || !idp || !title) return Promise.resolve({}) - const formData = new URLSearchParams() - formData.append('id', id) - formData.append('idc', idc) - formData.append('id_alineacion', id_alineacion) - formData.append('idp', idp) - formData.append('title', title) - const content = await axios - .post('https://www.reportv.com.ar/buscador/DetallePrograma.php', formData) - .then(r => r.data.toString()) - .catch(console.error) - if (!content) return Promise.resolve({}) - - const $ = cheerio.load(content) - - return Promise.resolve({ - image: parseImage($), - actors: parseActors($), - directors: parseDirectors($), - description: parseDescription($) - }) -} - -function parseActors($) { - const section = $('#Ficha > div') - .html() - .split('
    ') - .find(str => str.includes('Actores:')) - if (!section) return null - const $section = cheerio.load(section) - - return $section('span') - .map((i, el) => $(el).text().trim()) - .get() -} - -function parseDirectors($) { - const section = $('#Ficha > div') - .html() - .split('
    ') - .find(str => str.includes('Directores:')) - if (!section) return null - const $section = cheerio.load(section) - - return $section('span') - .map((i, el) => $(el).text().trim()) - .get() -} - -function parseDescription($) { - return $('#Sinopsis > div').text().trim() -} - -function parseImage($) { - const src = $('#ImgProg').attr('src') - const url = new URL(src, 'https://www.reportv.com.ar/buscador/') - - return url.href -} - -function parseTitle($item) { - const [, title] = $item('div:nth-child(1) > span').text().split(' - ') - - return title -} - -function parseCategory($item) { - return $item('div:nth-child(3) > span').text() -} - -function parseStart($item, date) { - const [time] = $item('div:nth-child(1) > span').text().split(' - ') - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'America/Caracas') -} - -function parseDuration($item) { - const [hh, mm, ss] = $item('div:nth-child(4) > span').text().split(':') - - return parseInt(hh) * 3600 + parseInt(mm) * 60 + parseInt(ss) -} - -function parseItems(content, date) { - if (!content) return [] - const $ = cheerio.load(content) - const d = _.startCase(date.locale('es').format('DD MMMM YYYY')) - - return $(`.trProg[title*="${d}"]`).toArray() -} +require('dayjs/locale/es') +const axios = require('axios') +const dayjs = require('dayjs') +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('lodash.startcase') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'reportv.com.ar', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + }, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + data({ channel, date }) { + const formData = new URLSearchParams() + formData.append('idSenial', channel.site_id) + formData.append('Alineacion', '2694') + formData.append('DiaDesde', date.format('YYYY/MM/DD')) + formData.append('HoraDesde', '00:00:00') + + return formData + } + }, + url: 'https://www.reportv.com.ar/buscador/ProgXSenial.php', + parser: async function ({ content, date }) { + let programs = [] + const items = parseItems(content, date) + for (let item of items) { + const $item = cheerio.load(item) + const start = parseStart($item, date) + const duration = parseDuration($item) + const stop = start.add(duration, 's') + const details = await loadProgramDetails($item) + programs.push({ + title: parseTitle($item), + category: parseCategory($item), + image: details.image, + description: details.description, + directors: details.directors, + actors: details.actors, + start, + stop + }) + } + + return programs + }, + async channels() { + const content = await axios + .get('https://www.reportv.com.ar/buscador/Buscador.php?aid=2694') + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(content) + const items = $('#tr_home_2 > td:nth-child(1) > select > option').toArray() + + return items.map(item => { + return { + lang: 'es', + site_id: $(item).attr('value'), + name: $(item).text() + } + }) + } +} + +async function loadProgramDetails($item) { + const onclick = $item('*').attr('onclick') + const regexp = /detallePrograma\((\d+),(\d+),(\d+),(\d+),'([^']+)'\);/g + const match = [...onclick.matchAll(regexp)] + const [, id, idc, id_alineacion, idp, title] = match[0] + if (!id || !idc || !id_alineacion || !idp || !title) return Promise.resolve({}) + const formData = new URLSearchParams() + formData.append('id', id) + formData.append('idc', idc) + formData.append('id_alineacion', id_alineacion) + formData.append('idp', idp) + formData.append('title', title) + const content = await axios + .post('https://www.reportv.com.ar/buscador/DetallePrograma.php', formData) + .then(r => r.data.toString()) + .catch(console.error) + if (!content) return Promise.resolve({}) + + const $ = cheerio.load(content) + + return Promise.resolve({ + image: parseImage($), + actors: parseActors($), + directors: parseDirectors($), + description: parseDescription($) + }) +} + +function parseActors($) { + const section = $('#Ficha > div') + .html() + .split('
    ') + .find(str => str.includes('Actores:')) + if (!section) return null + const $section = cheerio.load(section) + + return $section('span') + .map((i, el) => $(el).text().trim()) + .get() +} + +function parseDirectors($) { + const section = $('#Ficha > div') + .html() + .split('
    ') + .find(str => str.includes('Directores:')) + if (!section) return null + const $section = cheerio.load(section) + + return $section('span') + .map((i, el) => $(el).text().trim()) + .get() +} + +function parseDescription($) { + return $('#Sinopsis > div').text().trim() +} + +function parseImage($) { + const src = $('#ImgProg').attr('src') + const url = new URL(src, 'https://www.reportv.com.ar/buscador/') + + return url.href +} + +function parseTitle($item) { + const [, title] = $item('div:nth-child(1) > span').text().split(' - ') + + return title +} + +function parseCategory($item) { + return $item('div:nth-child(3) > span').text() +} + +function parseStart($item, date) { + const [time] = $item('div:nth-child(1) > span').text().split(' - ') + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'America/Caracas') +} + +function parseDuration($item) { + const [hh, mm, ss] = $item('div:nth-child(4) > span').text().split(':') + + return parseInt(hh) * 3600 + parseInt(mm) * 60 + parseInt(ss) +} + +function parseItems(content, date) { + if (!content) return [] + const $ = cheerio.load(content) + const d = startCase(date.locale('es').format('DD MMMM YYYY')) + + return $(`.trProg[title*="${d}"]`).toArray() +} diff --git a/sites/reportv.com.ar/reportv.com.ar.test.js b/sites/reportv.com.ar/reportv.com.ar.test.js index 19fa99bb..2b20f79d 100644 --- a/sites/reportv.com.ar/reportv.com.ar.test.js +++ b/sites/reportv.com.ar/reportv.com.ar.test.js @@ -1,111 +1,111 @@ -const { parser, url, request } = require('./reportv.com.ar.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 axios = require('axios') -jest.mock('axios') - -const date = dayjs.utc('2022-10-03', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '914', - xmltv_id: 'VePlusVenezuela.ve' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.reportv.com.ar/buscador/ProgXSenial.php') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded' - }) -}) - -it('can generate valid request data', () => { - const result = request.data({ channel, date }) - expect(result.get('idSenial')).toBe('914') - expect(result.get('Alineacion')).toBe('2694') - expect(result.get('DiaDesde')).toBe('2022/10/03') - expect(result.get('HoraDesde')).toBe('00:00:00') -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - - axios.post.mockImplementation((url, data) => { - if ( - url === 'https://www.reportv.com.ar/buscador/DetallePrograma.php' && - data.get('id') == '286096' - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/program1.html')) - }) - } else if ( - url === 'https://www.reportv.com.ar/buscador/DetallePrograma.php' && - data.get('id') == '392803' - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/program2.html')) - }) - } else { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/no_program.html')) - }) - } - }) - - let results = await parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-03T04:00:00.000Z', - stop: '2022-10-03T05:00:00.000Z', - title: '¿Quién tiene la razón?', - category: 'Talk Show', - image: 'https://www.reportv.com.ar/buscador/img/Programas/4401882.jpg', - actors: ['Nancy Álvarez'], - description: - 'Espacio que dará de qué hablar cuando la doctora Nancy Álvarez y Carmen Jara, acompañadas de un jurado implacable, lleguen a escuchar y a resolver los problemas de las partes en conflicto para luego decidir quién tiene la razón.' - }) - - expect(results[21]).toMatchObject({ - start: '2022-10-04T03:00:00.000Z', - stop: '2022-10-04T04:00:00.000Z', - title: 'Valeria', - category: 'Comedia', - image: 'https://www.reportv.com.ar/buscador/img/Programas/18788047.jpg', - directors: ['Inma Torrente'], - actors: [ - 'Diana Gómez', - 'Silma López', - 'Paula Malia', - 'Teresa Riott', - 'Maxi Iglesias', - 'Juanlu González', - 'Aitor Luna', - 'Lauren McFall', - 'Éva Martin', - 'Raquel Ventosa' - ], - description: - 'Valeria es una escritora que no está pasando por su mejor momento a nivel profesional y sentimental. La distancia emocional que la separa de su marido la lleva a refugiarse en sus tres mejores amigas: Carmen, Lola y Nerea. Valeria y sus amigas están inmersas en un torbellino de emociones de amor, amistad, celos, infidelidad, dudas, desamor, secretos, trabajo, preocupaciones, alegrías y sueños sobre el futuro.' - }) -}) - -it('can handle empty guide', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - const result = await parser({ content, date }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./reportv.com.ar.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 axios = require('axios') +jest.mock('axios') + +const date = dayjs.utc('2022-10-03', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '914', + xmltv_id: 'VePlusVenezuela.ve' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.reportv.com.ar/buscador/ProgXSenial.php') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded' + }) +}) + +it('can generate valid request data', () => { + const result = request.data({ channel, date }) + expect(result.get('idSenial')).toBe('914') + expect(result.get('Alineacion')).toBe('2694') + expect(result.get('DiaDesde')).toBe('2022/10/03') + expect(result.get('HoraDesde')).toBe('00:00:00') +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + + axios.post.mockImplementation((url, data) => { + if ( + url === 'https://www.reportv.com.ar/buscador/DetallePrograma.php' && + data.get('id') == '286096' + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/program1.html')) + }) + } else if ( + url === 'https://www.reportv.com.ar/buscador/DetallePrograma.php' && + data.get('id') == '392803' + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/program2.html')) + }) + } else { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/no_program.html')) + }) + } + }) + + let results = await parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-03T04:00:00.000Z', + stop: '2022-10-03T05:00:00.000Z', + title: '¿Quién tiene la razón?', + category: 'Talk Show', + image: 'https://www.reportv.com.ar/buscador/img/Programas/4401882.jpg', + actors: ['Nancy Álvarez'], + description: + 'Espacio que dará de qué hablar cuando la doctora Nancy Álvarez y Carmen Jara, acompañadas de un jurado implacable, lleguen a escuchar y a resolver los problemas de las partes en conflicto para luego decidir quién tiene la razón.' + }) + + expect(results[21]).toMatchObject({ + start: '2022-10-04T03:00:00.000Z', + stop: '2022-10-04T04:00:00.000Z', + title: 'Valeria', + category: 'Comedia', + image: 'https://www.reportv.com.ar/buscador/img/Programas/18788047.jpg', + directors: ['Inma Torrente'], + actors: [ + 'Diana Gómez', + 'Silma López', + 'Paula Malia', + 'Teresa Riott', + 'Maxi Iglesias', + 'Juanlu González', + 'Aitor Luna', + 'Lauren McFall', + 'Éva Martin', + 'Raquel Ventosa' + ], + description: + 'Valeria es una escritora que no está pasando por su mejor momento a nivel profesional y sentimental. La distancia emocional que la separa de su marido la lleva a refugiarse en sus tres mejores amigas: Carmen, Lola y Nerea. Valeria y sus amigas están inmersas en un torbellino de emociones de amor, amistad, celos, infidelidad, dudas, desamor, secretos, trabajo, preocupaciones, alegrías y sueños sobre el futuro.' + }) +}) + +it('can handle empty guide', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + const result = await parser({ content, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/rikstv.no/__data__/content.json b/sites/rikstv.no/__data__/content.json new file mode 100644 index 00000000..7d2d5cb3 --- /dev/null +++ b/sites/rikstv.no/__data__/content.json @@ -0,0 +1,21 @@ +[ + { + "seriesName": "Vakre og ville Oman", + "name": "Vakre og ville Oman", + "description": "Oman er eit arabisk skattkammer av unike habitat og variert dyreliv. Rev, kvalhai, reptil og skjelpadder er blant skapningane du finn her.", + "season": 1, + "episode": 1, + "genres": [ + "Dokumentar", + "Fakta", + "Natur" + ], + "actors": [ + "Gergana Muskalla" + ], + "director": "Stefania Muller", + "imagePackUri": "https://imageservice.rikstv.no/hash/EC206C374F42287C0BDF850A7D3CB4D3.jpg", + "broadcastedTime": "2025-01-13T23:00:00Z", + "broadcastedTimeEnd": "2025-01-13T23:55:00Z" + } +] \ No newline at end of file diff --git a/sites/rikstv.no/rikstv.no.config.js b/sites/rikstv.no/rikstv.no.config.js index b897473b..7204ffe3 100644 --- a/sites/rikstv.no/rikstv.no.config.js +++ b/sites/rikstv.no/rikstv.no.config.js @@ -1,76 +1,76 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const axios = require('axios') - -dayjs.extend(utc) - -module.exports = { - site: 'rikstv.no', - days: 3, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url({ channel, date }) { - return `https://play.rikstv.no/api/content-search/1/channel/${ - channel.site_id - }/epg/${date.format('YYYY-MM-DD')}` - }, - parser: function ({ content }) { - let data - try { - data = JSON.parse(content) - } catch (error) { - console.error('Error parsing JSON:', error) - return [] - } - - const programs = [] - - if (data && Array.isArray(data)) { - data.forEach(item => { - if (!item) return - //const start = dayjs.utc(item.broadcastedTime) - //const stop = dayjs.utc(item.broadcastedTimeEnd) - - programs.push({ - title: item.seriesName, - sub_title: item.name, - description: item.description || item.synopsis, - season: item.season || null, - episode: item.episode || null, - category: item.genres, - actors: item.actors, - directors: item.director || item.directors, - icon: item.imagePackUri, - start: item.broadcastedTime, - stop: item.broadcastedTimeEnd - }) - }) - } - - return programs - }, - async channels() { - try { - const response = await axios.get( - 'https://play.rikstv.no/api/content-search/1/channel?includePrograms=false' - ) - if (!response.data || !Array.isArray(response.data)) { - console.error('Error: No channels data found') - return [] - } - return response.data.map(item => { - return { - lang: 'no', - site_id: item.channelId, - name: item.serviceName - } - }) - } catch (error) { - console.error('Error fetching channels:', error) - return [] - } - } -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const axios = require('axios') + +dayjs.extend(utc) + +module.exports = { + site: 'rikstv.no', + days: 3, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url({ channel, date }) { + return `https://play.rikstv.no/api/content-search/1/channel/${ + channel.site_id + }/epg/${date.format('YYYY-MM-DD')}` + }, + parser: function ({ content }) { + let data + try { + data = JSON.parse(content) + } catch (error) { + console.error('Error parsing JSON:', error) + return [] + } + + const programs = [] + + if (data && Array.isArray(data)) { + data.forEach(item => { + if (!item) return + //const start = dayjs.utc(item.broadcastedTime) + //const stop = dayjs.utc(item.broadcastedTimeEnd) + + programs.push({ + title: item.seriesName, + sub_title: item.name, + description: item.description || item.synopsis, + season: item.season || null, + episode: item.episode || null, + category: item.genres, + actors: item.actors, + directors: item.director || item.directors, + icon: item.imagePackUri, + start: item.broadcastedTime, + stop: item.broadcastedTimeEnd + }) + }) + } + + return programs + }, + async channels() { + try { + const response = await axios.get( + 'https://play.rikstv.no/api/content-search/1/channel?includePrograms=false' + ) + if (!response.data || !Array.isArray(response.data)) { + console.error('Error: No channels data found') + return [] + } + return response.data.map(item => { + return { + lang: 'no', + site_id: item.channelId, + name: item.serviceName + } + }) + } catch (error) { + console.error('Error fetching channels:', error) + return [] + } + } +} diff --git a/sites/rikstv.no/rikstv.no.test.js b/sites/rikstv.no/rikstv.no.test.js index ce0f0959..3ea233a5 100644 --- a/sites/rikstv.no/rikstv.no.test.js +++ b/sites/rikstv.no/rikstv.no.test.js @@ -1,72 +1,58 @@ -const { parser, url } = require('./rikstv.no.config.js') -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-14', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '47', - xmltv_id: 'NRK1.no' -} - -describe('rikstv.no Module Tests', () => { - it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - `https://play.rikstv.no/api/content-search/1/channel/${channel.site_id}/epg/${date.format( - 'YYYY-MM-DD' - )}` - ) - }) - - it('can parse response', () => { - const content = JSON.stringify([ - { - seriesName: 'Vakre og ville Oman', - name: 'Vakre og ville Oman', - description: - 'Oman er eit arabisk skattkammer av unike habitat og variert dyreliv. Rev, kvalhai, reptil og skjelpadder er blant skapningane du finn her.', - season: 1, - episode: 1, - genres: ['Dokumentar', 'Fakta', 'Natur'], - actors: ['Gergana Muskalla'], - director: 'Stefania Muller', - imagePackUri: 'https://imageservice.rikstv.no/hash/EC206C374F42287C0BDF850A7D3CB4D3.jpg', - broadcastedTime: '2025-01-13T23:00:00Z', - broadcastedTimeEnd: '2025-01-13T23:55:00Z' - } - ]) - - const result = parser({ content }).map(p => { - p.start = dayjs(p.start).toISOString() - p.stop = dayjs(p.stop).toISOString() - return p - }) - - expect(result).toMatchObject([ - { - title: 'Vakre og ville Oman', - sub_title: 'Vakre og ville Oman', - description: - 'Oman er eit arabisk skattkammer av unike habitat og variert dyreliv. Rev, kvalhai, reptil og skjelpadder er blant skapningane du finn her.', - season: 1, - episode: 1, - category: ['Dokumentar', 'Fakta', 'Natur'], - actors: ['Gergana Muskalla'], - directors: 'Stefania Muller', - icon: 'https://imageservice.rikstv.no/hash/EC206C374F42287C0BDF850A7D3CB4D3.jpg', - start: '2025-01-13T23:00:00.000Z', - stop: '2025-01-13T23:55:00.000Z' - } - ]) - }) - - it('can handle empty guide', () => { - const result = parser({ - content: '[]' - }) - expect(result).toMatchObject([]) - }) -}) +const { parser, url } = require('./rikstv.no.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-14', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '47', + xmltv_id: 'NRK1.no' +} + +describe('rikstv.no Module Tests', () => { + it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + `https://play.rikstv.no/api/content-search/1/channel/${channel.site_id}/epg/${date.format( + 'YYYY-MM-DD' + )}` + ) + }) + + it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content }).map(p => { + p.start = dayjs(p.start).toISOString() + p.stop = dayjs(p.stop).toISOString() + return p + }) + + expect(result).toMatchObject([ + { + title: 'Vakre og ville Oman', + sub_title: 'Vakre og ville Oman', + description: + 'Oman er eit arabisk skattkammer av unike habitat og variert dyreliv. Rev, kvalhai, reptil og skjelpadder er blant skapningane du finn her.', + season: 1, + episode: 1, + category: ['Dokumentar', 'Fakta', 'Natur'], + actors: ['Gergana Muskalla'], + directors: 'Stefania Muller', + icon: 'https://imageservice.rikstv.no/hash/EC206C374F42287C0BDF850A7D3CB4D3.jpg', + start: '2025-01-13T23:00:00.000Z', + stop: '2025-01-13T23:55:00.000Z' + } + ]) + }) + + it('can handle empty guide', () => { + const result = parser({ + content: '[]' + }) + expect(result).toMatchObject([]) + }) +}) diff --git a/sites/rotana.net/rotana.net.config.js b/sites/rotana.net/rotana.net.config.js index f38abbff..d8ab85b6 100644 --- a/sites/rotana.net/rotana.net.config.js +++ b/sites/rotana.net/rotana.net.config.js @@ -1,189 +1,189 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const timezone = require('dayjs/plugin/timezone') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const doFetch = require('@ntlab/sfetch') -const debug = require('debug')('site:rotana.net') - -dayjs.extend(timezone) -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -doFetch.setCheckResult(false).setDebugger(debug) - -const tz = 'Asia/Riyadh' -const defaultHeaders = { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 OPR/104.0.0.0' -} -const cookies = {} - -module.exports = { - site: 'rotana.net', - days: 2, - url({ channel }) { - return `https://rotana.net/${channel.lang}/streams?channel=${channel.site_id}&tz=` - }, - request: { - headers: defaultHeaders, - timeout: 15000 - }, - async parser({ content, headers, channel, date }) { - const programs = [] - if (!cookies[channel.lang]) { - cookies[channel.lang] = parseCookies(headers) - } - - const items = parseItems(content, date) - if (items.length) { - const queues = [] - for (const item of items) { - const url = `https://rotana.net/${channel.lang}/streams?channel=${channel.site_id}&itemId=${item.program}` - const params = { - headers: { - ...defaultHeaders, - 'X-Requested-With': 'XMLHttpRequest', - cookie: cookies[channel.lang] - } - } - queues.push({ i: item, url, params }) - } - await doFetch(queues, (queue, res) => { - programs.push(parseProgram(queue.i, res)) - }) - } - - return programs - }, - async channels({ lang = 'en' }) { - const result = await axios - .get('https://rotana.net/api/channels') - .then(response => response.data) - .catch(console.error) - - return result.data.map(item => { - return { - lang, - site_id: item.id, - name: item.name[lang] - } - }) - } -} - -function parseProgram(item, result) { - const $ = cheerio.load(result) - const details = $('.trending-info .row div > span') - if (details.length) { - for (const el of details[0].children) { - switch (el.constructor.name) { - case 'Text': - if (item.description === undefined) { - const desc = $(el).text().trim() - if (desc) { - item.description = desc - } - } - break - case 'Element': - if (el.name === 'span') { - const [k, v] = $(el) - .text() - .split(':') - .map(a => a.trim()) - switch (k) { - case 'Category': - case 'التصنيف': - item.category = v - break - case 'Country': - case 'البلد': - item.country = v - break - case 'Director': - case 'المخرج': - item.director = v - break - case 'Language': - case 'اللغة': - item.language = v - break - case 'Release Year': - case 'سنة الإصدار': - item.date = v - break - } - } - break - } - } - } - const img = $('.row > div > img') - if (img.length) { - item.image = img.attr('src') - } - delete item.program - return item -} - -function parseItems(content, date) { - const $ = cheerio.load(content) - - const items = [] - let curDate - $('.hour > div').each((_, item) => { - const $item = $(item) - if ($item.hasClass('bg')) { - curDate = $item.attr('id') - curDate = curDate.substr(curDate.indexOf('-') + 1).split('-') - } else if ($item.hasClass('iq-accordion')) { - const top = $item.find('.iq-accordion-block') - const heading = top.find('.iq-accordion-title .big-title') - if (heading.length) { - const progId = top.attr('id') - const title = heading - .find('span:eq(1)') - .text() - .split('\n') - .map(a => a.trim()) - .join(' ') - const time = heading.find('span:eq(0)').text() - const [d, m, y] = curDate - items.push({ - program: progId.substr(progId.indexOf('-') + 1), - title: title ? title.trim() : title, - start: `${y}-${m}-${d} ${time.trim()}` - }) - } - } - }) - items.sort((a, b) => a.start.localeCompare(b.start)) - for (let i = 0; i < items.length; i++) { - if (i < items.length - 2) { - items[i].stop = items[i + 1].start - } else { - const dt = dayjs.tz(items[i].start).add(1, 'd') - items[i].stop = `${dt.format('YYYY-MM-DD')} 00:00` - } - } - const expectedDate = `${date.format('YYYY-MM-DD')}` - return items - .filter(a => a.start.startsWith(expectedDate) || a.stop.startsWith(expectedDate)) - .map(a => { - a.start = dayjs.tz(a.start, tz) - a.stop = dayjs.tz(a.stop, tz) - return a - }) -} - -function parseCookies(headers) { - const cookies = [] - if (headers && Array.isArray(headers['set-cookie'])) { - headers['set-cookie'].forEach(cookie => { - cookies.push(cookie.split('; ')[0]) - }) - } - return cookies.length ? cookies.join('; ') : null -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const timezone = require('dayjs/plugin/timezone') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const doFetch = require('@ntlab/sfetch') +const debug = require('debug')('site:rotana.net') + +dayjs.extend(timezone) +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +doFetch.setCheckResult(false).setDebugger(debug) + +const tz = 'Asia/Riyadh' +const defaultHeaders = { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 OPR/104.0.0.0' +} +const cookies = {} + +module.exports = { + site: 'rotana.net', + days: 2, + url({ channel }) { + return `https://rotana.net/${channel.lang}/streams?channel=${channel.site_id}&tz=` + }, + request: { + headers: defaultHeaders, + timeout: 15000 + }, + async parser({ content, headers, channel, date }) { + const programs = [] + if (!cookies[channel.lang]) { + cookies[channel.lang] = parseCookies(headers) + } + + const items = parseItems(content, date) + if (items.length) { + const queues = [] + for (const item of items) { + const url = `https://rotana.net/${channel.lang}/streams?channel=${channel.site_id}&itemId=${item.program}` + const params = { + headers: { + ...defaultHeaders, + 'X-Requested-With': 'XMLHttpRequest', + cookie: cookies[channel.lang] + } + } + queues.push({ i: item, url, params }) + } + await doFetch(queues, (queue, res) => { + programs.push(parseProgram(queue.i, res)) + }) + } + + return programs + }, + async channels({ lang = 'en' }) { + const result = await axios + .get('https://rotana.net/api/channels') + .then(response => response.data) + .catch(console.error) + + return result.data.map(item => { + return { + lang, + site_id: item.id, + name: item.name[lang] + } + }) + } +} + +function parseProgram(item, result) { + const $ = cheerio.load(result) + const details = $('.trending-info .row div > span') + if (details.length) { + for (const el of details[0].children) { + switch (el.constructor.name) { + case 'Text': + if (item.description === undefined) { + const desc = $(el).text().trim() + if (desc) { + item.description = desc + } + } + break + case 'Element': + if (el.name === 'span') { + const [k, v] = $(el) + .text() + .split(':') + .map(a => a.trim()) + switch (k) { + case 'Category': + case 'التصنيف': + item.category = v + break + case 'Country': + case 'البلد': + item.country = v + break + case 'Director': + case 'المخرج': + item.director = v + break + case 'Language': + case 'اللغة': + item.language = v + break + case 'Release Year': + case 'سنة الإصدار': + item.date = v + break + } + } + break + } + } + } + const img = $('.row > div > img') + if (img.length) { + item.image = img.attr('src') + } + delete item.program + return item +} + +function parseItems(content, date) { + const $ = cheerio.load(content) + + const items = [] + let curDate + $('.hour > div').each((_, item) => { + const $item = $(item) + if ($item.hasClass('bg')) { + curDate = $item.attr('id') + curDate = curDate.substr(curDate.indexOf('-') + 1).split('-') + } else if ($item.hasClass('iq-accordion')) { + const top = $item.find('.iq-accordion-block') + const heading = top.find('.iq-accordion-title .big-title') + if (heading.length) { + const progId = top.attr('id') + const title = heading + .find('span:eq(1)') + .text() + .split('\n') + .map(a => a.trim()) + .join(' ') + const time = heading.find('span:eq(0)').text() + const [d, m, y] = curDate + items.push({ + program: progId.substr(progId.indexOf('-') + 1), + title: title ? title.trim() : title, + start: `${y}-${m}-${d} ${time.trim()}` + }) + } + } + }) + items.sort((a, b) => a.start.localeCompare(b.start)) + for (let i = 0; i < items.length; i++) { + if (i < items.length - 2) { + items[i].stop = items[i + 1].start + } else { + const dt = dayjs.tz(items[i].start).add(1, 'd') + items[i].stop = `${dt.format('YYYY-MM-DD')} 00:00` + } + } + const expectedDate = `${date.format('YYYY-MM-DD')}` + return items + .filter(a => a.start.startsWith(expectedDate) || a.stop.startsWith(expectedDate)) + .map(a => { + a.start = dayjs.tz(a.start, tz) + a.stop = dayjs.tz(a.stop, tz) + return a + }) +} + +function parseCookies(headers) { + const cookies = [] + if (headers && Array.isArray(headers['set-cookie'])) { + headers['set-cookie'].forEach(cookie => { + cookies.push(cookie.split('; ')[0]) + }) + } + return cookies.length ? cookies.join('; ') : null +} diff --git a/sites/rotana.net/rotana.net.test.js b/sites/rotana.net/rotana.net.test.js index ef49b147..cc04274a 100644 --- a/sites/rotana.net/rotana.net.test.js +++ b/sites/rotana.net/rotana.net.test.js @@ -1,113 +1,113 @@ -const { parser, url, request } = require('./rotana.net.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('2024-11-26').startOf('d') -const channel = { - lang: 'en', - site_id: '439', - xmltv_id: 'RotanaCinemaMasr.sa' -} -const channelAr = Object.assign({}, channel, { lang: 'ar' }) - -axios.get.mockImplementation(url => { - if (url === 'https://rotana.net/en/streams?channel=439&itemId=736970') { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/program_en.html')) - }) - } - if (url === 'https://rotana.net/ar/streams?channel=439&itemId=736970') { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/program_ar.html')) - }) - } - - return Promise.resolve({ data: '' }) -}) - -it('can use defined user agent', () => { - const result = request.headers['User-Agent'] - expect(result).toBe( - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 OPR/104.0.0.0' - ) -}) - -it('can generate valid english url', () => { - const result = url({ channel, date }) - expect(result).toBe('https://rotana.net/en/streams?channel=439&tz=') -}) - -it('can generate valid arabic url', () => { - const result = url({ channel: channelAr, date }) - expect(result).toBe('https://rotana.net/ar/streams?channel=439&tz=') -}) - -it('can parse english response', async () => { - const result = ( - await parser({ - channel, - date, - content: fs.readFileSync(path.join(__dirname, '/__data__/content_en.html')) - }) - ).map(a => { - a.start = a.start.toJSON() - a.stop = a.stop.toJSON() - return a - }) - - expect(result.length).toBe(12) - expect(result[11]).toMatchObject({ - start: '2024-11-26T20:00:00.000Z', - stop: '2024-11-26T22:00:00.000Z', - title: 'Khiyana Mashroua', - description: - 'Hisham knows that his father has given all his wealth to his elder brother. This leads him to plan to kill his brother to make it look like a defense of honor, which he does by killing his wife along...', - image: - 'https://s3.eu-central-1.amazonaws.com/rotana.website/spider_storage/1398X1000/1687084565', - category: 'Movie' - }) -}) - -it('can parse arabic response', async () => { - const result = ( - await parser({ - channel: channelAr, - date, - content: fs.readFileSync(path.join(__dirname, '/__data__/content_ar.html')) - }) - ).map(a => { - a.start = a.start.toJSON() - a.stop = a.stop.toJSON() - return a - }) - - expect(result.length).toBe(12) - expect(result[11]).toMatchObject({ - start: '2024-11-26T20:00:00.000Z', - stop: '2024-11-26T22:00:00.000Z', - title: 'خيانة مشروعة', - description: - 'يعلم هشام البحيري أن والده قد حرمه من الميراث، ووهب كل ثروته لشقيقه اﻷكبر، وهو ما يدفعه لتدبير جريمة قتل شقيقه لتبدو وكأنها دفاع عن الشرف، وذلك حين يقتل هشام زوجته مع شقيقه.', - image: - 'https://s3.eu-central-1.amazonaws.com/rotana.website/spider_storage/1398X1000/1687084565', - category: 'فيلم' - }) -}) - -it('can handle empty guide', async () => { - const result = await parser({ - content: '', - date, - channel - }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./rotana.net.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('2024-11-26').startOf('d') +const channel = { + lang: 'en', + site_id: '439', + xmltv_id: 'RotanaCinemaMasr.sa' +} +const channelAr = Object.assign({}, channel, { lang: 'ar' }) + +axios.get.mockImplementation(url => { + if (url === 'https://rotana.net/en/streams?channel=439&itemId=736970') { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/program_en.html')) + }) + } + if (url === 'https://rotana.net/ar/streams?channel=439&itemId=736970') { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/program_ar.html')) + }) + } + + return Promise.resolve({ data: '' }) +}) + +it('can use defined user agent', () => { + const result = request.headers['User-Agent'] + expect(result).toBe( + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 OPR/104.0.0.0' + ) +}) + +it('can generate valid english url', () => { + const result = url({ channel, date }) + expect(result).toBe('https://rotana.net/en/streams?channel=439&tz=') +}) + +it('can generate valid arabic url', () => { + const result = url({ channel: channelAr, date }) + expect(result).toBe('https://rotana.net/ar/streams?channel=439&tz=') +}) + +it('can parse english response', async () => { + const result = ( + await parser({ + channel, + date, + content: fs.readFileSync(path.join(__dirname, '/__data__/content_en.html')) + }) + ).map(a => { + a.start = a.start.toJSON() + a.stop = a.stop.toJSON() + return a + }) + + expect(result.length).toBe(12) + expect(result[11]).toMatchObject({ + start: '2024-11-26T20:00:00.000Z', + stop: '2024-11-26T22:00:00.000Z', + title: 'Khiyana Mashroua', + description: + 'Hisham knows that his father has given all his wealth to his elder brother. This leads him to plan to kill his brother to make it look like a defense of honor, which he does by killing his wife along...', + image: + 'https://s3.eu-central-1.amazonaws.com/rotana.website/spider_storage/1398X1000/1687084565', + category: 'Movie' + }) +}) + +it('can parse arabic response', async () => { + const result = ( + await parser({ + channel: channelAr, + date, + content: fs.readFileSync(path.join(__dirname, '/__data__/content_ar.html')) + }) + ).map(a => { + a.start = a.start.toJSON() + a.stop = a.stop.toJSON() + return a + }) + + expect(result.length).toBe(12) + expect(result[11]).toMatchObject({ + start: '2024-11-26T20:00:00.000Z', + stop: '2024-11-26T22:00:00.000Z', + title: 'خيانة مشروعة', + description: + 'يعلم هشام البحيري أن والده قد حرمه من الميراث، ووهب كل ثروته لشقيقه اﻷكبر، وهو ما يدفعه لتدبير جريمة قتل شقيقه لتبدو وكأنها دفاع عن الشرف، وذلك حين يقتل هشام زوجته مع شقيقه.', + image: + 'https://s3.eu-central-1.amazonaws.com/rotana.website/spider_storage/1398X1000/1687084565', + category: 'فيلم' + }) +}) + +it('can handle empty guide', async () => { + const result = await parser({ + content: '', + date, + channel + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/rtb.gov.bn/rtb.gov.bn.config.js b/sites/rtb.gov.bn/rtb.gov.bn.config.js index c560f224..5a4239eb 100644 --- a/sites/rtb.gov.bn/rtb.gov.bn.config.js +++ b/sites/rtb.gov.bn/rtb.gov.bn.config.js @@ -1,74 +1,74 @@ -const pdf = require('pdf-parse') -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: 'rtb.gov.bn', - days: 2, - url: function ({ channel, date }) { - return encodeURI( - `http://www.rtb.gov.bn/PublishingImages/SitePages/Programme Guide/${ - channel.site_id - } ${date.format('DD MMMM YYYY')}.pdf` - ) - }, - parser: async function ({ buffer, date }) { - let programs = [] - const items = await parseItems(buffer) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(1, 'h') - programs.push({ - title: item.title, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item, date) { - const dateString = `${date.format('YYYY-MM-DD')} ${item.time}` - - return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Asia/Brunei') -} - -async function parseItems(buffer) { - let data - try { - data = await pdf(buffer) - } catch { - return [] - } - - if (!data) return [] - - return data.text - .split('\n') - .filter(s => { - const string = s.trim() - - return string && /^\d{2}:\d{2}/.test(string) - }) - .map(s => { - const [, time, title] = s.trim().match(/^(\d{2}:\d{2}) (.*)/) || [null, null, null] - - return { time, title } - }) -} +const pdf = require('pdf-parse') +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: 'rtb.gov.bn', + days: 2, + url: function ({ channel, date }) { + return encodeURI( + `http://www.rtb.gov.bn/PublishingImages/SitePages/Programme Guide/${ + channel.site_id + } ${date.format('DD MMMM YYYY')}.pdf` + ) + }, + parser: async function ({ buffer, date }) { + let programs = [] + const items = await parseItems(buffer) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(1, 'h') + programs.push({ + title: item.title, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item, date) { + const dateString = `${date.format('YYYY-MM-DD')} ${item.time}` + + return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Asia/Brunei') +} + +async function parseItems(buffer) { + let data + try { + data = await pdf(buffer) + } catch { + return [] + } + + if (!data) return [] + + return data.text + .split('\n') + .filter(s => { + const string = s.trim() + + return string && /^\d{2}:\d{2}/.test(string) + }) + .map(s => { + const [, time, title] = s.trim().match(/^(\d{2}:\d{2}) (.*)/) || [null, null, null] + + return { time, title } + }) +} diff --git a/sites/rtb.gov.bn/rtb.gov.bn.test.js b/sites/rtb.gov.bn/rtb.gov.bn.test.js index 5ce7f0ee..544b483b 100644 --- a/sites/rtb.gov.bn/rtb.gov.bn.test.js +++ b/sites/rtb.gov.bn/rtb.gov.bn.test.js @@ -1,94 +1,94 @@ -const { parser, url } = require('./rtb.gov.bn.config.js') -const path = require('path') -const fs = require('fs') -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('2021-11-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'Sukmaindera', - xmltv_id: 'RTBSukmaindera.bn' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'http://www.rtb.gov.bn/PublishingImages/SitePages/Programme%20Guide/Sukmaindera%2011%20November%202021.pdf' - ) -}) - -it('can parse Sukmaindera 11 November 2021.pdf', done => { - const buffer = fs.readFileSync( - path.resolve(__dirname, '__data__/Sukmaindera 11 November 2021.pdf'), - { - charset: 'utf8' - } - ) - parser({ buffer, date }) - .then(results => { - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(results.length).toBe(47) - expect(results[0]).toMatchObject({ - start: '2021-11-10T22:00:00.000Z', - stop: '2021-11-10T22:05:00.000Z', - title: 'NATIONAL ANTHEM' - }) - expect(results[46]).toMatchObject({ - start: '2021-11-11T21:30:00.000Z', - stop: '2021-11-11T22:30:00.000Z', - title: 'BACAAN SURAH YASSIN' - }) - done() - }) - .catch(error => { - done(error) - }) -}) - -it('can parse Aneka 11 November 2021.pdf', done => { - const buffer = fs.readFileSync(path.resolve(__dirname, '__data__/Aneka 11 November 2021.pdf'), { - charset: 'utf8' - }) - parser({ buffer, date }) - .then(results => { - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(results.length).toBe(26) - expect(results[4]).toMatchObject({ - start: '2021-11-11T03:00:00.000Z', - stop: '2021-11-11T04:05:00.000Z', - title: 'DRAMA TURKI:' - }) - done() - }) - .catch(error => { - done(error) - }) -}) - -it('can handle empty guide', done => { - parser({ - date, - channel, - content: `Object moved -

    Object moved to here.

    - -` - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(error => { - done(error) - }) -}) +const { parser, url } = require('./rtb.gov.bn.config.js') +const path = require('path') +const fs = require('fs') +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('2021-11-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'Sukmaindera', + xmltv_id: 'RTBSukmaindera.bn' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'http://www.rtb.gov.bn/PublishingImages/SitePages/Programme%20Guide/Sukmaindera%2011%20November%202021.pdf' + ) +}) + +it('can parse Sukmaindera 11 November 2021.pdf', done => { + const buffer = fs.readFileSync( + path.resolve(__dirname, '__data__/Sukmaindera 11 November 2021.pdf'), + { + charset: 'utf8' + } + ) + parser({ buffer, date }) + .then(results => { + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(results.length).toBe(47) + expect(results[0]).toMatchObject({ + start: '2021-11-10T22:00:00.000Z', + stop: '2021-11-10T22:05:00.000Z', + title: 'NATIONAL ANTHEM' + }) + expect(results[46]).toMatchObject({ + start: '2021-11-11T21:30:00.000Z', + stop: '2021-11-11T22:30:00.000Z', + title: 'BACAAN SURAH YASSIN' + }) + done() + }) + .catch(error => { + done(error) + }) +}) + +it('can parse Aneka 11 November 2021.pdf', done => { + const buffer = fs.readFileSync(path.resolve(__dirname, '__data__/Aneka 11 November 2021.pdf'), { + charset: 'utf8' + }) + parser({ buffer, date }) + .then(results => { + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(results.length).toBe(26) + expect(results[4]).toMatchObject({ + start: '2021-11-11T03:00:00.000Z', + stop: '2021-11-11T04:05:00.000Z', + title: 'DRAMA TURKI:' + }) + done() + }) + .catch(error => { + done(error) + }) +}) + +it('can handle empty guide', done => { + parser({ + date, + channel, + content: `Object moved +

    Object moved to here.

    + +` + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(error => { + done(error) + }) +}) diff --git a/sites/rthk.hk/rthk.hk.config.js b/sites/rthk.hk/rthk.hk.config.js index f9efcc69..babdb960 100644 --- a/sites/rthk.hk/rthk.hk.config.js +++ b/sites/rthk.hk/rthk.hk.config.js @@ -1,89 +1,89 @@ -const dayjs = require('dayjs') -const cheerio = require('cheerio') -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: 'rthk.hk', - days: 2, - request: { - headers({ channel }) { - return { - Cookie: `lang=${channel.lang}` - } - }, - cache: { - ttl: 60 * 60 * 1000 // 1h - } - }, - url: function ({ date }) { - return `https://www.rthk.hk/timetable/main_timetable/${date.format('YYYYMMDD')}` - }, - parser({ content, channel, date }) { - const programs = [] - const items = parseItems(content, channel) - for (let item of items) { - const $item = cheerio.load(item) - programs.push({ - title: parseTitle($item), - sub_title: parseSubTitle($item), - categories: parseCategories($item), - image: parseImage($item), - start: parseStart($item, date), - stop: parseStop($item, date) - }) - } - - return programs - } -} - -function parseImage($item) { - return $item('.single-wrap').data('p') -} - -function parseCategories($item) { - let cate = $item('.single-wrap').data('cate') || '' - let [, categories] = cate.match(/^\|(.*)\|$/) || [null, ''] - - return categories.split('||').filter(Boolean) -} - -function parseTitle($item) { - return $item('.showTit').attr('title') -} - -function parseSubTitle($item) { - return $item('.showEpi').attr('title') -} - -function parseStart($item, date) { - const timeRow = $item('.timeRow').text().trim() - const [, HH, mm] = timeRow.match(/^(\d+):(\d+)-/) || [null, null, null] - if (!HH || !mm) return null - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Asia/Hong_Kong') -} - -function parseStop($item, date) { - const timeRow = $item('.timeRow').text().trim() - const [, HH, mm] = timeRow.match(/-(\d+):(\d+)$/) || [null, null, null] - if (!HH || !mm) return null - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Asia/Hong_Kong') -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.result)) return [] - const channelData = data.result.find(i => i.key == channel.site_id) - if (!channelData || !channelData.data) return [] - const $ = cheerio.load(channelData.data) - - return $('.showWrap').toArray() -} +const dayjs = require('dayjs') +const cheerio = require('cheerio') +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: 'rthk.hk', + days: 2, + request: { + headers({ channel }) { + return { + Cookie: `lang=${channel.lang}` + } + }, + cache: { + ttl: 60 * 60 * 1000 // 1h + } + }, + url: function ({ date }) { + return `https://www.rthk.hk/timetable/main_timetable/${date.format('YYYYMMDD')}` + }, + parser({ content, channel, date }) { + const programs = [] + const items = parseItems(content, channel) + for (let item of items) { + const $item = cheerio.load(item) + programs.push({ + title: parseTitle($item), + sub_title: parseSubTitle($item), + categories: parseCategories($item), + image: parseImage($item), + start: parseStart($item, date), + stop: parseStop($item, date) + }) + } + + return programs + } +} + +function parseImage($item) { + return $item('.single-wrap').data('p') +} + +function parseCategories($item) { + let cate = $item('.single-wrap').data('cate') || '' + let [, categories] = cate.match(/^\|(.*)\|$/) || [null, ''] + + return categories.split('||').filter(Boolean) +} + +function parseTitle($item) { + return $item('.showTit').attr('title') +} + +function parseSubTitle($item) { + return $item('.showEpi').attr('title') +} + +function parseStart($item, date) { + const timeRow = $item('.timeRow').text().trim() + const [, HH, mm] = timeRow.match(/^(\d+):(\d+)-/) || [null, null, null] + if (!HH || !mm) return null + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Asia/Hong_Kong') +} + +function parseStop($item, date) { + const timeRow = $item('.timeRow').text().trim() + const [, HH, mm] = timeRow.match(/-(\d+):(\d+)$/) || [null, null, null] + if (!HH || !mm) return null + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Asia/Hong_Kong') +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.result)) return [] + const channelData = data.result.find(i => i.key == channel.site_id) + if (!channelData || !channelData.data) return [] + const $ = cheerio.load(channelData.data) + + return $('.showWrap').toArray() +} diff --git a/sites/rthk.hk/rthk.hk.test.js b/sites/rthk.hk/rthk.hk.test.js index 19541dfc..40da1519 100644 --- a/sites/rthk.hk/rthk.hk.test.js +++ b/sites/rthk.hk/rthk.hk.test.js @@ -1,80 +1,80 @@ -const { parser, url, request } = require('./rthk.hk.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) - -jest.mock('axios') - -const date = dayjs.utc('2022-12-02', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '31', - xmltv_id: 'RTHKTV31.hk', - lang: 'zh' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://www.rthk.hk/timetable/main_timetable/20221202') -}) - -it('can generate valid request headers', () => { - expect(request.headers({ channel })).toMatchObject({ - Cookie: 'lang=zh' - }) -}) - -it('can generate valid request headers for English version', () => { - const channelEN = { ...channel, lang: 'en' } - - expect(request.headers({ channel: channelEN })).toMatchObject({ - Cookie: 'lang=en' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_zh.json')) - let results = parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-12-01T16:00:00.000Z', - stop: '2022-12-01T17:00:00.000Z', - title: '問天', - sub_title: '第十四集', - categories: ['戲劇'], - image: 'https://www.rthk.hk/assets/images/rthk/dtt31/thegreataerospace/10239_1920_s.jpg' - }) -}) - -it('can parse response in English', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_en.json')) - let results = parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-12-01T16:00:00.000Z', - stop: '2022-12-01T17:00:00.000Z', - title: 'The Great Aerospace', - sub_title: 'Episode 14', - categories: ['戲劇'], - image: 'https://www.rthk.hk/assets/images/rthk/dtt31/thegreataerospace/10239_1920_s.jpg' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const results = parser({ date, channel, content }) - - expect(results).toMatchObject([]) -}) +const { parser, url, request } = require('./rthk.hk.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) + +jest.mock('axios') + +const date = dayjs.utc('2022-12-02', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '31', + xmltv_id: 'RTHKTV31.hk', + lang: 'zh' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://www.rthk.hk/timetable/main_timetable/20221202') +}) + +it('can generate valid request headers', () => { + expect(request.headers({ channel })).toMatchObject({ + Cookie: 'lang=zh' + }) +}) + +it('can generate valid request headers for English version', () => { + const channelEN = { ...channel, lang: 'en' } + + expect(request.headers({ channel: channelEN })).toMatchObject({ + Cookie: 'lang=en' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_zh.json')) + let results = parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-12-01T16:00:00.000Z', + stop: '2022-12-01T17:00:00.000Z', + title: '問天', + sub_title: '第十四集', + categories: ['戲劇'], + image: 'https://www.rthk.hk/assets/images/rthk/dtt31/thegreataerospace/10239_1920_s.jpg' + }) +}) + +it('can parse response in English', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_en.json')) + let results = parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-12-01T16:00:00.000Z', + stop: '2022-12-01T17:00:00.000Z', + title: 'The Great Aerospace', + sub_title: 'Episode 14', + categories: ['戲劇'], + image: 'https://www.rthk.hk/assets/images/rthk/dtt31/thegreataerospace/10239_1920_s.jpg' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const results = parser({ date, channel, content }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/rtmklik.rtm.gov.my/__data__/content.json b/sites/rtmklik.rtm.gov.my/__data__/content.json new file mode 100644 index 00000000..9ed7c329 --- /dev/null +++ b/sites/rtmklik.rtm.gov.my/__data__/content.json @@ -0,0 +1 @@ +{"id":2,"channel":"TV2","channelId":"2","image":"/live_channel/tv2_Trans.png","idAuthor":9,"type":"TV","timezone":8,"dateCreated":"2022-07-08T01:22:33.233","dateModified":"2022-07-21T21:58:39.77","itemCount":30,"prev":"https://rtm-admin.glueapi.io/v3/epg/2/ChannelSchedule?dateStart=2022-09-03&dateEnd=2022-09-03","next":"https://rtm-admin.glueapi.io/v3/epg/2/ChannelSchedule?dateStart=2022-09-05&dateEnd=2022-09-05","schedule":[{"idEPGProgramSchedule":109303,"dateTimeStart":"2022-09-04T19:00:00","dateTimeEnd":"2022-09-04T20:00:00","scheduleProgramTitle":"Hope Of Life","scheduleProgramDescription":"Kisah kehidupan 3 pakar bedah yang berbeza status dan latar belakang, namun mereka komited dengan kerjaya mereka sebagai doktor. Lakonan : Johnson Low, Elvis Chin, Mayjune Tan, Kelvin Liew, Jacky Kam dan Katrina Ho.","scheduleEpisodeNumber":0,"scheduleSeries":0,"duration":3600,"idEPGProgram":3603,"programTitle":"Hope Of Life","description":"Kisah kehidupan 3 pakar bedah yang berbeza status dan latar belakang, namun mereka komited dengan kerjaya mereka sebagai doktor. Lakonan : Johnson Low, Elvis Chin, Mayjune Tan, Kelvin Liew, Jacky Kam dan Katrina Ho.","episodeNumber":0,"series":0,"repeat":"Never","dateModified":"2022-08-29T02:14:56.647","dateCreated":"0001-01-01T00:00:00"}]} \ No newline at end of file diff --git a/sites/rtmklik.rtm.gov.my/__data__/no_content.json b/sites/rtmklik.rtm.gov.my/__data__/no_content.json new file mode 100644 index 00000000..908d1528 --- /dev/null +++ b/sites/rtmklik.rtm.gov.my/__data__/no_content.json @@ -0,0 +1 @@ +{"id":2,"channel":"TV2","channelId":"2","schedule":[]} \ No newline at end of file diff --git a/sites/rtmklik.rtm.gov.my/rtmklik.rtm.gov.my.config.js b/sites/rtmklik.rtm.gov.my/rtmklik.rtm.gov.my.config.js index 623a7c83..f42a408b 100644 --- a/sites/rtmklik.rtm.gov.my/rtmklik.rtm.gov.my.config.js +++ b/sites/rtmklik.rtm.gov.my/rtmklik.rtm.gov.my.config.js @@ -1,44 +1,44 @@ -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: 'rtmklik.rtm.gov.my', - days: 2, - url: function ({ date, channel }) { - return `https://rtm.glueapi.io/v3/epg/${ - channel.site_id - }/ChannelSchedule?dateStart=${date.format('YYYY-MM-DD')}&dateEnd=${date.format( - 'YYYY-MM-DD' - )}&timezone=0` - }, - parser: function ({ content }) { - const programs = [] - const items = parseItems(content) - if (!items.length) return programs - items.forEach(item => { - programs.push({ - title: item.programTitle, - description: item.description, - start: parseTime(item.dateTimeStart), - stop: parseTime(item.dateTimeEnd) - }) - }) - - return programs - } -} - -function parseItems(content) { - const data = JSON.parse(content) - return data.schedule ? data.schedule : [] -} - -function parseTime(time) { - return dayjs.utc(time, 'YYYY-MM-DDTHH:mm:ss') -} +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: 'rtmklik.rtm.gov.my', + days: 2, + url: function ({ date, channel }) { + return `https://rtm.glueapi.io/v3/epg/${ + channel.site_id + }/ChannelSchedule?dateStart=${date.format('YYYY-MM-DD')}&dateEnd=${date.format( + 'YYYY-MM-DD' + )}&timezone=0` + }, + parser: function ({ content }) { + const programs = [] + const items = parseItems(content) + if (!items.length) return programs + items.forEach(item => { + programs.push({ + title: item.programTitle, + description: item.description, + start: parseTime(item.dateTimeStart), + stop: parseTime(item.dateTimeEnd) + }) + }) + + return programs + } +} + +function parseItems(content) { + const data = JSON.parse(content) + return data.schedule ? data.schedule : [] +} + +function parseTime(time) { + return dayjs.utc(time, 'YYYY-MM-DDTHH:mm:ss') +} diff --git a/sites/rtmklik.rtm.gov.my/rtmklik.rtm.gov.my.test.js b/sites/rtmklik.rtm.gov.my/rtmklik.rtm.gov.my.test.js index ee59eca2..2f5b56b1 100644 --- a/sites/rtmklik.rtm.gov.my/rtmklik.rtm.gov.my.test.js +++ b/sites/rtmklik.rtm.gov.my/rtmklik.rtm.gov.my.test.js @@ -1,48 +1,48 @@ -const { parser, url } = require('./rtmklik.rtm.gov.my.config.js') -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('2022-09-04', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2', - xmltv_id: 'TV2.my' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://rtm.glueapi.io/v3/epg/2/ChannelSchedule?dateStart=2022-09-04&dateEnd=2022-09-04&timezone=0' - ) -}) - -it('can parse response', () => { - const content = - '{"id":2,"channel":"TV2","channelId":"2","image":"/live_channel/tv2_Trans.png","idAuthor":9,"type":"TV","timezone":8,"dateCreated":"2022-07-08T01:22:33.233","dateModified":"2022-07-21T21:58:39.77","itemCount":30,"prev":"https://rtm-admin.glueapi.io/v3/epg/2/ChannelSchedule?dateStart=2022-09-03&dateEnd=2022-09-03","next":"https://rtm-admin.glueapi.io/v3/epg/2/ChannelSchedule?dateStart=2022-09-05&dateEnd=2022-09-05","schedule":[{"idEPGProgramSchedule":109303,"dateTimeStart":"2022-09-04T19:00:00","dateTimeEnd":"2022-09-04T20:00:00","scheduleProgramTitle":"Hope Of Life","scheduleProgramDescription":"Kisah kehidupan 3 pakar bedah yang berbeza status dan latar belakang, namun mereka komited dengan kerjaya mereka sebagai doktor. Lakonan : Johnson Low, Elvis Chin, Mayjune Tan, Kelvin Liew, Jacky Kam dan Katrina Ho.","scheduleEpisodeNumber":0,"scheduleSeries":0,"duration":3600,"idEPGProgram":3603,"programTitle":"Hope Of Life","description":"Kisah kehidupan 3 pakar bedah yang berbeza status dan latar belakang, namun mereka komited dengan kerjaya mereka sebagai doktor. Lakonan : Johnson Low, Elvis Chin, Mayjune Tan, Kelvin Liew, Jacky Kam dan Katrina Ho.","episodeNumber":0,"series":0,"repeat":"Never","dateModified":"2022-08-29T02:14:56.647","dateCreated":"0001-01-01T00:00:00"}]}' - - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-09-04T19:00:00.000Z', - stop: '2022-09-04T20:00:00.000Z', - title: 'Hope Of Life', - description: - 'Kisah kehidupan 3 pakar bedah yang berbeza status dan latar belakang, namun mereka komited dengan kerjaya mereka sebagai doktor. Lakonan : Johnson Low, Elvis Chin, Mayjune Tan, Kelvin Liew, Jacky Kam dan Katrina Ho.' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"id":2,"channel":"TV2","channelId":"2","schedule":[]}' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./rtmklik.rtm.gov.my.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('2022-09-04', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2', + xmltv_id: 'TV2.my' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://rtm.glueapi.io/v3/epg/2/ChannelSchedule?dateStart=2022-09-04&dateEnd=2022-09-04&timezone=0' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-09-04T19:00:00.000Z', + stop: '2022-09-04T20:00:00.000Z', + title: 'Hope Of Life', + description: + 'Kisah kehidupan 3 pakar bedah yang berbeza status dan latar belakang, namun mereka komited dengan kerjaya mereka sebagai doktor. Lakonan : Johnson Low, Elvis Chin, Mayjune Tan, Kelvin Liew, Jacky Kam dan Katrina Ho.' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/rtp.pt/rtp.pt.config.js b/sites/rtp.pt/rtp.pt.config.js index 0108e255..7f53e268 100644 --- a/sites/rtp.pt/rtp.pt.config.js +++ b/sites/rtp.pt/rtp.pt.config.js @@ -1,66 +1,65 @@ -const _ = require('lodash') -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) - -const tz = { - lis: 'Europe/Lisbon', - per: 'Asia/Macau', - rja: 'America/Sao_Paulo' -} - -module.exports = { - site: 'rtp.pt', - days: 2, - url({ channel, date }) { - let [region, channelCode] = channel.site_id.split('#') - return `https://www.rtp.pt/EPG/json/rtp-channels-page/list-grid/tv/${channelCode}/${date.format( - 'D-M-YYYY' - )}/${region}` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, channel) - if (!start) return - if (prev) { - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: item.name, - description: item.description, - image: parseImage(item), - start, - stop - }) - }) - - return programs - } -} - -function parseImage(item) { - const last = item.image.pop() - if (!last) return null - return last.src -} - -function parseStart(item, channel) { - let [region] = channel.site_id.split('#') - return dayjs.tz(item.date, 'YYYY-MM-DD HH:mm:ss', tz[region]) -} - -function parseItems(content) { - if (!content) return [] - const data = JSON.parse(content) - - return _.flatten(Object.values(data.result)) -} +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) + +const tz = { + lis: 'Europe/Lisbon', + per: 'Asia/Macau', + rja: 'America/Sao_Paulo' +} + +module.exports = { + site: 'rtp.pt', + days: 2, + url({ channel, date }) { + let [region, channelCode] = channel.site_id.split('#') + return `https://www.rtp.pt/EPG/json/rtp-channels-page/list-grid/tv/${channelCode}/${date.format( + 'D-M-YYYY' + )}/${region}` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, channel) + if (!start) return + if (prev) { + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: item.name, + description: item.description, + image: parseImage(item), + start, + stop + }) + }) + + return programs + } +} + +function parseImage(item) { + const last = item.image.pop() + if (!last) return null + return last.src +} + +function parseStart(item, channel) { + let [region] = channel.site_id.split('#') + return dayjs.tz(item.date, 'YYYY-MM-DD HH:mm:ss', tz[region]) +} + +function parseItems(content) { + if (!content) return [] + const data = JSON.parse(content) + + return Object.values(data.result).flat() +} diff --git a/sites/rtp.pt/rtp.pt.test.js b/sites/rtp.pt/rtp.pt.test.js index 7feeb1f6..97cf797a 100644 --- a/sites/rtp.pt/rtp.pt.test.js +++ b/sites/rtp.pt/rtp.pt.test.js @@ -1,42 +1,42 @@ -const { parser, url } = require('./rtp.pt.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('2022-12-02', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'lis#4', - xmltv_id: 'RTPMadeira.pt' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.rtp.pt/EPG/json/rtp-channels-page/list-grid/tv/4/2-12-2022/lis' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[9]).toMatchObject({ - start: '2022-12-02T23:30:00.000Z', - stop: '2022-12-03T00:00:00.000Z', - title: 'Telejornal Madeira', - description: 'Informação de proximidade. De confiança!', - image: 'https://cdn-images.rtp.pt/EPG/imagens/15790_43438_8820.png?w=384&h=216' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ content: '', channel, date }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./rtp.pt.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('2022-12-02', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'lis#4', + xmltv_id: 'RTPMadeira.pt' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.rtp.pt/EPG/json/rtp-channels-page/list-grid/tv/4/2-12-2022/lis' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[9]).toMatchObject({ + start: '2022-12-02T23:30:00.000Z', + stop: '2022-12-03T00:00:00.000Z', + title: 'Telejornal Madeira', + description: 'Informação de proximidade. De confiança!', + image: 'https://cdn-images.rtp.pt/EPG/imagens/15790_43438_8820.png?w=384&h=216' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ content: '', channel, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/ruv.is/ruv.is.config.js b/sites/ruv.is/ruv.is.config.js index 7bc6024a..5612640c 100644 --- a/sites/ruv.is/ruv.is.config.js +++ b/sites/ruv.is/ruv.is.config.js @@ -1,79 +1,79 @@ -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: 'ruv.is', - days: 2, - url({ channel, date }) { - let params = new URLSearchParams() - params.append('operationName', 'getSchedule') - params.append( - 'variables', - JSON.stringify({ channel: channel.site_id, date: date.format('YYYY-MM-DD') }) - ) - params.append( - 'extensions', - JSON.stringify({ - persistedQuery: { - version: 1, - sha256Hash: '7d133b9bd9e50127e90f2b3af1b41eb5e89cd386ed9b100b55169f395af350e6' - } - }) - ) - - return `https://www.ruv.is/gql/?${params.toString()}` - }, - parser({ content, date }) { - let programs = [] - const items = parseItems(content, date) - items.forEach(item => { - let start = parseStart(item, date) - let stop = parseStop(item, date) - if (stop.isBefore(start)) { - stop = stop.add(1, 'd') - } - programs.push({ - title: item.title, - description: item.description, - image: parseImage(item), - start, - stop - }) - }) - - return programs - } -} - -function parseImage(item) { - return item.image.replace('$$IMAGESIZE$$', '480') -} - -function parseStart(item, date) { - return dayjs.tz( - `${date.format('YYYY-MM-DD')} ${item.start_time_friendly}`, - 'YYYY-MM-DD HH:mm', - 'Atlantic/Reykjavik' - ) -} - -function parseStop(item, date) { - return dayjs.tz( - `${date.format('YYYY-MM-DD')} ${item.end_time_friendly}`, - 'YYYY-MM-DD HH:mm', - 'Atlantic/Reykjavik' - ) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.data.Schedule.events)) return [] - - return data.data.Schedule.events -} +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: 'ruv.is', + days: 2, + url({ channel, date }) { + let params = new URLSearchParams() + params.append('operationName', 'getSchedule') + params.append( + 'variables', + JSON.stringify({ channel: channel.site_id, date: date.format('YYYY-MM-DD') }) + ) + params.append( + 'extensions', + JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: '7d133b9bd9e50127e90f2b3af1b41eb5e89cd386ed9b100b55169f395af350e6' + } + }) + ) + + return `https://www.ruv.is/gql/?${params.toString()}` + }, + parser({ content, date }) { + let programs = [] + const items = parseItems(content, date) + items.forEach(item => { + let start = parseStart(item, date) + let stop = parseStop(item, date) + if (stop.isBefore(start)) { + stop = stop.add(1, 'd') + } + programs.push({ + title: item.title, + description: item.description, + image: parseImage(item), + start, + stop + }) + }) + + return programs + } +} + +function parseImage(item) { + return item.image.replace('$$IMAGESIZE$$', '480') +} + +function parseStart(item, date) { + return dayjs.tz( + `${date.format('YYYY-MM-DD')} ${item.start_time_friendly}`, + 'YYYY-MM-DD HH:mm', + 'Atlantic/Reykjavik' + ) +} + +function parseStop(item, date) { + return dayjs.tz( + `${date.format('YYYY-MM-DD')} ${item.end_time_friendly}`, + 'YYYY-MM-DD HH:mm', + 'Atlantic/Reykjavik' + ) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.data.Schedule.events)) return [] + + return data.data.Schedule.events +} diff --git a/sites/ruv.is/ruv.is.test.js b/sites/ruv.is/ruv.is.test.js index ed52d287..0deab56a 100644 --- a/sites/ruv.is/ruv.is.test.js +++ b/sites/ruv.is/ruv.is.test.js @@ -1,47 +1,47 @@ -const { parser, url } = require('./ruv.is.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('2023-01-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'ruv', - xmltv_id: 'RUV.is' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.ruv.is/gql/?operationName=getSchedule&variables=%7B%22channel%22%3A%22ruv%22%2C%22date%22%3A%222023-01-17%22%7D&extensions=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C%22sha256Hash%22%3A%227d133b9bd9e50127e90f2b3af1b41eb5e89cd386ed9b100b55169f395af350e6%22%7D%7D' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-17T13:00:00.000Z', - stop: '2023-01-17T13:10:00.000Z', - title: 'Heimaleikfimi', - description: - 'Góð ráð og æfingar sem tilvalið er að gera heima. Íris Rut Garðarsdóttir sjúkraþjálfari hefur umsjón með leikfiminni. e.', - image: - 'https://d38kdhuogyllre.cloudfront.net/fit-in/480x/filters:quality(65)/hd_posters/91pvig-3p3hig.jpg' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./ruv.is.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('2023-01-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'ruv', + xmltv_id: 'RUV.is' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.ruv.is/gql/?operationName=getSchedule&variables=%7B%22channel%22%3A%22ruv%22%2C%22date%22%3A%222023-01-17%22%7D&extensions=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C%22sha256Hash%22%3A%227d133b9bd9e50127e90f2b3af1b41eb5e89cd386ed9b100b55169f395af350e6%22%7D%7D' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-17T13:00:00.000Z', + stop: '2023-01-17T13:10:00.000Z', + title: 'Heimaleikfimi', + description: + 'Góð ráð og æfingar sem tilvalið er að gera heima. Íris Rut Garðarsdóttir sjúkraþjálfari hefur umsjón með leikfiminni. e.', + image: + 'https://d38kdhuogyllre.cloudfront.net/fit-in/480x/filters:quality(65)/hd_posters/91pvig-3p3hig.jpg' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/s.mxtv.jp/__data__/content.json b/sites/s.mxtv.jp/__data__/content.json new file mode 100644 index 00000000..c67c0663 --- /dev/null +++ b/sites/s.mxtv.jp/__data__/content.json @@ -0,0 +1 @@ +[{ "Event_id": "0x6a57", "Start_time": "2024年07月27日05時00分00秒", "Duration": "01:00:00", "Event_name": "ヒーリングタイム&ヘッドラインニュース", "Event_text": "ねこの足跡", "Component": "480i 16:9 パンベクトルなし", "Sound": "ステレオ", "Event_detail": ""}] \ No newline at end of file diff --git a/sites/s.mxtv.jp/s.mxtv.jp.config.js b/sites/s.mxtv.jp/s.mxtv.jp.config.js index d85d3c06..0becd41f 100644 --- a/sites/s.mxtv.jp/s.mxtv.jp.config.js +++ b/sites/s.mxtv.jp/s.mxtv.jp.config.js @@ -1,83 +1,83 @@ -const dayjs = require('dayjs') -const duration = require('dayjs/plugin/duration') -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) -dayjs.extend(duration) - -module.exports = { - site: 's.mxtv.jp', - days: 1, - lang: 'ja', - url: function ({ date, channel }) { - const id = `SV${channel.site_id}EPG${date.format('YYYYMMDD')}` - return `https://s.mxtv.jp/bangumi_file/json01/${id}.json` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.Event_name, - description: item.Event_text, - category: parseCategory(item), - image: parseImage(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - return programs - }, - channels() { - return [ - { - lang: 'ja', - site_id: '1', - name: 'Tokyo MX1', - xmltv_id: 'TokyoMX1.jp' - }, - { - lang: 'ja', - site_id: '2', - name: 'Tokyo MX2', - xmltv_id: 'TokyoMX2.jp' - } - ] - } -} - -function parseImage() { - // Should return a string if we can output an image URL - // Might be done with `https://s.mxtv.jp/bangumi/link/weblinkU.csv?1722421896752` ? - return null -} - -function parseCategory() { - // Should return a string if we can determine the category - // Might be done with `https://s.mxtv.jp/index_set/csv/ranking_bangumi_allU.csv` ? - return null -} - -function parseStart(item) { - return dayjs.tz(item.Start_time.toString(), 'YYYY年MM月DD日HH時mm分ss秒', 'Asia/Tokyo') -} - -function parseStop(item) { - // Add the duration to the start time - const durationDate = dayjs(item.Duration, 'HH:mm:ss') - return parseStart(item).add( - dayjs.duration({ - hours: durationDate.hour(), - minutes: durationDate.minute(), - seconds: durationDate.second() - }) - ) -} - -function parseItems(content) { - return JSON.parse(content) || [] -} +const dayjs = require('dayjs') +const duration = require('dayjs/plugin/duration') +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) +dayjs.extend(duration) + +module.exports = { + site: 's.mxtv.jp', + days: 1, + lang: 'ja', + url: function ({ date, channel }) { + const id = `SV${channel.site_id}EPG${date.format('YYYYMMDD')}` + return `https://s.mxtv.jp/bangumi_file/json01/${id}.json` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.Event_name, + description: item.Event_text, + category: parseCategory(item), + image: parseImage(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + return programs + }, + channels() { + return [ + { + lang: 'ja', + site_id: '1', + name: 'Tokyo MX1', + xmltv_id: 'TokyoMX1.jp' + }, + { + lang: 'ja', + site_id: '2', + name: 'Tokyo MX2', + xmltv_id: 'TokyoMX2.jp' + } + ] + } +} + +function parseImage() { + // Should return a string if we can output an image URL + // Might be done with `https://s.mxtv.jp/bangumi/link/weblinkU.csv?1722421896752` ? + return null +} + +function parseCategory() { + // Should return a string if we can determine the category + // Might be done with `https://s.mxtv.jp/index_set/csv/ranking_bangumi_allU.csv` ? + return null +} + +function parseStart(item) { + return dayjs.tz(item.Start_time.toString(), 'YYYY年MM月DD日HH時mm分ss秒', 'Asia/Tokyo') +} + +function parseStop(item) { + // Add the duration to the start time + const durationDate = dayjs(item.Duration, 'HH:mm:ss') + return parseStart(item).add( + dayjs.duration({ + hours: durationDate.hour(), + minutes: durationDate.minute(), + seconds: durationDate.second() + }) + ) +} + +function parseItems(content) { + return JSON.parse(content) || [] +} diff --git a/sites/s.mxtv.jp/s.mxtv.jp.test.js b/sites/s.mxtv.jp/s.mxtv.jp.test.js index b6891abc..fe47492b 100644 --- a/sites/s.mxtv.jp/s.mxtv.jp.test.js +++ b/sites/s.mxtv.jp/s.mxtv.jp.test.js @@ -1,48 +1,49 @@ -const { parser, url } = require('./s.mxtv.jp.config.js') -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('2024-08-01', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2', - name: 'Tokyo MX2', - xmltv_id: 'TokyoMX2.jp' -} -const content = - '[{ "Event_id": "0x6a57", "Start_time": "2024年07月27日05時00分00秒", "Duration": "01:00:00", "Event_name": "ヒーリングタイム&ヘッドラインニュース", "Event_text": "ねこの足跡", "Component": "480i 16:9 パンベクトルなし", "Sound": "ステレオ", "Event_detail": ""}]' - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe('https://s.mxtv.jp/bangumi_file/json01/SV2EPG20240801.json') -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2024-07-26T20:00:00.000Z', // UTC time - stop: '2024-07-26T21:00:00.000Z', // UTC - title: 'ヒーリングタイム&ヘッドラインニュース', - description: 'ねこの足跡', - image: null, - category: null - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '[]' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./s.mxtv.jp.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('2024-08-01', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2', + name: 'Tokyo MX2', + xmltv_id: 'TokyoMX2.jp' +} + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe('https://s.mxtv.jp/bangumi_file/json01/SV2EPG20240801.json') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2024-07-26T20:00:00.000Z', // UTC time + stop: '2024-07-26T21:00:00.000Z', // UTC + title: 'ヒーリングタイム&ヘッドラインニュース', + description: 'ねこの足跡', + image: null, + category: null + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '[]' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/sat.tv/sat.tv.config.js b/sites/sat.tv/sat.tv.config.js index 204ab99a..e342db6a 100644 --- a/sites/sat.tv/sat.tv.config.js +++ b/sites/sat.tv/sat.tv.config.js @@ -1,173 +1,173 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -const API_ENDPOINT = 'https://www.sat.tv/wp-content/themes/twentytwenty-child/ajax_chaines.php' - -module.exports = { - site: 'sat.tv', - days: 2, - url: API_ENDPOINT, - request: { - method: 'POST', - headers({ channel }) { - return { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - Cookie: `pll_language=${channel.lang}` - } - }, - data({ channel, date }) { - const [satSatellite, satLineup] = channel.site_id.split('#') - const params = new URLSearchParams() - params.append('dateFiltre', date.format('YYYY-MM-DD')) - params.append('hoursFiltre', '0') - params.append('satLineup', satLineup) - params.append('satSatellite', satSatellite) - params.append('userDateTime', date.valueOf()) - params.append('userTimezone', 'Europe/London') - - return params - }, - cache: { - ttl: 60 * 60 * 1000 // 1h - } - }, - parser: function ({ content, date, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - let $item = cheerio.load(item) - let start = parseStart($item, date) - let duration = parseDuration($item) - let stop = start.add(duration, 'm') - - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - image: parseImage($item), - start, - stop - }) - }) - - return programs - }, - async channels({ lang }) { - const satellites = [ - { satellite: 2, lineup: 55 }, - { satellite: 2, lineup: 58 }, - { satellite: 2, lineup: 53 }, - { satellite: 2, lineup: 57 }, - { satellite: 2, lineup: 54 }, - { satellite: 2, lineup: 56 }, - { satellite: 1, lineup: 48 }, - { satellite: 1, lineup: 44 }, - { satellite: 1, lineup: 42 }, - { satellite: 1, lineup: 39 }, - { satellite: 1, lineup: 37 }, - { satellite: 1, lineup: 38 }, - { satellite: 1, lineup: 68 }, - { satellite: 1, lineup: 47 }, - { satellite: 1, lineup: 41 }, - { satellite: 1, lineup: 49 }, - { satellite: 1, lineup: 46 }, - { satellite: 1, lineup: 35 }, - { satellite: 1, lineup: 43 }, - { satellite: 1, lineup: 45 }, - { satellite: 1, lineup: 50 }, - { satellite: 1, lineup: 71 }, - { satellite: 1, lineup: 40 }, - { satellite: 1, lineup: 72 }, - { satellite: 1, lineup: 33 }, - { satellite: 8, lineup: 62 }, - { satellite: 8, lineup: 63 }, - { satellite: 8, lineup: 64 }, - { satellite: 8, lineup: 65 }, - { satellite: 8, lineup: 66 }, - { satellite: 8, lineup: 67 } - ] - - let channels = [] - for (let sat of satellites) { - const params = new URLSearchParams() - params.append('dateFiltre', dayjs().format('YYYY-MM-DD')) - params.append('hoursFiltre', '0') - params.append('satLineup', sat.lineup) - params.append('satSatellite', sat.satellite) - params.append('userDateTime', dayjs().valueOf()) - params.append('userTimezone', 'Europe/London') - const data = await axios - .post(API_ENDPOINT, params, { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - Cookie: `pll_language=${lang}` - } - }) - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(data) - $('.main-container-channels-events > .container-channel-events').each((i, el) => { - const name = $(el).find('.channel-title').text().trim() - const channelId = name.replace(/\s&\s/gi, ' & ') - - if (!name) return - - channels.push({ - lang, - site_id: `${sat.satellite}#${sat.lineup}#${channelId}`, - name - }) - }) - } - - return channels - } -} - -function parseImage($item) { - const src = $item('.event-logo img:not(.no-img)').attr('src') - - return src ? `https://sat.tv${src}` : null -} - -function parseTitle($item) { - return $item('.event-data-title').text() -} - -function parseDescription($item) { - return $item('.event-data-desc').text() -} - -function parseStart($item, date) { - let eventDataDate = $item('.event-data-date').text().trim() - let [, time] = eventDataDate.match(/(\d{2}:\d{2})/) || [null, null] - if (!time) return null - - return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm') -} - -function parseDuration($item) { - let eventDataInfo = $item('.event-data-info').text().trim() - let [, h, m] = eventDataInfo.match(/(\d{2})h(\d{2})/) || [null, 0, 0] - - return parseInt(h) * 60 + parseInt(m) -} - -function parseItems(content, channel) { - const [, , site_id] = channel.site_id.split('#') - const $ = cheerio.load(content) - const channelData = $('.main-container-channels-events > .container-channel-events') - .filter((index, el) => { - return $(el).find('.channel-title').text().trim() === site_id - }) - .first() - if (!channelData) return [] - - return $(channelData).find('.container-event').toArray() -} +const axios = require('axios') +const dayjs = require('dayjs') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +const API_ENDPOINT = 'https://www.sat.tv/wp-content/themes/twentytwenty-child/ajax_chaines.php' + +module.exports = { + site: 'sat.tv', + days: 2, + url: API_ENDPOINT, + request: { + method: 'POST', + headers({ channel }) { + return { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + Cookie: `pll_language=${channel.lang}` + } + }, + data({ channel, date }) { + const [satSatellite, satLineup] = channel.site_id.split('#') + const params = new URLSearchParams() + params.append('dateFiltre', date.format('YYYY-MM-DD')) + params.append('hoursFiltre', '0') + params.append('satLineup', satLineup) + params.append('satSatellite', satSatellite) + params.append('userDateTime', date.valueOf()) + params.append('userTimezone', 'Europe/London') + + return params + }, + cache: { + ttl: 60 * 60 * 1000 // 1h + } + }, + parser: function ({ content, date, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + let $item = cheerio.load(item) + let start = parseStart($item, date) + let duration = parseDuration($item) + let stop = start.add(duration, 'm') + + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + image: parseImage($item), + start, + stop + }) + }) + + return programs + }, + async channels({ lang }) { + const satellites = [ + { satellite: 2, lineup: 55 }, + { satellite: 2, lineup: 58 }, + { satellite: 2, lineup: 53 }, + { satellite: 2, lineup: 57 }, + { satellite: 2, lineup: 54 }, + { satellite: 2, lineup: 56 }, + { satellite: 1, lineup: 48 }, + { satellite: 1, lineup: 44 }, + { satellite: 1, lineup: 42 }, + { satellite: 1, lineup: 39 }, + { satellite: 1, lineup: 37 }, + { satellite: 1, lineup: 38 }, + { satellite: 1, lineup: 68 }, + { satellite: 1, lineup: 47 }, + { satellite: 1, lineup: 41 }, + { satellite: 1, lineup: 49 }, + { satellite: 1, lineup: 46 }, + { satellite: 1, lineup: 35 }, + { satellite: 1, lineup: 43 }, + { satellite: 1, lineup: 45 }, + { satellite: 1, lineup: 50 }, + { satellite: 1, lineup: 71 }, + { satellite: 1, lineup: 40 }, + { satellite: 1, lineup: 72 }, + { satellite: 1, lineup: 33 }, + { satellite: 8, lineup: 62 }, + { satellite: 8, lineup: 63 }, + { satellite: 8, lineup: 64 }, + { satellite: 8, lineup: 65 }, + { satellite: 8, lineup: 66 }, + { satellite: 8, lineup: 67 } + ] + + let channels = [] + for (let sat of satellites) { + const params = new URLSearchParams() + params.append('dateFiltre', dayjs().format('YYYY-MM-DD')) + params.append('hoursFiltre', '0') + params.append('satLineup', sat.lineup) + params.append('satSatellite', sat.satellite) + params.append('userDateTime', dayjs().valueOf()) + params.append('userTimezone', 'Europe/London') + const data = await axios + .post(API_ENDPOINT, params, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + Cookie: `pll_language=${lang}` + } + }) + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(data) + $('.main-container-channels-events > .container-channel-events').each((i, el) => { + const name = $(el).find('.channel-title').text().trim() + const channelId = name.replace(/\s&\s/gi, ' & ') + + if (!name) return + + channels.push({ + lang, + site_id: `${sat.satellite}#${sat.lineup}#${channelId}`, + name + }) + }) + } + + return channels + } +} + +function parseImage($item) { + const src = $item('.event-logo img:not(.no-img)').attr('src') + + return src ? `https://sat.tv${src}` : null +} + +function parseTitle($item) { + return $item('.event-data-title').text() +} + +function parseDescription($item) { + return $item('.event-data-desc').text() +} + +function parseStart($item, date) { + let eventDataDate = $item('.event-data-date').text().trim() + let [, time] = eventDataDate.match(/(\d{2}:\d{2})/) || [null, null] + if (!time) return null + + return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm') +} + +function parseDuration($item) { + let eventDataInfo = $item('.event-data-info').text().trim() + let [, h, m] = eventDataInfo.match(/(\d{2})h(\d{2})/) || [null, 0, 0] + + return parseInt(h) * 60 + parseInt(m) +} + +function parseItems(content, channel) { + const [, , site_id] = channel.site_id.split('#') + const $ = cheerio.load(content) + const channelData = $('.main-container-channels-events > .container-channel-events') + .filter((index, el) => { + return $(el).find('.channel-title').text().trim() === site_id + }) + .first() + if (!channelData) return [] + + return $(channelData).find('.container-event').toArray() +} diff --git a/sites/sat.tv/sat.tv.test.js b/sites/sat.tv/sat.tv.test.js index e7e95780..7c8ef56c 100644 --- a/sites/sat.tv/sat.tv.test.js +++ b/sites/sat.tv/sat.tv.test.js @@ -1,112 +1,112 @@ -const { parser, url, request } = require('./sat.tv.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('2023-06-26', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1#38#السعودية', - xmltv_id: 'AlSaudiya.sa', - lang: 'ar' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.sat.tv/wp-content/themes/twentytwenty-child/ajax_chaines.php') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers({ channel })).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - Cookie: 'pll_language=ar' - }) -}) - -it('can generate valid request data', () => { - const data = request.data({ channel, date }) - expect(data.get('dateFiltre')).toBe('2023-06-26') - expect(data.get('hoursFiltre')).toBe('0') - expect(data.get('satLineup')).toBe('38') - expect(data.get('satSatellite')).toBe('1') - expect(data.get('userDateTime')).toBe('1687737600000') - expect(data.get('userTimezone')).toBe('Europe/London') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_ar.html')) - const results = parser({ content, date, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(35) - expect(results[0]).toMatchObject({ - start: '2023-06-26T06:30:00.000Z', - stop: '2023-06-26T07:00:00.000Z', - title: 'تعظيم البلد الحرام', - description: `الناس, دين, ثقافة -يلقي صانع الفيلم الضوء على مشروع تعظيم البلد الحرام في مكة من العائلة الملكية في المملكة العربية السعودية، والذي يهدف لإبراز حرمته لدى المسلمين حول العالم.`, - image: null - }) - - expect(results[34]).toMatchObject({ - start: '2023-06-26T22:30:00.000Z', - stop: '2023-06-27T01:00:00.000Z', - title: 'الأخبار', - description: `نشرة -.يطرح أهم القضايا والأحداث على الساحة السعودية والعالمية`, - image: - 'https://sat.tv/wp-content/themes/twentytwenty-child/data_lineups/nilesat/images3/epg-3077892.jpg' - }) -}) - -it('can parse response in english', () => { - const channel = { - site_id: '1#38#Saudi HD', - xmltv_id: 'AlSaudiya.sa', - lang: 'en' - } - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_en.html')) - const results = parser({ content, date, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(32) - expect(results[0]).toMatchObject({ - start: '2023-06-26T09:00:00.000Z', - stop: '2023-06-26T10:00:00.000Z', - title: 'News', - description: `Newscast -The most important issues and events on the Saudi and the world.`, - image: - 'https://sat.tv/wp-content/themes/twentytwenty-child/data_lineups/nilesat/images3/epg-3077892.jpg' - }) - - expect(results[31]).toMatchObject({ - start: '2023-06-26T23:15:00.000Z', - stop: '2023-06-27T00:00:00.000Z', - title: "Bride's Father", - description: `Romance, Drama, Family -2022 -Abdelhamid's family struggles to deal with the challenges of life that keep flowing one by one. they manage to stay strong-armed with their love and trust for each other. -Sayed Ragab, Sawsan Badr, Medhat Saleh, Nermine Al Feqy, Mohamed Adel, Khaled Kamal, Rania Farid, Hani Kamal, Hani Kamal`, - image: - 'https://sat.tv/wp-content/themes/twentytwenty-child/data_lineups/nilesat/images3/epg-3157177.jpg' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - const result = parser({ content, date, channel }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./sat.tv.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('2023-06-26', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1#38#السعودية', + xmltv_id: 'AlSaudiya.sa', + lang: 'ar' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.sat.tv/wp-content/themes/twentytwenty-child/ajax_chaines.php') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers({ channel })).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + Cookie: 'pll_language=ar' + }) +}) + +it('can generate valid request data', () => { + const data = request.data({ channel, date }) + expect(data.get('dateFiltre')).toBe('2023-06-26') + expect(data.get('hoursFiltre')).toBe('0') + expect(data.get('satLineup')).toBe('38') + expect(data.get('satSatellite')).toBe('1') + expect(data.get('userDateTime')).toBe('1687737600000') + expect(data.get('userTimezone')).toBe('Europe/London') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_ar.html')) + const results = parser({ content, date, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(35) + expect(results[0]).toMatchObject({ + start: '2023-06-26T06:30:00.000Z', + stop: '2023-06-26T07:00:00.000Z', + title: 'تعظيم البلد الحرام', + description: `الناس, دين, ثقافة +يلقي صانع الفيلم الضوء على مشروع تعظيم البلد الحرام في مكة من العائلة الملكية في المملكة العربية السعودية، والذي يهدف لإبراز حرمته لدى المسلمين حول العالم.`, + image: null + }) + + expect(results[34]).toMatchObject({ + start: '2023-06-26T22:30:00.000Z', + stop: '2023-06-27T01:00:00.000Z', + title: 'الأخبار', + description: `نشرة +.يطرح أهم القضايا والأحداث على الساحة السعودية والعالمية`, + image: + 'https://sat.tv/wp-content/themes/twentytwenty-child/data_lineups/nilesat/images3/epg-3077892.jpg' + }) +}) + +it('can parse response in english', () => { + const channel = { + site_id: '1#38#Saudi HD', + xmltv_id: 'AlSaudiya.sa', + lang: 'en' + } + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_en.html')) + const results = parser({ content, date, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(32) + expect(results[0]).toMatchObject({ + start: '2023-06-26T09:00:00.000Z', + stop: '2023-06-26T10:00:00.000Z', + title: 'News', + description: `Newscast +The most important issues and events on the Saudi and the world.`, + image: + 'https://sat.tv/wp-content/themes/twentytwenty-child/data_lineups/nilesat/images3/epg-3077892.jpg' + }) + + expect(results[31]).toMatchObject({ + start: '2023-06-26T23:15:00.000Z', + stop: '2023-06-27T00:00:00.000Z', + title: "Bride's Father", + description: `Romance, Drama, Family +2022 +Abdelhamid's family struggles to deal with the challenges of life that keep flowing one by one. they manage to stay strong-armed with their love and trust for each other. +Sayed Ragab, Sawsan Badr, Medhat Saleh, Nermine Al Feqy, Mohamed Adel, Khaled Kamal, Rania Farid, Hani Kamal, Hani Kamal`, + image: + 'https://sat.tv/wp-content/themes/twentytwenty-child/data_lineups/nilesat/images3/epg-3157177.jpg' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + const result = parser({ content, date, channel }) + expect(result).toMatchObject([]) +}) diff --git a/sites/shahid.mbc.net/__data__/content.json b/sites/shahid.mbc.net/__data__/content.json new file mode 100644 index 00000000..eb889329 --- /dev/null +++ b/sites/shahid.mbc.net/__data__/content.json @@ -0,0 +1 @@ +{"items":[{"channelId":"996520","items":[{"actualFrom":"2023-11-11T00:00:00.000+00:00","actualTo":"2023-11-11T00:30:00.000+00:00","description":"The presenter reviews the most prominent episodes of news programs produced by the channel's team on a weekly basis, which include the most important global updates and developments at all levels.","duration":null,"emptySlot":false,"episodeNumber":194,"from":"2023-11-11T00:00:00.000+00:00","genres":["TV Show"],"productId":null,"productionYear":null,"productPoster":"https://imagesmbc.whatsonindia.com/dasimages/landscape/1920x1080/F968D4A39DB25793E9EED1BDAFBAD2EA8A8F9B30Z.jpg","productSubType":null,"productType":null,"replay":false,"restritectContent":null,"seasonId":null,"seasonNumber":"1","showId":null,"streamInfo":null,"title":"Menassaatona Fi Osboo'","to":"2023-11-11T00:30:00.000+00:00"}]}]} \ No newline at end of file diff --git a/sites/shahid.mbc.net/shahid.mbc.net.config.js b/sites/shahid.mbc.net/shahid.mbc.net.config.js index a847c769..f3ac9087 100644 --- a/sites/shahid.mbc.net/shahid.mbc.net.config.js +++ b/sites/shahid.mbc.net/shahid.mbc.net.config.js @@ -1,79 +1,79 @@ -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: 'shahid.mbc.net', - days: 2, - url({ channel, date }) { - return `https://api2.shahid.net/proxy/v2.1/shahid-epg-api/?csvChannelIds=${ - channel.site_id - }&from=${date.format('YYYY-MM-DD')}T00:00:00.000Z&to=${date.format( - 'YYYY-MM-DD' - )}T23:59:59.999Z&country=SA&language=${channel.lang}&Accept-Language=${channel.lang}` - }, - parser({ content, channel }) { - const programs = parseItems(content, channel).map(item => { - return { - title: item.title, - description: item.description, - session: item.seasonNumber, - episode: item.episodeNumber, - start: dayjs.tz(item.actualFrom, 'Asia/Riyadh').toISOString(), - stop: dayjs.tz(item.actualTo, 'Asia/Riyadh').toISOString() - } - }) - - return programs - }, - async channels({ lang = 'en' }) { - const axios = require('axios') - const items = [] - let page = 0 - while (true) { - const result = await axios - .get( - `https://api2.shahid.net/proxy/v2.1/product/filter?filter=%7B"pageNumber":${page},"pageSize":100,"productType":"LIVESTREAM","productSubType":"LIVE_CHANNEL"%7D&country=SA&language=${lang}&Accept-Language=${lang}` - ) - .then(response => response.data) - .catch(console.error) - if (result.productList) { - items.push(...result.productList.products) - if (result.productList.hasMore) { - page++ - continue - } - } - break - } - const channels = items.map(channel => { - return { - lang, - site_id: channel.id, - name: channel.title - } - }) - - return channels - } -} - -function parseItems(content, channel) { - const items = [] - content = content ? JSON.parse(content) : [] - if (content.items) { - content.items.forEach(schedules => { - if (schedules.channelId == channel.site_id) { - items.push(...schedules.items) - return true - } - }) - } - - return items -} +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: 'shahid.mbc.net', + days: 2, + url({ channel, date }) { + return `https://api2.shahid.net/proxy/v2.1/shahid-epg-api/?csvChannelIds=${ + channel.site_id + }&from=${date.format('YYYY-MM-DD')}T00:00:00.000Z&to=${date.format( + 'YYYY-MM-DD' + )}T23:59:59.999Z&country=SA&language=${channel.lang}&Accept-Language=${channel.lang}` + }, + parser({ content, channel }) { + const programs = parseItems(content, channel).map(item => { + return { + title: item.title, + description: item.description, + session: item.seasonNumber, + episode: item.episodeNumber, + start: dayjs.tz(item.actualFrom, 'Asia/Riyadh').toISOString(), + stop: dayjs.tz(item.actualTo, 'Asia/Riyadh').toISOString() + } + }) + + return programs + }, + async channels({ lang = 'en' }) { + const axios = require('axios') + const items = [] + let page = 0 + while (true) { + const result = await axios + .get( + `https://api2.shahid.net/proxy/v2.1/product/filter?filter=%7B"pageNumber":${page},"pageSize":100,"productType":"LIVESTREAM","productSubType":"LIVE_CHANNEL"%7D&country=SA&language=${lang}&Accept-Language=${lang}` + ) + .then(response => response.data) + .catch(console.error) + if (result.productList) { + items.push(...result.productList.products) + if (result.productList.hasMore) { + page++ + continue + } + } + break + } + const channels = items.map(channel => { + return { + lang, + site_id: channel.id, + name: channel.title + } + }) + + return channels + } +} + +function parseItems(content, channel) { + const items = [] + content = content ? JSON.parse(content) : [] + if (content.items) { + content.items.forEach(schedules => { + if (schedules.channelId == channel.site_id) { + items.push(...schedules.items) + return true + } + }) + } + + return items +} diff --git a/sites/shahid.mbc.net/shahid.mbc.net.test.js b/sites/shahid.mbc.net/shahid.mbc.net.test.js index 7cc03032..c804fa44 100644 --- a/sites/shahid.mbc.net/shahid.mbc.net.test.js +++ b/sites/shahid.mbc.net/shahid.mbc.net.test.js @@ -1,40 +1,41 @@ -const { url, parser } = require('./shahid.mbc.net.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -const date = dayjs.utc('2023-11-11').startOf('d') -const channel = { site_id: '996520', xmltv_id: 'AlAanTV.ae', lang: 'en' } - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - `https://api2.shahid.net/proxy/v2.1/shahid-epg-api/?csvChannelIds=${ - channel.site_id - }&from=${date.format('YYYY-MM-DD')}T00:00:00.000Z&to=${date.format( - 'YYYY-MM-DD' - )}T23:59:59.999Z&country=SA&language=${channel.lang}&Accept-Language=${channel.lang}` - ) -}) - -it('can parse response', () => { - const content = - '{"items":[{"channelId":"996520","items":[{"actualFrom":"2023-11-11T00:00:00.000+00:00","actualTo":"2023-11-11T00:30:00.000+00:00","description":"The presenter reviews the most prominent episodes of news programs produced by the channel\'s team on a weekly basis, which include the most important global updates and developments at all levels.","duration":null,"emptySlot":false,"episodeNumber":194,"from":"2023-11-11T00:00:00.000+00:00","genres":["TV Show"],"productId":null,"productionYear":null,"productPoster":"https://imagesmbc.whatsonindia.com/dasimages/landscape/1920x1080/F968D4A39DB25793E9EED1BDAFBAD2EA8A8F9B30Z.jpg","productSubType":null,"productType":null,"replay":false,"restritectContent":null,"seasonId":null,"seasonNumber":"1","showId":null,"streamInfo":null,"title":"Menassaatona Fi Osboo\'","to":"2023-11-11T00:30:00.000+00:00"}]}]}' - const result = parser({ content, channel, date }) - - expect(result).toMatchObject([ - { - start: '2023-11-10T21:00:00.000Z', - stop: '2023-11-10T21:30:00.000Z', - title: "Menassaatona Fi Osboo'", - description: - "The presenter reviews the most prominent episodes of news programs produced by the channel's team on a weekly basis, which include the most important global updates and developments at all levels." - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ content: '' }) - - expect(result).toMatchObject([]) -}) +const { url, parser } = require('./shahid.mbc.net.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +const date = dayjs.utc('2023-11-11').startOf('d') +const channel = { site_id: '996520', xmltv_id: 'AlAanTV.ae', lang: 'en' } + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + `https://api2.shahid.net/proxy/v2.1/shahid-epg-api/?csvChannelIds=${ + channel.site_id + }&from=${date.format('YYYY-MM-DD')}T00:00:00.000Z&to=${date.format( + 'YYYY-MM-DD' + )}T23:59:59.999Z&country=SA&language=${channel.lang}&Accept-Language=${channel.lang}` + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content, channel, date }) + + expect(result).toMatchObject([ + { + start: '2023-11-10T21:00:00.000Z', + stop: '2023-11-10T21:30:00.000Z', + title: "Menassaatona Fi Osboo'", + description: + "The presenter reviews the most prominent episodes of news programs produced by the channel's team on a weekly basis, which include the most important global updates and developments at all levels." + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ content: '' }) + + expect(result).toMatchObject([]) +}) diff --git a/sites/siba.com.co/__data__/content.json b/sites/siba.com.co/__data__/content.json new file mode 100644 index 00000000..eaddd574 --- /dev/null +++ b/sites/siba.com.co/__data__/content.json @@ -0,0 +1 @@ +{"list":[{"id":"395","nom":"CANAL CLARO","num":"102","logo":"7c4b9e8566a6e867d1db4c7ce845f1f4.jpg","cat":"Exclusivos Claro","prog":[{"id":"665724465","nom":"Worst Cooks In America","ini":1636588800,"fin":1636592400}]}],"error":null} \ No newline at end of file diff --git a/sites/siba.com.co/__data__/no_content.json b/sites/siba.com.co/__data__/no_content.json new file mode 100644 index 00000000..97850767 --- /dev/null +++ b/sites/siba.com.co/__data__/no_content.json @@ -0,0 +1 @@ +{"list":[],"error":null} \ No newline at end of file diff --git a/sites/siba.com.co/siba.com.co.config.js b/sites/siba.com.co/siba.com.co.config.js index 970815be..d1ef37d2 100644 --- a/sites/siba.com.co/siba.com.co.config.js +++ b/sites/siba.com.co/siba.com.co.config.js @@ -1,56 +1,56 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'siba.com.co', - days: 2, - url: 'http://devportal.siba.com.co/index.php?action=grilla', - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }, - data({ channel, date }) { - const params = new URLSearchParams() - params.append('servicio', '10') - params.append('ini', date.unix()) - params.append('end', date.add(1, 'd').unix()) - params.append('chn', channel.site_id) - - return params - } - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.nom, - start: parseStart(item).toJSON(), - stop: parseStop(item).toJSON() - }) - }) - - return programs - } -} - -function parseStart(item) { - return dayjs.unix(item.ini) -} - -function parseStop(item) { - return dayjs.unix(item.fin) -} - -function parseContent(content, channel) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.list)) return null - - return data.list.find(i => i.id === channel.site_id) -} - -function parseItems(content, channel) { - const data = parseContent(content, channel) - - return data ? data.prog : [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'siba.com.co', + days: 2, + url: 'http://devportal.siba.com.co/index.php?action=grilla', + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }, + data({ channel, date }) { + const params = new URLSearchParams() + params.append('servicio', '10') + params.append('ini', date.unix()) + params.append('end', date.add(1, 'd').unix()) + params.append('chn', channel.site_id) + + return params + } + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.nom, + start: parseStart(item).toJSON(), + stop: parseStop(item).toJSON() + }) + }) + + return programs + } +} + +function parseStart(item) { + return dayjs.unix(item.ini) +} + +function parseStop(item) { + return dayjs.unix(item.fin) +} + +function parseContent(content, channel) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.list)) return null + + return data.list.find(i => i.id === channel.site_id) +} + +function parseItems(content, channel) { + const data = parseContent(content, channel) + + return data ? data.prog : [] +} diff --git a/sites/siba.com.co/siba.com.co.test.js b/sites/siba.com.co/siba.com.co.test.js index d63a0346..c804e185 100644 --- a/sites/siba.com.co/siba.com.co.test.js +++ b/sites/siba.com.co/siba.com.co.test.js @@ -1,52 +1,54 @@ -const { parser, url, request } = require('./siba.com.co.config.js') -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('2021-11-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '395', - xmltv_id: 'CanalClaro.cl' -} -const content = - '{"list":[{"id":"395","nom":"CANAL CLARO","num":"102","logo":"7c4b9e8566a6e867d1db4c7ce845f1f4.jpg","cat":"Exclusivos Claro","prog":[{"id":"665724465","nom":"Worst Cooks In America","ini":1636588800,"fin":1636592400}]}],"error":null}' - -it('can generate valid url', () => { - expect(url).toBe('http://devportal.siba.com.co/index.php?action=grilla') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }) -}) - -it('can generate valid request data', () => { - const result = request.data({ channel, date }) - expect(result.has('servicio')).toBe(true) - expect(result.has('ini')).toBe(true) - expect(result.has('end')).toBe(true) - expect(result.has('chn')).toBe(true) -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: '2021-11-11T00:00:00.000Z', - stop: '2021-11-11T01:00:00.000Z', - title: 'Worst Cooks In America' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"list":[],"error":null}' - }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./siba.com.co.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('2021-11-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '395', + xmltv_id: 'CanalClaro.cl' +} + +it('can generate valid url', () => { + expect(url).toBe('http://devportal.siba.com.co/index.php?action=grilla') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }) +}) + +it('can generate valid request data', () => { + const result = request.data({ channel, date }) + expect(result.has('servicio')).toBe(true) + expect(result.has('ini')).toBe(true) + expect(result.has('end')).toBe(true) + expect(result.has('chn')).toBe(true) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: '2021-11-11T00:00:00.000Z', + stop: '2021-11-11T01:00:00.000Z', + title: 'Worst Cooks In America' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/singtel.com/singtel.com.config.js b/sites/singtel.com/singtel.com.config.js index 07d3a33e..26d6301c 100644 --- a/sites/singtel.com/singtel.com.config.js +++ b/sites/singtel.com/singtel.com.config.js @@ -1,68 +1,68 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'singtel.com', - days: 3, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url({ date }) { - return `https://www.singtel.com/etc/singtel/public/tv/epg-parsed-data/${date.format( - 'DDMMYYYY' - )}.json` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const start = dayjs.tz(item.startDateTime, 'Asia/Singapore') - const stop = start.add(item.duration, 's') - programs.push({ - title: item.program.title, - category: item.program.subCategory, - description: item.program.description, - start, - stop - }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - const cheerio = require('cheerio') - - const data = await axios - .get('https://www.singtel.com/personal/products-services/tv/tv-programme-guide') - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(data) - let datamodel = $('ux-tv-channel-epg').attr('datamodel') - datamodel = JSON.parse(datamodel) - - return datamodel.tvChannelLists.map(item => { - return { - lang: 'en', - site_id: item.epgChannelId, - name: item.title.trim() - } - }) - } -} - -function parseItems(content, channel) { - try { - const data = JSON.parse(content) - return data && data[channel.site_id] ? data[channel.site_id] : [] - } catch { - return [] - } -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'singtel.com', + days: 3, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url({ date }) { + return `https://www.singtel.com/etc/singtel/public/tv/epg-parsed-data/${date.format( + 'DDMMYYYY' + )}.json` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const start = dayjs.tz(item.startDateTime, 'Asia/Singapore') + const stop = start.add(item.duration, 's') + programs.push({ + title: item.program.title, + category: item.program.subCategory, + description: item.program.description, + start, + stop + }) + }) + + return programs + }, + async channels() { + const axios = require('axios') + const cheerio = require('cheerio') + + const data = await axios + .get('https://www.singtel.com/personal/products-services/tv/tv-programme-guide') + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(data) + let datamodel = $('ux-tv-channel-epg').attr('datamodel') + datamodel = JSON.parse(datamodel) + + return datamodel.tvChannelLists.map(item => { + return { + lang: 'en', + site_id: item.epgChannelId, + name: item.title.trim() + } + }) + } +} + +function parseItems(content, channel) { + try { + const data = JSON.parse(content) + return data && data[channel.site_id] ? data[channel.site_id] : [] + } catch { + return [] + } +} diff --git a/sites/singtel.com/singtel.com.test.js b/sites/singtel.com/singtel.com.test.js index 2d6b0bbd..82689ea0 100644 --- a/sites/singtel.com/singtel.com.test.js +++ b/sites/singtel.com/singtel.com.test.js @@ -1,59 +1,59 @@ -const { parser, url } = require('./singtel.com.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) - -jest.mock('axios') - -const date = dayjs.utc('2023-01-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '5418', - xmltv_id: 'ParamountNetworkSingapore.sg' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.singtel.com/etc/singtel/public/tv/epg-parsed-data/29012023.json' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - 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: '2023-01-28T16:00:00.000Z', - stop: '2023-01-28T17:30:00.000Z', - title: 'Hip Hop Family Christmas Wedding', - description: - 'Hip Hop\'s most famous family is back, and this time Christmas wedding bells are ringing! Jessica and Jayson are getting ready to say their "I do\'s".', - category: 'Specials' - }) - - expect(results[10]).toMatchObject({ - start: '2023-01-29T01:00:00.000Z', - stop: '2023-01-29T01:30:00.000Z', - title: 'The Daily Show', - description: - 'The Daily Show correspondents tackle the biggest stories in news, politics and pop culture.', - category: 'English Entertainment' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - const results = parser({ content, channel }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./singtel.com.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) + +jest.mock('axios') + +const date = dayjs.utc('2023-01-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '5418', + xmltv_id: 'ParamountNetworkSingapore.sg' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.singtel.com/etc/singtel/public/tv/epg-parsed-data/29012023.json' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + 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: '2023-01-28T16:00:00.000Z', + stop: '2023-01-28T17:30:00.000Z', + title: 'Hip Hop Family Christmas Wedding', + description: + 'Hip Hop\'s most famous family is back, and this time Christmas wedding bells are ringing! Jessica and Jayson are getting ready to say their "I do\'s".', + category: 'Specials' + }) + + expect(results[10]).toMatchObject({ + start: '2023-01-29T01:00:00.000Z', + stop: '2023-01-29T01:30:00.000Z', + title: 'The Daily Show', + description: + 'The Daily Show correspondents tackle the biggest stories in news, politics and pop culture.', + category: 'English Entertainment' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + const results = parser({ content, channel }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/sjonvarp.is/sjonvarp.is.config.js b/sites/sjonvarp.is/sjonvarp.is.config.js index 7fd6c33f..d2f656b5 100644 --- a/sites/sjonvarp.is/sjonvarp.is.config.js +++ b/sites/sjonvarp.is/sjonvarp.is.config.js @@ -1,90 +1,90 @@ -const dayjs = require('dayjs') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'sjonvarp.is', - days: 2, - url: function ({ channel, date }) { - return `http://www.sjonvarp.is/index.php?Tm=%3F&p=idag&c=${channel.site_id}&y=${date.format( - 'YYYY' - )}&m=${date.format('MM')}&d=${date.format('DD')}` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - const cheerio = require('cheerio') - - const data = await axios - .get('https://sjonvarp.is/') - .then(r => r.data) - .catch(console.log) - - let channels = [] - - const $ = cheerio.load(data) - $('.listing-row').each((i, el) => { - const site_id = $(el).attr('id') - const title = $(el).find('a.channel').first().attr('title') - const [, name] = title.match(/^Skoða dagskránna á (.*) í dag$/) - - channels.push({ - lang: 'is', - site_id, - name - }) - }) - - return channels - } -} - -function parseTitle($item) { - return $item('.day-listing-title').text() -} - -function parseDescription($item) { - return $item('.day-listing-description').text() -} - -function parseStart($item, date) { - const time = $item('.day-listing-time') - - return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $( - 'body > div.container.nano-container > div > ul > div.day-listing > div:not(.day-listing-channel)' - ).toArray() -} +const dayjs = require('dayjs') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'sjonvarp.is', + days: 2, + url: function ({ channel, date }) { + return `http://www.sjonvarp.is/index.php?Tm=%3F&p=idag&c=${channel.site_id}&y=${date.format( + 'YYYY' + )}&m=${date.format('MM')}&d=${date.format('DD')}` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const axios = require('axios') + const cheerio = require('cheerio') + + const data = await axios + .get('https://sjonvarp.is/') + .then(r => r.data) + .catch(console.log) + + let channels = [] + + const $ = cheerio.load(data) + $('.listing-row').each((i, el) => { + const site_id = $(el).attr('id') + const title = $(el).find('a.channel').first().attr('title') + const [, name] = title.match(/^Skoða dagskránna á (.*) í dag$/) + + channels.push({ + lang: 'is', + site_id, + name + }) + }) + + return channels + } +} + +function parseTitle($item) { + return $item('.day-listing-title').text() +} + +function parseDescription($item) { + return $item('.day-listing-description').text() +} + +function parseStart($item, date) { + const time = $item('.day-listing-time') + + return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $( + 'body > div.container.nano-container > div > ul > div.day-listing > div:not(.day-listing-channel)' + ).toArray() +} diff --git a/sites/sjonvarp.is/sjonvarp.is.test.js b/sites/sjonvarp.is/sjonvarp.is.test.js index 609b210c..3b96a52d 100644 --- a/sites/sjonvarp.is/sjonvarp.is.test.js +++ b/sites/sjonvarp.is/sjonvarp.is.test.js @@ -1,48 +1,48 @@ -const { parser, url } = require('./sjonvarp.is.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('2022-08-28', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'RUV', - xmltv_id: 'RUV.is' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'http://www.sjonvarp.is/index.php?Tm=%3F&p=idag&c=RUV&y=2022&m=08&d=28' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-08-28T07:15:00.000Z', - stop: '2022-08-28T07:16:00.000Z', - title: 'KrakkaRÚV' - }) - - expect(results[1]).toMatchObject({ - start: '2022-08-28T07:16:00.000Z', - stop: '2022-08-28T07:21:00.000Z', - title: 'Tölukubbar', - description: 'Lærið um tölustafina með Tölukubbunum! e.' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - const result = parser({ content, date }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./sjonvarp.is.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('2022-08-28', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'RUV', + xmltv_id: 'RUV.is' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'http://www.sjonvarp.is/index.php?Tm=%3F&p=idag&c=RUV&y=2022&m=08&d=28' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-08-28T07:15:00.000Z', + stop: '2022-08-28T07:16:00.000Z', + title: 'KrakkaRÚV' + }) + + expect(results[1]).toMatchObject({ + start: '2022-08-28T07:16:00.000Z', + stop: '2022-08-28T07:21:00.000Z', + title: 'Tölukubbar', + description: 'Lærið um tölustafina með Tölukubbunum! e.' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + const result = parser({ content, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/sky.co.nz/sky.co.nz.config.js b/sites/sky.co.nz/sky.co.nz.config.js index 117fbd9f..c095e5f3 100644 --- a/sites/sky.co.nz/sky.co.nz.config.js +++ b/sites/sky.co.nz/sky.co.nz.config.js @@ -1,55 +1,55 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'sky.co.nz', - days: 2, - url({ date, channel }) { - return `https://web-epg.sky.co.nz/prod/epgs/v1?channelNumber=${ - channel.site_id - }&start=${date.valueOf()}&end=${date.add(1, 'day').valueOf()}&limit=20000` - }, - parser({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.synopsis, - category: item.genres, - rating: parseRating(item), - start: dayjs(parseInt(item.start)), - stop: dayjs(parseInt(item.end)) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://skywebconfig.msl-prod.skycloud.co.nz/sky/json/channels.prod.json') - .then(r => r.data) - .catch(console.log) - - return data.channels.map(item => { - return { - lang: 'en', - site_id: parseInt(item.number).toString(), - name: item.name - } - }) - } -} - -function parseItems(content) { - const data = JSON.parse(content) - return data && data.events && Array.isArray(data.events) ? data.events : [] -} - -function parseRating(item) { - if (!item.rating) return null - return { - system: 'OFLC', - value: item.rating - } -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'sky.co.nz', + days: 2, + url({ date, channel }) { + return `https://web-epg.sky.co.nz/prod/epgs/v1?channelNumber=${ + channel.site_id + }&start=${date.valueOf()}&end=${date.add(1, 'day').valueOf()}&limit=20000` + }, + parser({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.synopsis, + category: item.genres, + rating: parseRating(item), + start: dayjs(parseInt(item.start)), + stop: dayjs(parseInt(item.end)) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://skywebconfig.msl-prod.skycloud.co.nz/sky/json/channels.prod.json') + .then(r => r.data) + .catch(console.log) + + return data.channels.map(item => { + return { + lang: 'en', + site_id: parseInt(item.number).toString(), + name: item.name + } + }) + } +} + +function parseItems(content) { + const data = JSON.parse(content) + return data && data.events && Array.isArray(data.events) ? data.events : [] +} + +function parseRating(item) { + if (!item.rating) return null + return { + system: 'OFLC', + value: item.rating + } +} diff --git a/sites/sky.co.nz/sky.co.nz.test.js b/sites/sky.co.nz/sky.co.nz.test.js index 25b35dea..4840ee62 100644 --- a/sites/sky.co.nz/sky.co.nz.test.js +++ b/sites/sky.co.nz/sky.co.nz.test.js @@ -1,60 +1,60 @@ -const { parser, url } = require('./sky.co.nz.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-21', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '36', - xmltv_id: 'SkyMoviesFamily.nz' -} -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://web-epg.sky.co.nz/prod/epgs/v1?channelNumber=36&start=1674259200000&end=1674345600000&limit=20000' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result[0]).toMatchObject({ - title: 'Sing 2', - description: - "Animated: Buster Moon and his friends must persuade the world's most reclusive rock star to help launch their most dazzling extravaganza yet. Voices Of: Matthew McConaughey, Reese Witherspoon (2021)", - category: ['Animated'], - rating: { system: 'OFLC', value: 'PG' }, - start: '2023-01-20T23:41:00.000Z', - stop: '2023-01-21T01:28:00.000Z' - }) - - expect(result[5]).toMatchObject({ - title: 'Harry Potter and the Goblet of Fire', - description: - 'Adventure: Harry is selected to represent Hogwarts at a legendary and dangerous wizardry competition between three schools of magic. Stars: Daniel Radcliffe, Rupert Grint (2005)', - category: ['Action/Adventure'], - rating: { system: 'OFLC', value: 'M-V' }, - start: '2023-01-21T07:42:00.000Z', - stop: '2023-01-21T10:13:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const result = parser( - { - content: `{ - "code": "DATE_FORMAT_ERROR", - "description": "DateFormat error", - "message": "Unparseable date: x" - }` - }, - channel - ) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./sky.co.nz.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-21', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '36', + xmltv_id: 'SkyMoviesFamily.nz' +} +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://web-epg.sky.co.nz/prod/epgs/v1?channelNumber=36&start=1674259200000&end=1674345600000&limit=20000' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result[0]).toMatchObject({ + title: 'Sing 2', + description: + "Animated: Buster Moon and his friends must persuade the world's most reclusive rock star to help launch their most dazzling extravaganza yet. Voices Of: Matthew McConaughey, Reese Witherspoon (2021)", + category: ['Animated'], + rating: { system: 'OFLC', value: 'PG' }, + start: '2023-01-20T23:41:00.000Z', + stop: '2023-01-21T01:28:00.000Z' + }) + + expect(result[5]).toMatchObject({ + title: 'Harry Potter and the Goblet of Fire', + description: + 'Adventure: Harry is selected to represent Hogwarts at a legendary and dangerous wizardry competition between three schools of magic. Stars: Daniel Radcliffe, Rupert Grint (2005)', + category: ['Action/Adventure'], + rating: { system: 'OFLC', value: 'M-V' }, + start: '2023-01-21T07:42:00.000Z', + stop: '2023-01-21T10:13:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const result = parser( + { + content: `{ + "code": "DATE_FORMAT_ERROR", + "description": "DateFormat error", + "message": "Unparseable date: x" + }` + }, + channel + ) + expect(result).toMatchObject([]) +}) diff --git a/sites/sky.com/sky.com.config.js b/sites/sky.com/sky.com.config.js index bed9a107..bb33b6c2 100644 --- a/sites/sky.com/sky.com.config.js +++ b/sites/sky.com/sky.com.config.js @@ -1,85 +1,85 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const doFetch = require('@ntlab/sfetch') -const debug = require('debug')('site:sky.com') -const _ = require('lodash') - -dayjs.extend(utc) - -doFetch.setDebugger(debug) - -module.exports = { - site: 'sky.com', - days: 2, - url({ date, channel }) { - return `https://awk.epgsky.com/hawk/linear/schedule/${date.format('YYYYMMDD')}/${ - channel.site_id - }` - }, - parser({ content, channel, date }) { - const programs = [] - if (content) { - const items = JSON.parse(content) || null - if (Array.isArray(items.schedule)) { - items.schedule - .filter(schedule => schedule.sid === channel.site_id) - .forEach(schedule => { - if (Array.isArray(schedule.events)) { - _.sortBy(schedule.events, 'st').forEach(event => { - const start = dayjs.utc(event.st * 1000) - if (start.isSame(date, 'd')) { - const image = `https://images.metadata.sky.com/pd-image/${event.programmeuuid}/16-9/640` - programs.push({ - title: event.t, - description: event.sy, - season: event.seasonnumber, - episode: event.episodenumber, - start, - stop: start.add(event.d, 's'), - icon: image, - image - }) - } - }) - } - }) - } - } - - return programs - }, - async channels() { - const channels = {} - const queues = [{ t: 'r', url: 'https://www.sky.com/tv-guide' }] - await doFetch(queues, (queue, res) => { - // process regions - if (queue.t === 'r') { - const $ = cheerio.load(res) - const initialData = JSON.parse(decodeURIComponent($('#initialData').text())) - initialData.state.epgData.regions.forEach(region => { - queues.push({ - t: 'c', - url: `https://awk.epgsky.com/hawk/linear/services/${region.bouquet}/${region.subBouquet}` - }) - }) - } - // process channels - if (queue.t === 'c') { - if (Array.isArray(res.services)) { - for (const ch of res.services) { - if (channels[ch.sid] === undefined) { - channels[ch.sid] = { - lang: 'en', - site_id: ch.sid, - name: ch.t - } - } - } - } - } - }) - - return Object.values(channels) - } -} +const cheerio = require('cheerio') +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('lodash.sortby') + +dayjs.extend(utc) + +doFetch.setDebugger(debug) + +module.exports = { + site: 'sky.com', + days: 2, + url({ date, channel }) { + return `https://awk.epgsky.com/hawk/linear/schedule/${date.format('YYYYMMDD')}/${ + channel.site_id + }` + }, + parser({ content, channel, date }) { + const programs = [] + if (content) { + const items = JSON.parse(content) || null + if (Array.isArray(items.schedule)) { + items.schedule + .filter(schedule => schedule.sid === channel.site_id) + .forEach(schedule => { + if (Array.isArray(schedule.events)) { + sortBy(schedule.events, p => p.st).forEach(event => { + const start = dayjs.utc(event.st * 1000) + if (start.isSame(date, 'd')) { + const image = `https://images.metadata.sky.com/pd-image/${event.programmeuuid}/16-9/640` + programs.push({ + title: event.t, + description: event.sy, + season: event.seasonnumber, + episode: event.episodenumber, + start, + stop: start.add(event.d, 's'), + icon: image, + image + }) + } + }) + } + }) + } + } + + return programs + }, + async channels() { + const channels = {} + const queues = [{ t: 'r', url: 'https://www.sky.com/tv-guide' }] + await doFetch(queues, (queue, res) => { + // process regions + if (queue.t === 'r') { + const $ = cheerio.load(res) + const initialData = JSON.parse(decodeURIComponent($('#initialData').text())) + initialData.state.epgData.regions.forEach(region => { + queues.push({ + t: 'c', + url: `https://awk.epgsky.com/hawk/linear/services/${region.bouquet}/${region.subBouquet}` + }) + }) + } + // process channels + if (queue.t === 'c') { + if (Array.isArray(res.services)) { + for (const ch of res.services) { + if (channels[ch.sid] === undefined) { + channels[ch.sid] = { + lang: 'en', + site_id: ch.sid, + name: ch.t + } + } + } + } + } + }) + + return Object.values(channels) + } +} diff --git a/sites/sky.com/sky.com.test.js b/sites/sky.com/sky.com.test.js index e58a818d..141a5f79 100644 --- a/sites/sky.com/sky.com.test.js +++ b/sites/sky.com/sky.com.test.js @@ -1,61 +1,61 @@ -const { parser, url } = require('./sky.com.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('2024-12-14', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '4086', - xmltv_id: 'SkyHistoryHD.uk' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://awk.epgsky.com/hawk/linear/schedule/20241214/4086') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.json')) - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result.length).toBe(31) - expect(result[0]).toMatchObject({ - start: '2024-12-14T00:00:00.000Z', - stop: '2024-12-14T00:30:00.000Z', - title: 'Storage Wars', - description: - 'A Sale Of Two Cities: Emily brings her mother along with her to Walnut, and Darrell wastes no time finding an advantage. Ivy and Ivy jr clean up with their locker. (S12, ep 4)', - season: 12, - episode: 4, - icon: 'https://images.metadata.sky.com/pd-image/b9572a38-8db7-471e-a2d7-462e1dd26af2/16-9/640', - image: 'https://images.metadata.sky.com/pd-image/b9572a38-8db7-471e-a2d7-462e1dd26af2/16-9/640' - }) - expect(result[2]).toMatchObject({ - start: '2024-12-14T01:00:00.000Z', - stop: '2024-12-14T01:30:00.000Z', - title: 'Storage Wars', - description: - 'Not All That Glitters Is Gourd: Back in the city of Orange, the Vegas Ladies arrive in vintage style - though not everyone agrees. (S12, ep 6)', - season: 12, - episode: 6, - icon: 'https://images.metadata.sky.com/pd-image/e9521ccc-bdcc-4075-9c2e-bc835247148b/16-9/640', - image: 'https://images.metadata.sky.com/pd-image/e9521ccc-bdcc-4075-9c2e-bc835247148b/16-9/640' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./sky.com.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('2024-12-14', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '4086', + xmltv_id: 'SkyHistoryHD.uk' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://awk.epgsky.com/hawk/linear/schedule/20241214/4086') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.json')) + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result.length).toBe(31) + expect(result[0]).toMatchObject({ + start: '2024-12-14T00:00:00.000Z', + stop: '2024-12-14T00:30:00.000Z', + title: 'Storage Wars', + description: + 'A Sale Of Two Cities: Emily brings her mother along with her to Walnut, and Darrell wastes no time finding an advantage. Ivy and Ivy jr clean up with their locker. (S12, ep 4)', + season: 12, + episode: 4, + icon: 'https://images.metadata.sky.com/pd-image/b9572a38-8db7-471e-a2d7-462e1dd26af2/16-9/640', + image: 'https://images.metadata.sky.com/pd-image/b9572a38-8db7-471e-a2d7-462e1dd26af2/16-9/640' + }) + expect(result[2]).toMatchObject({ + start: '2024-12-14T01:00:00.000Z', + stop: '2024-12-14T01:30:00.000Z', + title: 'Storage Wars', + description: + 'Not All That Glitters Is Gourd: Back in the city of Orange, the Vegas Ladies arrive in vintage style - though not everyone agrees. (S12, ep 6)', + season: 12, + episode: 6, + icon: 'https://images.metadata.sky.com/pd-image/e9521ccc-bdcc-4075-9c2e-bc835247148b/16-9/640', + image: 'https://images.metadata.sky.com/pd-image/e9521ccc-bdcc-4075-9c2e-bc835247148b/16-9/640' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/sky.de/__data__/content.json b/sites/sky.de/__data__/content.json new file mode 100644 index 00000000..89a208b0 --- /dev/null +++ b/sites/sky.de/__data__/content.json @@ -0,0 +1 @@ +{"cl":[{"ci":522,"el":[{"ei":122309300,"bsdt":1645916700000,"bst":"00:05","bedt":1645918200000,"len":25,"et":"King of Queens","ec":"Comedyserie","cop":"USA","yop":2001,"fsk":"ab 0 Jahre","epit":"Der Experte","sn":"4","en":"11","pu":"/static/img/program_guide/1522936_s.jpg"},{"ei":122309301,"bsdt":1645918200000,"bst":"00:30","bedt":1645919700000,"len":25,"et":"King of Queens","ec":"Comedyserie","cop":"USA","yop":2001,"fsk":"ab 0 Jahre","epit":"Speedy Gonzales","sn":"4","en":"12","pu":"/static/img/program_guide/1522937_s.jpg"}]}]} \ No newline at end of file diff --git a/sites/sky.de/sky.de.config.js b/sites/sky.de/sky.de.config.js index e91451bf..19a24580 100644 --- a/sites/sky.de/sky.de.config.js +++ b/sites/sky.de/sky.de.config.js @@ -1,78 +1,78 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'sky.de', - days: 2, - url: 'https://www.sky.de/sgtvg/service/getBroadcastsForGrid', - request: { - method: 'POST', - headers: { - 'accept-language': 'en-GB', - 'accept-encoding': 'gzip, deflate, br', - accept: 'application/json' - }, - data: function ({ channel, date }) { - return { - cil: [channel.site_id], - d: date.valueOf() - } - } - }, - parser: function ({ content, channel }) { - const programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.et, - description: item.epit, - category: item.ec, - start: dayjs(item.bsdt), - stop: dayjs(item.bedt), - season: item.sn, - episode: item.en, - image: item.pu ? `http://sky.de${item.pu}` : null - }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - const data = await axios - .post( - 'https://www.sky.de/sgtvg/service/getChannelList', - { dom: 'de', s: 0, feed: 1 }, - { - headers: { - 'Content-Type': 'application/json', - Referer: 'https://www.sky.de/tvguide-7599', - 'X-Requested-With': 'XMLHttpRequest' - } - } - ) - .then(r => r.data) - .catch(console.log) - - let channels = [] - data.cl.forEach(item => { - channels.push({ - lang: 'de', - name: item.cn, - site_id: item.ci - }) - }) - - return channels - } -} - -function parseContent(content, channel) { - const json = JSON.parse(content) - if (!Array.isArray(json.cl)) return null - return json.cl.find(i => i.ci == channel.site_id) -} - -function parseItems(content, channel) { - const data = parseContent(content, channel) - return data && Array.isArray(data.el) ? data.el : [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'sky.de', + days: 2, + url: 'https://www.sky.de/sgtvg/service/getBroadcastsForGrid', + request: { + method: 'POST', + headers: { + 'accept-language': 'en-GB', + 'accept-encoding': 'gzip, deflate, br', + accept: 'application/json' + }, + data: function ({ channel, date }) { + return { + cil: [channel.site_id], + d: date.valueOf() + } + } + }, + parser: function ({ content, channel }) { + const programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.et, + description: item.epit, + category: item.ec, + start: dayjs(item.bsdt), + stop: dayjs(item.bedt), + season: item.sn, + episode: item.en, + image: item.pu ? `http://sky.de${item.pu}` : null + }) + }) + + return programs + }, + async channels() { + const axios = require('axios') + const data = await axios + .post( + 'https://www.sky.de/sgtvg/service/getChannelList', + { dom: 'de', s: 0, feed: 1 }, + { + headers: { + 'Content-Type': 'application/json', + Referer: 'https://www.sky.de/tvguide-7599', + 'X-Requested-With': 'XMLHttpRequest' + } + } + ) + .then(r => r.data) + .catch(console.log) + + let channels = [] + data.cl.forEach(item => { + channels.push({ + lang: 'de', + name: item.cn, + site_id: item.ci + }) + }) + + return channels + } +} + +function parseContent(content, channel) { + const json = JSON.parse(content) + if (!Array.isArray(json.cl)) return null + return json.cl.find(i => i.ci == channel.site_id) +} + +function parseItems(content, channel) { + const data = parseContent(content, channel) + return data && Array.isArray(data.el) ? data.el : [] +} diff --git a/sites/sky.de/sky.de.test.js b/sites/sky.de/sky.de.test.js index 24835919..2a6120a5 100644 --- a/sites/sky.de/sky.de.test.js +++ b/sites/sky.de/sky.de.test.js @@ -1,66 +1,66 @@ -const { parser, url, request } = require('./sky.de.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2022-02-28', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '522', - xmltv_id: 'WarnerTVComedyHD.de' -} - -const content = - '{"cl":[{"ci":522,"el":[{"ei":122309300,"bsdt":1645916700000,"bst":"00:05","bedt":1645918200000,"len":25,"et":"King of Queens","ec":"Comedyserie","cop":"USA","yop":2001,"fsk":"ab 0 Jahre","epit":"Der Experte","sn":"4","en":"11","pu":"/static/img/program_guide/1522936_s.jpg"},{"ei":122309301,"bsdt":1645918200000,"bst":"00:30","bedt":1645919700000,"len":25,"et":"King of Queens","ec":"Comedyserie","cop":"USA","yop":2001,"fsk":"ab 0 Jahre","epit":"Speedy Gonzales","sn":"4","en":"12","pu":"/static/img/program_guide/1522937_s.jpg"}]}]}' - -it('can generate valid url', () => { - expect(url).toBe('https://www.sky.de/sgtvg/service/getBroadcastsForGrid') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request data', () => { - expect(request.data({ channel, date })).toMatchObject({ - cil: [channel.site_id], - d: date.valueOf() - }) -}) - -it('can parse response', () => { - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - title: 'King of Queens', - description: 'Der Experte', - category: 'Comedyserie', - start: '2022-02-26T23:05:00.000Z', - stop: '2022-02-26T23:30:00.000Z', - season: '4', - episode: '11', - image: 'http://sky.de/static/img/program_guide/1522936_s.jpg' - }, - { - title: 'King of Queens', - description: 'Speedy Gonzales', - category: 'Comedyserie', - start: '2022-02-26T23:30:00.000Z', - stop: '2022-02-26T23:55:00.000Z', - season: '4', - episode: '12', - image: 'http://sky.de/static/img/program_guide/1522937_s.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '[]' - }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./sky.de.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2022-02-28', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '522', + xmltv_id: 'WarnerTVComedyHD.de' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.sky.de/sgtvg/service/getBroadcastsForGrid') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request data', () => { + expect(request.data({ channel, date })).toMatchObject({ + cil: [channel.site_id], + d: date.valueOf() + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + title: 'King of Queens', + description: 'Der Experte', + category: 'Comedyserie', + start: '2022-02-26T23:05:00.000Z', + stop: '2022-02-26T23:30:00.000Z', + season: '4', + episode: '11', + image: 'http://sky.de/static/img/program_guide/1522936_s.jpg' + }, + { + title: 'King of Queens', + description: 'Speedy Gonzales', + category: 'Comedyserie', + start: '2022-02-26T23:30:00.000Z', + stop: '2022-02-26T23:55:00.000Z', + season: '4', + episode: '12', + image: 'http://sky.de/static/img/program_guide/1522937_s.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '[]' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/skylife.co.kr/skylife.co.kr.config.js b/sites/skylife.co.kr/skylife.co.kr.config.js index 617be99a..0dc4d138 100644 --- a/sites/skylife.co.kr/skylife.co.kr.config.js +++ b/sites/skylife.co.kr/skylife.co.kr.config.js @@ -1,81 +1,81 @@ -const dayjs = require('dayjs') -const axios = require('axios') -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: 'skylife.co.kr', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url({ date }) { - return `https://www.skylife.co.kr/api/api/public/tv/schedule/${date.format('YYYYMMDD')}` - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.name, - description: item.summary, - category: item.mainCategory, - actors: parseCast(item.cast), - start: parseTime(item.startTime), - stop: parseTime(item.endTime) - }) - }) - - return programs - }, - async channels() { - let channels = [] - - const url = `https://www.skylife.co.kr/api/api/public/tv/schedule/${dayjs().format('YYYYMMDD')}` - const data = await axios - .get(url) - .then(r => r.data) - .catch(console.log) - - for (let category of data) { - for (let channel of category.channels) { - channels.push({ - name: channel.name, - site_id: `${category.code}#${channel.id}`, - lang: 'ko' - }) - } - } - - return channels - } -} - -function parseCast(cast) { - if (!cast) return [] - - return cast.split(',') -} - -function parseTime(time) { - return dayjs.tz(time, 'YYYYMMDDHHmmss', 'Asia/Seoul') -} - -function parseItems(content, channel) { - const [categoryCode, channelId] = channel.site_id.split('#') - const data = JSON.parse(content) - if (!Array.isArray(data)) return [] - const category = data.find(_category => _category.code === categoryCode) - if (!category || !Array.isArray(category.channels)) return [] - const channelData = category.channels.find(_channel => _channel.id === channelId) - if (!channelData || !Array.isArray(channelData.programs)) return [] - - return channelData.programs -} +const dayjs = require('dayjs') +const axios = require('axios') +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: 'skylife.co.kr', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url({ date }) { + return `https://www.skylife.co.kr/api/api/public/tv/schedule/${date.format('YYYYMMDD')}` + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.name, + description: item.summary, + category: item.mainCategory, + actors: parseCast(item.cast), + start: parseTime(item.startTime), + stop: parseTime(item.endTime) + }) + }) + + return programs + }, + async channels() { + let channels = [] + + const url = `https://www.skylife.co.kr/api/api/public/tv/schedule/${dayjs().format('YYYYMMDD')}` + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + + for (let category of data) { + for (let channel of category.channels) { + channels.push({ + name: channel.name, + site_id: `${category.code}#${channel.id}`, + lang: 'ko' + }) + } + } + + return channels + } +} + +function parseCast(cast) { + if (!cast) return [] + + return cast.split(',') +} + +function parseTime(time) { + return dayjs.tz(time, 'YYYYMMDDHHmmss', 'Asia/Seoul') +} + +function parseItems(content, channel) { + const [categoryCode, channelId] = channel.site_id.split('#') + const data = JSON.parse(content) + if (!Array.isArray(data)) return [] + const category = data.find(_category => _category.code === categoryCode) + if (!category || !Array.isArray(category.channels)) return [] + const channelData = category.channels.find(_channel => _channel.id === channelId) + if (!channelData || !Array.isArray(channelData.programs)) return [] + + return channelData.programs +} diff --git a/sites/skylife.co.kr/skylife.co.kr.test.js b/sites/skylife.co.kr/skylife.co.kr.test.js index c35e85d4..0ba7d4a9 100644 --- a/sites/skylife.co.kr/skylife.co.kr.test.js +++ b/sites/skylife.co.kr/skylife.co.kr.test.js @@ -1,42 +1,42 @@ -const { parser, url } = require('./skylife.co.kr.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('2024-06-26', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '4003#798', - xmltv_id: 'EBS.kr' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://www.skylife.co.kr/api/api/public/tv/schedule/20240626') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[20]).toMatchObject({ - start: '2024-06-26T00:40:00.000Z', // 20240626094000 - stop: '2024-06-26T01:30:00.000Z', // 20240626103000 - title: '세상에 나쁜 개는 없다', - description: '문제 있는 반려견들의 행동을 알아 보고 원인을 찾아나가는 프로그램', - category: '교양/정보', - actors: ['박영진', '강형욱'] - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const result = parser({ content, channel }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./skylife.co.kr.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('2024-06-26', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '4003#798', + xmltv_id: 'EBS.kr' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://www.skylife.co.kr/api/api/public/tv/schedule/20240626') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[20]).toMatchObject({ + start: '2024-06-26T00:40:00.000Z', // 20240626094000 + stop: '2024-06-26T01:30:00.000Z', // 20240626103000 + title: '세상에 나쁜 개는 없다', + description: '문제 있는 반려견들의 행동을 알아 보고 원인을 찾아나가는 프로그램', + category: '교양/정보', + actors: ['박영진', '강형욱'] + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const result = parser({ content, channel }) + expect(result).toMatchObject([]) +}) diff --git a/sites/skyperfectv.co.jp/skyperfectv.co.jp.config.js b/sites/skyperfectv.co.jp/skyperfectv.co.jp.config.js index 1963124d..9b2508cd 100644 --- a/sites/skyperfectv.co.jp/skyperfectv.co.jp.config.js +++ b/sites/skyperfectv.co.jp/skyperfectv.co.jp.config.js @@ -1,121 +1,121 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const cheerio = require('cheerio') -const duration = require('dayjs/plugin/duration') -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) -dayjs.extend(duration) - -const exported = { - site: 'skyperfectv.co.jp', - days: 1, - lang: 'ja', - url: function ({ date, channel }) { - let [type, ...code] = channel.site_id.split('_') - code = code.join('_') - return `https://www.skyperfectv.co.jp/program/schedule/${type}/channel:${code}/date:${date.format( - 'YYMMDD' - )}` - }, - logo: function ({ channel }) { - return `https://www.skyperfectv.co.jp/library/common/img/channel/icon/basic/m_${channel.site_id.toLowerCase()}.gif` - }, - // Specific function that permits to gather NSFW channels (needs confirmation) - async fetchSchedule({ date, channel }) { - const url = exported.url({ date, channel }) - const response = await axios.get(url, { - headers: { - Cookie: 'adult_auth=true' - } - }) - return response.data - }, - parser({ content, date }) { - const $ = cheerio.load(content) - const programs = [] - - const sections = [ - { id: 'js-am', addition: 0 }, - { id: 'js-pm', addition: 0 }, - { id: 'js-md', addition: 1 } - ] - - sections.forEach(({ id, addition }) => { - $(`#${id} > td`).each((index, element) => { - // `td` is a column for a day - // the next `td` will be the next day - const today = date.add(index + addition, 'd').tz('Asia/Tokyo') - - const parseTime = timeString => { - // timeString is in the format "HH:mm" - // replace `today` with the time from timeString - const [hour, minute] = timeString.split(':').map(Number) - return today.hour(hour).minute(minute) - } - - const $element = $(element) // Wrap element with Cheerio - $element.find('.p-program__item').each((itemIndex, itemElement) => { - const $itemElement = $(itemElement) // Wrap itemElement with Cheerio - const [start, stop] = $itemElement - .find('.p-program__range') - .first() - .text() - .split('〜') - .map(parseTime) - const title = $itemElement.find('.p-program__name').first().text() - const image = $itemElement.find('.js-program_thumbnail').first().attr('data-lazysrc') - programs.push({ - title, - start, - stop, - image - }) - }) - }) - }) - - return programs - }, - async channels() { - const pageParser = (content, type) => { - // type: "basic" | "premium" - // Returns an array of channel objects - - const $ = cheerio.load(content) - const channels = [] - - $('.p-channel').each((index, element) => { - const site_id = `${type}_${$(element).find('.p-channel__id').text()}` - const name = $(element).find('.p-channel__name').text() - channels.push({ site_id, name, lang: 'ja' }) - }) - - return channels - } - - const getChannels = async type => { - const response = await axios.get(`https://www.skyperfectv.co.jp/program/schedule/${type}/`, { - headers: { - Cookie: 'adult_auth=true;' - } - }) - return pageParser(response.data, type) - } - - const fetchAllChannels = async () => { - const basicChannels = await getChannels('basic') - const premiumChannels = await getChannels('premium') - const results = [...basicChannels, ...premiumChannels] - return results - } - - return await fetchAllChannels() - } -} - -module.exports = exported +const axios = require('axios') +const dayjs = require('dayjs') +const cheerio = require('cheerio') +const duration = require('dayjs/plugin/duration') +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) +dayjs.extend(duration) + +const exported = { + site: 'skyperfectv.co.jp', + days: 1, + lang: 'ja', + url: function ({ date, channel }) { + let [type, ...code] = channel.site_id.split('_') + code = code.join('_') + return `https://www.skyperfectv.co.jp/program/schedule/${type}/channel:${code}/date:${date.format( + 'YYMMDD' + )}` + }, + logo: function ({ channel }) { + return `https://www.skyperfectv.co.jp/library/common/img/channel/icon/basic/m_${channel.site_id.toLowerCase()}.gif` + }, + // Specific function that permits to gather NSFW channels (needs confirmation) + async fetchSchedule({ date, channel }) { + const url = exported.url({ date, channel }) + const response = await axios.get(url, { + headers: { + Cookie: 'adult_auth=true' + } + }) + return response.data + }, + parser({ content, date }) { + const $ = cheerio.load(content) + const programs = [] + + const sections = [ + { id: 'js-am', addition: 0 }, + { id: 'js-pm', addition: 0 }, + { id: 'js-md', addition: 1 } + ] + + sections.forEach(({ id, addition }) => { + $(`#${id} > td`).each((index, element) => { + // `td` is a column for a day + // the next `td` will be the next day + const today = date.add(index + addition, 'd').tz('Asia/Tokyo') + + const parseTime = timeString => { + // timeString is in the format "HH:mm" + // replace `today` with the time from timeString + const [hour, minute] = timeString.split(':').map(Number) + return today.hour(hour).minute(minute) + } + + const $element = $(element) // Wrap element with Cheerio + $element.find('.p-program__item').each((itemIndex, itemElement) => { + const $itemElement = $(itemElement) // Wrap itemElement with Cheerio + const [start, stop] = $itemElement + .find('.p-program__range') + .first() + .text() + .split('〜') + .map(parseTime) + const title = $itemElement.find('.p-program__name').first().text() + const image = $itemElement.find('.js-program_thumbnail').first().attr('data-lazysrc') + programs.push({ + title, + start, + stop, + image + }) + }) + }) + }) + + return programs + }, + async channels() { + const pageParser = (content, type) => { + // type: "basic" | "premium" + // Returns an array of channel objects + + const $ = cheerio.load(content) + const channels = [] + + $('.p-channel').each((index, element) => { + const site_id = `${type}_${$(element).find('.p-channel__id').text()}` + const name = $(element).find('.p-channel__name').text() + channels.push({ site_id, name, lang: 'ja' }) + }) + + return channels + } + + const getChannels = async type => { + const response = await axios.get(`https://www.skyperfectv.co.jp/program/schedule/${type}/`, { + headers: { + Cookie: 'adult_auth=true;' + } + }) + return pageParser(response.data, type) + } + + const fetchAllChannels = async () => { + const basicChannels = await getChannels('basic') + const premiumChannels = await getChannels('premium') + const results = [...basicChannels, ...premiumChannels] + return results + } + + return await fetchAllChannels() + } +} + +module.exports = exported diff --git a/sites/skyperfectv.co.jp/skyperfectv.co.jp.test.js b/sites/skyperfectv.co.jp/skyperfectv.co.jp.test.js index c70ae27c..f96a9b9b 100644 --- a/sites/skyperfectv.co.jp/skyperfectv.co.jp.test.js +++ b/sites/skyperfectv.co.jp/skyperfectv.co.jp.test.js @@ -1,53 +1,53 @@ -const { parser, url } = require('./skyperfectv.co.jp.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('2024-08-01', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'basic_BS193', - name: 'WOWOWシネマ', - xmltv_id: 'WOWOWCinema.jp' -} - -const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe( - 'https://www.skyperfectv.co.jp/program/schedule/basic/channel:BS193/date:240801' - ) -}) - -it('can parse response', async () => { - const result = (await parser({ date, channel, content })).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result.filter(p => p.title == 'ヴァルキリードライヴマーメイド #06')).toMatchObject([ - { - start: '2024-07-31T19:00:00.000Z', // UTC time - stop: '2024-07-31T19:30:00.000Z', // UTC - title: 'ヴァルキリードライヴマーメイド #06', - image: - 'https://pm-img-ap.skyperfectv.co.jp/uploads/thumbnail/image/11301805/S_BC929697780313_be7975d4e26a4cad9b89fc6c94807e38_20240613144158569.jpg' - } - ]) -}) - -const empty = fs.readFileSync(path.resolve(__dirname, '__data__/empty.html')) - -it('can handle empty guide', async () => { - const result = parser({ - date, - channel, - content: empty - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./skyperfectv.co.jp.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('2024-08-01', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'basic_BS193', + name: 'WOWOWシネマ', + xmltv_id: 'WOWOWCinema.jp' +} + +const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe( + 'https://www.skyperfectv.co.jp/program/schedule/basic/channel:BS193/date:240801' + ) +}) + +it('can parse response', async () => { + const result = (await parser({ date, channel, content })).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result.filter(p => p.title == 'ヴァルキリードライヴマーメイド #06')).toMatchObject([ + { + start: '2024-07-31T19:00:00.000Z', // UTC time + stop: '2024-07-31T19:30:00.000Z', // UTC + title: 'ヴァルキリードライヴマーメイド #06', + image: + 'https://pm-img-ap.skyperfectv.co.jp/uploads/thumbnail/image/11301805/S_BC929697780313_be7975d4e26a4cad9b89fc6c94807e38_20240613144158569.jpg' + } + ]) +}) + +const empty = fs.readFileSync(path.resolve(__dirname, '__data__/empty.html')) + +it('can handle empty guide', async () => { + const result = parser({ + date, + channel, + content: empty + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/snrt.ma/snrt.ma.config.js b/sites/snrt.ma/snrt.ma.config.js index 84d5ede0..4448c7f3 100644 --- a/sites/snrt.ma/snrt.ma.config.js +++ b/sites/snrt.ma/snrt.ma.config.js @@ -1,98 +1,98 @@ -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) - -const tz = 'Africa/Casablanca' - -module.exports = { - site: 'snrt.ma', - days: 2, - url({ channel }) { - return `https://www.snrt.ma/ar/node/${channel.site_id}` - }, - request: { - cache: { - ttl: 24 * 60 * 60 * 1000 // 1 day - } - }, - parser({ content, date }) { - const [$, items] = parseItems(content) - const programs = items.map(item => { - const $item = $(item) - const start = parseStart($item) - return { - title: parseTitle($item), - description: parseDescription($item), - category: parseCategory($item), - start - } - }).filter(item => item.start).sort((a, b) => a.start - b.start) - // fill start-stop - for (let i = 0; i < programs.length; i++) { - if (i < programs.length - 1) { - programs[i].stop = programs[i + 1].start - } else { - programs[i].stop = dayjs.tz( - `${date.add(1, 'd').format('YYYY-MM-DD')} 00:00`, - 'YYYY-MM-DD HH:mm', - tz - ) - } - } - - return programs.filter(p => p.start.isSame(date, 'd')) - }, - async channels({ lang = 'ar' }) { - const axios = require('axios') - const result = await axios - .get('https://www.snrt.ma/ar/node/1208') - .then(response => response.data) - .catch(console.error) - - const $ = cheerio.load(result) - const items = $('.channels-row h4').toArray() - const channels = items.map(item => { - const $item = $(item) - const url = $item.find('a').attr('href') - return { - lang, - site_id: url.substr(url.lastIndexOf('/') + 1), - name: $item.find('img').attr('alt') - } - }) - - return channels - } -} - -function parseStart($item) { - const date = $item.attr('class').match(/\d{8}/)[0] - const time = $item.find('.grille-time').text().trim() - if (time) { - return dayjs.tz(`${date} ${time.replace('H', ':')}`, 'YYYYMMDD HH:mm', tz) - } -} - -function parseTitle($item) { - return $item.find('.program-title-sm').text().trim() -} - -function parseDescription($item) { - return $item.find('.program-description-sm').text().trim() -} - -function parseCategory($item) { - return $item.find('.genre-first').text().trim() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return [$, $('.grille-line').toArray()] -} +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) + +const tz = 'Africa/Casablanca' + +module.exports = { + site: 'snrt.ma', + days: 2, + url({ channel }) { + return `https://www.snrt.ma/ar/node/${channel.site_id}` + }, + request: { + cache: { + ttl: 24 * 60 * 60 * 1000 // 1 day + } + }, + parser({ content, date }) { + const [$, items] = parseItems(content) + const programs = items.map(item => { + const $item = $(item) + const start = parseStart($item) + return { + title: parseTitle($item), + description: parseDescription($item), + category: parseCategory($item), + start + } + }).filter(item => item.start).sort((a, b) => a.start - b.start) + // fill start-stop + for (let i = 0; i < programs.length; i++) { + if (i < programs.length - 1) { + programs[i].stop = programs[i + 1].start + } else { + programs[i].stop = dayjs.tz( + `${date.add(1, 'd').format('YYYY-MM-DD')} 00:00`, + 'YYYY-MM-DD HH:mm', + tz + ) + } + } + + return programs.filter(p => p.start.isSame(date, 'd')) + }, + async channels({ lang = 'ar' }) { + const axios = require('axios') + const result = await axios + .get('https://www.snrt.ma/ar/node/1208') + .then(response => response.data) + .catch(console.error) + + const $ = cheerio.load(result) + const items = $('.channels-row h4').toArray() + const channels = items.map(item => { + const $item = $(item) + const url = $item.find('a').attr('href') + return { + lang, + site_id: url.substr(url.lastIndexOf('/') + 1), + name: $item.find('img').attr('alt') + } + }) + + return channels + } +} + +function parseStart($item) { + const date = $item.attr('class').match(/\d{8}/)[0] + const time = $item.find('.grille-time').text().trim() + if (time) { + return dayjs.tz(`${date} ${time.replace('H', ':')}`, 'YYYYMMDD HH:mm', tz) + } +} + +function parseTitle($item) { + return $item.find('.program-title-sm').text().trim() +} + +function parseDescription($item) { + return $item.find('.program-description-sm').text().trim() +} + +function parseCategory($item) { + return $item.find('.genre-first').text().trim() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return [$, $('.grille-line').toArray()] +} diff --git a/sites/snrt.ma/snrt.ma.test.js b/sites/snrt.ma/snrt.ma.test.js index d20856fa..dbb544cc 100644 --- a/sites/snrt.ma/snrt.ma.test.js +++ b/sites/snrt.ma/snrt.ma.test.js @@ -1,47 +1,47 @@ -const { parser, url } = require('./snrt.ma.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const fs = require('fs') -const path = require('path') - -dayjs.extend(utc) -dayjs.extend(timezone) - -const date = dayjs.utc('2025-01-13', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: '4075', xmltv_id: 'Tamazight.ma', lang: 'ar' } - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://www.snrt.ma/ar/node/4075') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ date, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(27) - expect(results[0]).toMatchObject({ - start: '2025-01-12T23:15:00.000Z', - stop: '2025-01-12T23:30:00.000Z', - title: 'الموعد الرياضي' - }) - expect(results[26]).toMatchObject({ - start: '2025-01-13T21:30:00.000Z', - stop: '2025-01-13T23:00:00.000Z', - title: 'سهرة خاصة براس السنة الامازيغية', - category: 'ترفيه' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel: channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./snrt.ma.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const fs = require('fs') +const path = require('path') + +dayjs.extend(utc) +dayjs.extend(timezone) + +const date = dayjs.utc('2025-01-13', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: '4075', xmltv_id: 'Tamazight.ma', lang: 'ar' } + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://www.snrt.ma/ar/node/4075') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ date, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(27) + expect(results[0]).toMatchObject({ + start: '2025-01-12T23:15:00.000Z', + stop: '2025-01-12T23:30:00.000Z', + title: 'الموعد الرياضي' + }) + expect(results[26]).toMatchObject({ + start: '2025-01-13T21:30:00.000Z', + stop: '2025-01-13T23:00:00.000Z', + title: 'سهرة خاصة براس السنة الامازيغية', + category: 'ترفيه' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel: channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/sporttv.pt/sporttv.pt.config.js b/sites/sporttv.pt/sporttv.pt.config.js index 201b98ed..0709918e 100644 --- a/sites/sporttv.pt/sporttv.pt.config.js +++ b/sites/sporttv.pt/sporttv.pt.config.js @@ -1,63 +1,63 @@ -const dayjs = require('dayjs') -const cheerio = require('cheerio') - -module.exports = { - site: 'sporttv.pt', - days: 2, - url: 'https://www.sporttv.pt/guia', - parser({ content, date, channel }) { - let programs = [] - const items = parseItems(content, channel, date) - items.forEach(item => { - const start = dayjs(item.data) - const stop = start.add(item.duracao, 'ms') - - programs.push({ - title: item.descricao, - description: item?.evento?.nome, - image: item.imagem, - category: item?.modalidade?.nomeModalidade, - start, - stop - }) - }) - - return programs - } -} - -function parseItems(content, channel, date) { - const $ = cheerio.load(content) - const nuxtData = $('#__NUXT_DATA__').html() - if (!nuxtData) return [] - const parsed = JSON.parse(nuxtData) - const dataIndex = parsed[1].data - const epgIndex = Object.values(parsed[dataIndex])[3] // 1611 - const epg = parsed[epgIndex].map(i => parsed[i]).map(obj => dataMapper(obj, parsed)) - if (!Array.isArray(epg)) return [] - - return epg - .filter( - item => item.canal.id === parseInt(channel.site_id) && date.isSame(dayjs(item.data), 'd') - ) - .sort((a, b) => { - if (a < b) return -1 - if (a > b) return 1 - return 0 - }) -} - -function dataMapper(object, parsed) { - let output = {} - - for (let key in object) { - const value = parsed[object[key]] - if (typeof value === 'object') { - output[key] = dataMapper(value, parsed) - } else { - output[key] = value - } - } - - return output -} +const dayjs = require('dayjs') +const cheerio = require('cheerio') + +module.exports = { + site: 'sporttv.pt', + days: 2, + url: 'https://www.sporttv.pt/guia', + parser({ content, date, channel }) { + let programs = [] + const items = parseItems(content, channel, date) + items.forEach(item => { + const start = dayjs(item.data) + const stop = start.add(item.duracao, 'ms') + + programs.push({ + title: item.descricao, + description: item?.evento?.nome, + image: item.imagem, + category: item?.modalidade?.nomeModalidade, + start, + stop + }) + }) + + return programs + } +} + +function parseItems(content, channel, date) { + const $ = cheerio.load(content) + const nuxtData = $('#__NUXT_DATA__').html() + if (!nuxtData) return [] + const parsed = JSON.parse(nuxtData) + const dataIndex = parsed[1].data + const epgIndex = Object.values(parsed[dataIndex])[3] // 1611 + const epg = parsed[epgIndex].map(i => parsed[i]).map(obj => dataMapper(obj, parsed)) + if (!Array.isArray(epg)) return [] + + return epg + .filter( + item => item.canal.id === parseInt(channel.site_id) && date.isSame(dayjs(item.data), 'd') + ) + .sort((a, b) => { + if (a < b) return -1 + if (a > b) return 1 + return 0 + }) +} + +function dataMapper(object, parsed) { + let output = {} + + for (let key in object) { + const value = parsed[object[key]] + if (typeof value === 'object') { + output[key] = dataMapper(value, parsed) + } else { + output[key] = value + } + } + + return output +} diff --git a/sites/sporttv.pt/sporttv.pt.test.js b/sites/sporttv.pt/sporttv.pt.test.js index a0836afe..b33128d3 100644 --- a/sites/sporttv.pt/sporttv.pt.test.js +++ b/sites/sporttv.pt/sporttv.pt.test.js @@ -1,54 +1,54 @@ -const { parser, url } = require('./sporttv.pt.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('2024-12-23', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 727, - xmltv_id: 'SportTV1.pt' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.sporttv.pt/guia') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(19) - - expect(results[0]).toMatchObject({ - start: '2024-12-23T01:00:00.000Z', - stop: '2024-12-23T01:30:00.000Z', - description: 'LIGA PORTUGAL BETCLIC', - category: 'FUTEBOL', - title: 'RESUMOS DA JORNADA 15', - image: 'https://www.sporttv.pt/default/0001/11/08cb25f0b9b427e0bb83179309074632410f536b.jpg' - }) - - expect(results[1]).toMatchObject({ - start: '2024-12-23T01:30:00.000Z', - stop: '2024-12-23T02:00:00.000Z', - description: 'LIGA ITALIANA', - category: 'FUTEBOL', - title: 'RESUMOS DA JORNADA 17', - image: - 'https://www.sporttv.pt/cms_media/default/0001/11/56ab6bb72a00c8a9543eff35f90f57c07fb0ff87.jpg' - }) -}) - -it('can handle empty guide', () => { - const content = '' - const result = parser({ content, date }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./sporttv.pt.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('2024-12-23', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 727, + xmltv_id: 'SportTV1.pt' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.sporttv.pt/guia') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(19) + + expect(results[0]).toMatchObject({ + start: '2024-12-23T01:00:00.000Z', + stop: '2024-12-23T01:30:00.000Z', + description: 'LIGA PORTUGAL BETCLIC', + category: 'FUTEBOL', + title: 'RESUMOS DA JORNADA 15', + image: 'https://www.sporttv.pt/default/0001/11/08cb25f0b9b427e0bb83179309074632410f536b.jpg' + }) + + expect(results[1]).toMatchObject({ + start: '2024-12-23T01:30:00.000Z', + stop: '2024-12-23T02:00:00.000Z', + description: 'LIGA ITALIANA', + category: 'FUTEBOL', + title: 'RESUMOS DA JORNADA 17', + image: + 'https://www.sporttv.pt/cms_media/default/0001/11/56ab6bb72a00c8a9543eff35f90f57c07fb0ff87.jpg' + }) +}) + +it('can handle empty guide', () => { + const content = '' + const result = parser({ content, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/starhubtvplus.com/starhubtvplus.com.config.js b/sites/starhubtvplus.com/starhubtvplus.com.config.js index c9710bae..5fc26feb 100644 --- a/sites/starhubtvplus.com/starhubtvplus.com.config.js +++ b/sites/starhubtvplus.com/starhubtvplus.com.config.js @@ -1,88 +1,88 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const languages = { en: 'en_US', zh: 'zh' } - -module.exports = { - site: 'starhubtvplus.com', - days: 2, - url({ date, channel }) { - return `https://waf-starhub-metadata-api-p001.ifs.vubiquity.com/v3.1/epg/schedules?locale=${ - languages[channel.lang] - }&locale_default=${languages[channel.lang]}&device=1&in_channel_id=${ - channel.site_id - }>_end=${date.unix()}<_start=${date.add(1, 'd').unix()}&limit=100&page=1` - }, - async parser({ content, date, channel }) { - const programs = [] - if (content) { - let res = JSON.parse(content) - while (res) { - if (res.resources) { - programs.push(...res.resources) - } - if (res.page && res.page.current < res.page.total) { - res = await axios - .get( - module.exports - .url({ date, channel }) - .replace(/page=(\d+)/, `page=${res.page.current + 1}`) - ) - .then(r => r.data) - .catch(console.error) - } else { - res = null - } - } - } - const season = s => { - if (s) { - const [, , n] = s.match(/(S|Season )(\d+)/) || [null, null, null] - if (n) { - return parseInt(n) - } - } - } - - return programs.map(item => { - return { - title: item.title, - subTitle: item.serie_title, - description: item.description, - category: item.genres, - image: item.pictures?.map(img => img.url), - season: season(item.serie_title), - episode: item.episode_number, - rating: item.rating, - start: dayjs(item.start * 1000), - stop: dayjs(item.end * 1000) - } - }) - }, - async channels({ lang = 'en' }) { - const resources = [] - let page = 1 - while (true) { - const items = await axios - .get( - `https://waf-starhub-metadata-api-p001.ifs.vubiquity.com/v3.1/epg/channels?locale=${languages[lang]}&locale_default=${languages[lang]}&device=1&limit=50&page=${page}` - ) - .then(r => r.data) - .catch(console.error) - if (items.resources) { - resources.push(...items.resources) - } - if (items.page && page < items.page.total) { - page++ - } else { - break - } - } - - return resources.map(ch => ({ - lang, - site_id: ch.id, - name: ch.title - })) - } -} +const axios = require('axios') +const dayjs = require('dayjs') + +const languages = { en: 'en_US', zh: 'zh' } + +module.exports = { + site: 'starhubtvplus.com', + days: 2, + url({ date, channel }) { + return `https://waf-starhub-metadata-api-p001.ifs.vubiquity.com/v3.1/epg/schedules?locale=${ + languages[channel.lang] + }&locale_default=${languages[channel.lang]}&device=1&in_channel_id=${ + channel.site_id + }>_end=${date.unix()}<_start=${date.add(1, 'd').unix()}&limit=100&page=1` + }, + async parser({ content, date, channel }) { + const programs = [] + if (content) { + let res = JSON.parse(content) + while (res) { + if (res.resources) { + programs.push(...res.resources) + } + if (res.page && res.page.current < res.page.total) { + res = await axios + .get( + module.exports + .url({ date, channel }) + .replace(/page=(\d+)/, `page=${res.page.current + 1}`) + ) + .then(r => r.data) + .catch(console.error) + } else { + res = null + } + } + } + const season = s => { + if (s) { + const [, , n] = s.match(/(S|Season )(\d+)/) || [null, null, null] + if (n) { + return parseInt(n) + } + } + } + + return programs.map(item => { + return { + title: item.title, + subTitle: item.serie_title, + description: item.description, + category: item.genres, + image: item.pictures?.map(img => img.url), + season: season(item.serie_title), + episode: item.episode_number, + rating: item.rating, + start: dayjs(item.start * 1000), + stop: dayjs(item.end * 1000) + } + }) + }, + async channels({ lang = 'en' }) { + const resources = [] + let page = 1 + while (true) { + const items = await axios + .get( + `https://waf-starhub-metadata-api-p001.ifs.vubiquity.com/v3.1/epg/channels?locale=${languages[lang]}&locale_default=${languages[lang]}&device=1&limit=50&page=${page}` + ) + .then(r => r.data) + .catch(console.error) + if (items.resources) { + resources.push(...items.resources) + } + if (items.page && page < items.page.total) { + page++ + } else { + break + } + } + + return resources.map(ch => ({ + lang, + site_id: ch.id, + name: ch.title + })) + } +} diff --git a/sites/starhubtvplus.com/starhubtvplus.com.test.js b/sites/starhubtvplus.com/starhubtvplus.com.test.js index 78d97c5b..97216eac 100644 --- a/sites/starhubtvplus.com/starhubtvplus.com.test.js +++ b/sites/starhubtvplus.com/starhubtvplus.com.test.js @@ -1,55 +1,55 @@ -const { parser, url } = require('./starhubtvplus.com.config.js') -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('2024-12-04', 'YYYY-MM-DD').startOf('d') -const channel = { - lang: 'en', - site_id: 'd258444e-b66b-4cbe-88db-e09f31ab8a1f', - xmltv_id: 'AXN.sg' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://waf-starhub-metadata-api-p001.ifs.vubiquity.com/v3.1/epg/schedules?locale=en_US&locale_default=en_US&device=1&in_channel_id=d258444e-b66b-4cbe-88db-e09f31ab8a1f>_end=1733270400<_start=1733356800&limit=100&page=1' - ) -}) - -it('can parse response', async () => { - const fs = require('fs') - const path = require('path') - const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.json')) - const result = (await parser({ content, date, channel })).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2024-12-03T17:25:00.000Z', - stop: '2024-12-03T18:20:00.000Z', - title: 'Northern Rexposure', - subTitle: 'Hudson & Rex (Season 5)', - description: - "When Jesse's sister contacts him for help, he, Sarah and Rex head to Northern Ontario and find themselves in the middle of a deadly situation.", - category: ['Drama'], - image: [ - 'https://poster.starhubgo.com/poster/ch511_hudson_rex5.jpg?w=960&h=540', - 'https://poster.starhubgo.com/poster/ch511_hudson_rex5.jpg?w=341&h=192' - ], - season: 5, - episode: 15, - rating: 'PG13' - } - ]) -}) - -it('can handle empty guide', async () => { - const result = await parser({ content: '' }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./starhubtvplus.com.config.js') +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('2024-12-04', 'YYYY-MM-DD').startOf('d') +const channel = { + lang: 'en', + site_id: 'd258444e-b66b-4cbe-88db-e09f31ab8a1f', + xmltv_id: 'AXN.sg' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://waf-starhub-metadata-api-p001.ifs.vubiquity.com/v3.1/epg/schedules?locale=en_US&locale_default=en_US&device=1&in_channel_id=d258444e-b66b-4cbe-88db-e09f31ab8a1f>_end=1733270400<_start=1733356800&limit=100&page=1' + ) +}) + +it('can parse response', async () => { + const fs = require('fs') + const path = require('path') + const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.json')) + const result = (await parser({ content, date, channel })).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2024-12-03T17:25:00.000Z', + stop: '2024-12-03T18:20:00.000Z', + title: 'Northern Rexposure', + subTitle: 'Hudson & Rex (Season 5)', + description: + "When Jesse's sister contacts him for help, he, Sarah and Rex head to Northern Ontario and find themselves in the middle of a deadly situation.", + category: ['Drama'], + image: [ + 'https://poster.starhubgo.com/poster/ch511_hudson_rex5.jpg?w=960&h=540', + 'https://poster.starhubgo.com/poster/ch511_hudson_rex5.jpg?w=341&h=192' + ], + season: 5, + episode: 15, + rating: 'PG13' + } + ]) +}) + +it('can handle empty guide', async () => { + const result = await parser({ content: '' }) + expect(result).toMatchObject([]) +}) diff --git a/sites/startimestv.com/startimestv.com.config.js b/sites/startimestv.com/startimestv.com.config.js index 214e28b1..cf61c7d0 100644 --- a/sites/startimestv.com/startimestv.com.config.js +++ b/sites/startimestv.com/startimestv.com.config.js @@ -1,112 +1,112 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const doFetch = require('@ntlab/sfetch') -const debug = require('debug')('site:startimestv.com') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -doFetch.setDebugger(debug).setMaxWorker(5) - -module.exports = { - site: 'startimestv.com', - days: 2, - url({ channel, date }) { - return `https://www.startimestv.com/channeldetail/${channel.site_id}/${date.format( - 'YYYY-MM-DD' - )}.html` - }, - parser({ content, date }) { - const programs = [] - if (content) { - const $ = cheerio.load(content) - $('.box .mask') - .toArray() - .forEach(el => { - let title = parseText($(el).find('h4')) - const [s, e] = title.substr(0, title.indexOf(' ')).split('-') || [null, null] - const start = dayjs.utc(`${date.format('YYYY-MM-DD')} ${s}`, 'YYYY-MM-DD HH:nn') - const stop = dayjs.utc(`${date.format('YYYY-MM-DD')} ${e}`, 'YYYY-MM-DD HH:nn') - title = title.substr(title.indexOf(' ') + 1) - const [, season, episode] = title.match(/ S(\d+) E(\d+)/) || [null, null, null] - const description = parseText($(el).find('p')) - programs.push({ - title, - description: description !== 'NA' ? description : null, - season: season ? parseInt(season) : season, - episode: episode ? parseInt(episode) : episode, - start, - stop - }) - }) - } - - return programs - }, - async channels() { - const channels = {} - const queues = [{ t: 'a', url: 'https://www.startimestv.com/tv_guide.html' }] - await doFetch(queues, (queue, res) => { - // process area-id - if (queue.t === 'a') { - const $ = cheerio.load(res) - $('dd.update-areaID') - .toArray() - .forEach(el => { - const dd = $(el) - const areaId = dd.attr('area-id') - queues.push({ - t: 's', - url: 'https://www.startimestv.com/tv_guide.html', - params: { - headers: { - cookie: `default_areaID=${areaId}` - } - } - }) - }) - } - // process channel - if (queue.t === 's') { - if (res) { - const $ = cheerio.load(res) - $('.channl .c') - .toArray() - .forEach(el => { - // only process channel with schedule only - const clazz = $(el).attr('class') - const [idx] = clazz.match(/\d+/) || [null] - if (idx && $(`.item.item-${idx} .mask`).length) { - const ch = $(el).find('.pic a[title]') - const [site_id] = ch.attr('href').match(/\d+/) || [null] - if (channels[site_id] === undefined) { - channels[site_id] = { - lang: 'en', - name: ch.attr('title'), - site_id - } - } - } - }) - } - } - }) - - return Object.values(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 cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const doFetch = require('@ntlab/sfetch') +const debug = require('debug')('site:startimestv.com') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +doFetch.setDebugger(debug).setMaxWorker(5) + +module.exports = { + site: 'startimestv.com', + days: 2, + url({ channel, date }) { + return `https://www.startimestv.com/channeldetail/${channel.site_id}/${date.format( + 'YYYY-MM-DD' + )}.html` + }, + parser({ content, date }) { + const programs = [] + if (content) { + const $ = cheerio.load(content) + $('.box .mask') + .toArray() + .forEach(el => { + let title = parseText($(el).find('h4')) + const [s, e] = title.substr(0, title.indexOf(' ')).split('-') || [null, null] + const start = dayjs.utc(`${date.format('YYYY-MM-DD')} ${s}`, 'YYYY-MM-DD HH:nn') + const stop = dayjs.utc(`${date.format('YYYY-MM-DD')} ${e}`, 'YYYY-MM-DD HH:nn') + title = title.substr(title.indexOf(' ') + 1) + const [, season, episode] = title.match(/ S(\d+) E(\d+)/) || [null, null, null] + const description = parseText($(el).find('p')) + programs.push({ + title, + description: description !== 'NA' ? description : null, + season: season ? parseInt(season) : season, + episode: episode ? parseInt(episode) : episode, + start, + stop + }) + }) + } + + return programs + }, + async channels() { + const channels = {} + const queues = [{ t: 'a', url: 'https://www.startimestv.com/tv_guide.html' }] + await doFetch(queues, (queue, res) => { + // process area-id + if (queue.t === 'a') { + const $ = cheerio.load(res) + $('dd.update-areaID') + .toArray() + .forEach(el => { + const dd = $(el) + const areaId = dd.attr('area-id') + queues.push({ + t: 's', + url: 'https://www.startimestv.com/tv_guide.html', + params: { + headers: { + cookie: `default_areaID=${areaId}` + } + } + }) + }) + } + // process channel + if (queue.t === 's') { + if (res) { + const $ = cheerio.load(res) + $('.channl .c') + .toArray() + .forEach(el => { + // only process channel with schedule only + const clazz = $(el).attr('class') + const [idx] = clazz.match(/\d+/) || [null] + if (idx && $(`.item.item-${idx} .mask`).length) { + const ch = $(el).find('.pic a[title]') + const [site_id] = ch.attr('href').match(/\d+/) || [null] + if (channels[site_id] === undefined) { + channels[site_id] = { + lang: 'en', + name: ch.attr('title'), + site_id + } + } + } + }) + } + } + }) + + return Object.values(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/startimestv.com/startimestv.com.test.js b/sites/startimestv.com/startimestv.com.test.js index 836537e6..4c138475 100644 --- a/sites/startimestv.com/startimestv.com.test.js +++ b/sites/startimestv.com/startimestv.com.test.js @@ -1,48 +1,48 @@ -const { parser, url } = require('./startimestv.com.config.js') -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('2024-12-10', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1023102509', - xmltv_id: 'ZeeOneAfrica.za' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.startimestv.com/channeldetail/1023102509/2024-12-10.html' - ) -}) - -it('can parse response', () => { - const fs = require('fs') - const path = require('path') - const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.html')) - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result.length).toBe(22) - expect(result[0]).toMatchObject({ - start: '2024-12-10T00:00:00.000Z', - stop: '2024-12-10T01:00:00.000Z', - title: 'Deserted S1 E37', - description: - 'Tora approaches Tubri for help, but she expresses her helplessness in seeking assistance from Arjun. Meanwhile, other family members are caught in the crossfire, trying to navigate their own positions within the household.', - season: 1, - episode: 37 - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: - '

    Rate:

    Category:


    ' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./startimestv.com.config.js') +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('2024-12-10', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1023102509', + xmltv_id: 'ZeeOneAfrica.za' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.startimestv.com/channeldetail/1023102509/2024-12-10.html' + ) +}) + +it('can parse response', () => { + const fs = require('fs') + const path = require('path') + const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.html')) + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result.length).toBe(22) + expect(result[0]).toMatchObject({ + start: '2024-12-10T00:00:00.000Z', + stop: '2024-12-10T01:00:00.000Z', + title: 'Deserted S1 E37', + description: + 'Tora approaches Tubri for help, but she expresses her helplessness in seeking assistance from Arjun. Meanwhile, other family members are caught in the crossfire, trying to navigate their own positions within the household.', + season: 1, + episode: 37 + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: + '

    Rate:

    Category:


    ' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/stod2.is/__data__/content.json b/sites/stod2.is/__data__/content.json new file mode 100644 index 00000000..a08167b2 --- /dev/null +++ b/sites/stod2.is/__data__/content.json @@ -0,0 +1,34 @@ +[ + { + "midill": "STOD2", + "midill_heiti": "Stöð 2", + "dagsetning": "2025-01-03T00:00:00Z", + "upphaf": "2025-01-03T08:00:00Z", + "titill": "Telma Borgþórsdóttir", + "isltitill": "Heimsókn", + "undirtitill": "Telma Borgþórsdóttir", + "seria": 8, + "thattur": 5, + "thattafjoldi": 10, + "birta_thatt": 1, + "opin": 0, + "beint": 0, + "frumsyning": 0, + "framundan_i_beinni": 0, + "tegund": "SER", + "flokkur": "Icelandic", + "adalhlutverk": "", + "leikstjori": "", + "ar": "2019", + "bannad": "Green", + "recidefni": 592645105, + "recidlidur": 592645184, + "recidsyning": null, + "refno": null, + "frelsi": 0, + "netdagar": 0, + "lysing": "Frábærir þættir með Sindra Sindrasyni sem lítur inn hjá íslenskum fagurkerum. Heimilin eru jafn ólík og þau eru mörg en eiga það þó eitt sameiginlegt að vera sett saman af alúð og smekklegheitum. Sindri hefur líka einstakt lag á að ná fram því besta í viðmælendum sínum.", + "slott": 15, + "slotlengd": "00:15" + } +] \ No newline at end of file diff --git a/sites/stod2.is/stod2.is.config.js b/sites/stod2.is/stod2.is.config.js index fc91355d..c5cffee8 100644 --- a/sites/stod2.is/stod2.is.config.js +++ b/sites/stod2.is/stod2.is.config.js @@ -1,67 +1,67 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const axios = require('axios') - -dayjs.extend(utc) - -module.exports = { - site: 'stod2.is', - days: 7, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url({ channel, date }) { - return `https://api.stod2.is/dagskra/api/${channel.site_id}/${date.format('YYYY-MM-DD')}` - }, - parser: function ({ content }) { - let data - try { - data = JSON.parse(content) - } catch (error) { - console.error('Error parsing JSON:', error) - return [] - } - - const programs = [] - - if (data && Array.isArray(data)) { - data.forEach(item => { - if (!item) return - const start = dayjs.utc(item.upphaf) - const stop = start.add(item.slott, 'm') - - programs.push({ - title: item.isltitill, - sub_title: item.undirtitill, - description: item.lysing, - actors: item.adalhlutverk, - directors: item.leikstjori, - start, - stop - }) - }) - } - - return programs - }, - async channels() { - try { - const response = await axios.get('https://api.stod2.is/dagskra/api') - if (!response.data || !Array.isArray(response.data)) { - console.error('Error: No channels data found') - return [] - } - return response.data.map(item => { - return { - lang: 'is', - site_id: item - } - }) - } catch (error) { - console.error('Error fetching channels:', error) - return [] - } - } -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const axios = require('axios') + +dayjs.extend(utc) + +module.exports = { + site: 'stod2.is', + days: 7, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url({ channel, date }) { + return `https://api.stod2.is/dagskra/api/${channel.site_id}/${date.format('YYYY-MM-DD')}` + }, + parser: function ({ content }) { + let data + try { + data = JSON.parse(content) + } catch (error) { + console.error('Error parsing JSON:', error) + return [] + } + + const programs = [] + + if (data && Array.isArray(data)) { + data.forEach(item => { + if (!item) return + const start = dayjs.utc(item.upphaf) + const stop = start.add(item.slott, 'm') + + programs.push({ + title: item.isltitill, + sub_title: item.undirtitill, + description: item.lysing, + actors: item.adalhlutverk, + directors: item.leikstjori, + start, + stop + }) + }) + } + + return programs + }, + async channels() { + try { + const response = await axios.get('https://api.stod2.is/dagskra/api') + if (!response.data || !Array.isArray(response.data)) { + console.error('Error: No channels data found') + return [] + } + return response.data.map(item => { + return { + lang: 'is', + site_id: item + } + }) + } catch (error) { + console.error('Error fetching channels:', error) + return [] + } + } +} diff --git a/sites/stod2.is/stod2.is.test.js b/sites/stod2.is/stod2.is.test.js index 4dba9150..2c7a2130 100644 --- a/sites/stod2.is/stod2.is.test.js +++ b/sites/stod2.is/stod2.is.test.js @@ -1,80 +1,46 @@ -const { parser, url } = require('./stod2.is.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) -dayjs.extend(timezone) - -const date = dayjs.utc('2025-01-03', 'YYYY-MM-DD').startOf('day') -const channel = { site_id: 'stod2', xmltv_id: 'Stod2.is' } - -const mockEpgData = JSON.stringify([ - { - midill: 'STOD2', - midill_heiti: 'Stöð 2', - dagsetning: '2025-01-03T00:00:00Z', - upphaf: '2025-01-03T08:00:00Z', - titill: 'Telma Borgþórsdóttir', - isltitill: 'Heimsókn', - undirtitill: 'Telma Borgþórsdóttir', - seria: 8, - thattur: 5, - thattafjoldi: 10, - birta_thatt: 1, - opin: 0, - beint: 0, - frumsyning: 0, - framundan_i_beinni: 0, - tegund: 'SER', - flokkur: 'Icelandic', - adalhlutverk: '', - leikstjori: '', - ar: '2019', - bannad: 'Green', - recidefni: 592645105, - recidlidur: 592645184, - recidsyning: null, - refno: null, - frelsi: 0, - netdagar: 0, - lysing: - 'Frábærir þættir með Sindra Sindrasyni sem lítur inn hjá íslenskum fagurkerum. Heimilin eru jafn ólík og þau eru mörg en eiga það þó eitt sameiginlegt að vera sett saman af alúð og smekklegheitum. Sindri hefur líka einstakt lag á að ná fram því besta í viðmælendum sínum.', - slott: 15, - slotlengd: '00:15' - } -]) - -it('can generate valid url', () => { - const generatedUrl = url({ date, channel }) - expect(generatedUrl).toBe('https://api.stod2.is/dagskra/api/stod2/2025-01-03') -}) - -it('can parse response', () => { - const content = mockEpgData - const result = parser({ content }).map(p => { - p.start = p.start.toISOString() - p.stop = p.stop.toISOString() - return p - }) - - expect(result).toMatchObject([ - { - title: 'Heimsókn', - sub_title: 'Telma Borgþórsdóttir', - description: - 'Frábærir þættir með Sindra Sindrasyni sem lítur inn hjá íslenskum fagurkerum. Heimilin eru jafn ólík og þau eru mörg en eiga það þó eitt sameiginlegt að vera sett saman af alúð og smekklegheitum. Sindri hefur líka einstakt lag á að ná fram því besta í viðmælendum sínum.', - actors: '', - directors: '', - start: '2025-01-03T08:00:00.000Z', - stop: '2025-01-03T08:15:00.000Z' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ content: '[]' }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./stod2.is.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') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) +dayjs.extend(timezone) + +const date = dayjs.utc('2025-01-03', 'YYYY-MM-DD').startOf('day') +const channel = { site_id: 'stod2', xmltv_id: 'Stod2.is' } + +it('can generate valid url', () => { + const generatedUrl = url({ date, channel }) + expect(generatedUrl).toBe('https://api.stod2.is/dagskra/api/stod2/2025-01-03') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content }).map(p => { + p.start = p.start.toISOString() + p.stop = p.stop.toISOString() + return p + }) + + expect(result).toMatchObject([ + { + title: 'Heimsókn', + sub_title: 'Telma Borgþórsdóttir', + description: + 'Frábærir þættir með Sindra Sindrasyni sem lítur inn hjá íslenskum fagurkerum. Heimilin eru jafn ólík og þau eru mörg en eiga það þó eitt sameiginlegt að vera sett saman af alúð og smekklegheitum. Sindri hefur líka einstakt lag á að ná fram því besta í viðmælendum sínum.', + actors: '', + directors: '', + start: '2025-01-03T08:00:00.000Z', + stop: '2025-01-03T08:15:00.000Z' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ content: '[]' }) + expect(result).toMatchObject([]) +}) diff --git a/sites/streamingtvguides.com/streamingtvguides.com.config.js b/sites/streamingtvguides.com/streamingtvguides.com.config.js index 440ca9e1..75f0f976 100644 --- a/sites/streamingtvguides.com/streamingtvguides.com.config.js +++ b/sites/streamingtvguides.com/streamingtvguides.com.config.js @@ -1,97 +1,98 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const timezone = require('dayjs/plugin/timezone') -const _ = require('lodash') - -dayjs.extend(customParseFormat) -dayjs.extend(timezone) - -module.exports = { - site: 'streamingtvguides.com', - days: 2, - url({ channel }) { - return `https://streamingtvguides.com/Channel/${channel.site_id}` - }, - parser({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const start = parseStart($item) - if (!date.isSame(start, 'd')) return - - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - start, - stop: parseStop($item) - }) - }) - - programs = _.orderBy(_.uniqBy(programs, 'start'), 'start') - - return programs - }, - async channels() { - const axios = require('axios') - const data = await axios - .get('https://streamingtvguides.com/Preferences') - .then(r => r.data) - .catch(console.log) - - let channels = [] - - const $ = cheerio.load(data) - $('#channel-group-all > div > div').each((i, el) => { - const site_id = $(el).find('input').attr('value').replace('&', '&') - const label = $(el).text().trim() - const svgTitle = $(el).find('svg').attr('alt') - const name = (label || svgTitle || '').replace(site_id, '').trim() - - if (!name || !site_id) return - - channels.push({ - lang: 'en', - site_id, - name - }) - }) - - return channels - } -} - -function parseTitle($item) { - return $item('.card-body > .prog-contains > .card-title') - .clone() - .children() - .remove() - .end() - .text() - .trim() -} - -function parseDescription($item) { - return $item('.card-body > .card-text').clone().children().remove().end().text().trim() -} - -function parseStart($item) { - const date = $item('.card-body').clone().children().remove().end().text().trim() - const [time] = date.split(' - ') - - return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss [PST]', 'PST').utc() -} - -function parseStop($item) { - const date = $item('.card-body').clone().children().remove().end().text().trim() - const [, time] = date.split(' - ') - - return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss [PST]', 'PST').utc() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.container').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const timezone = require('dayjs/plugin/timezone') +const sortBy = require('lodash.sortby') +const uniqBy = require('lodash.uniqby') + +dayjs.extend(customParseFormat) +dayjs.extend(timezone) + +module.exports = { + site: 'streamingtvguides.com', + days: 2, + url({ channel }) { + return `https://streamingtvguides.com/Channel/${channel.site_id}` + }, + parser({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const start = parseStart($item) + if (!date.isSame(start, 'd')) return + + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + start, + stop: parseStop($item) + }) + }) + + programs = sortBy(uniqBy(programs, p => p.start), p => p.start.valueOf()) + + return programs + }, + async channels() { + const axios = require('axios') + const data = await axios + .get('https://streamingtvguides.com/Preferences') + .then(r => r.data) + .catch(console.log) + + let channels = [] + + const $ = cheerio.load(data) + $('#channel-group-all > div > div').each((i, el) => { + const site_id = $(el).find('input').attr('value').replace('&', '&') + const label = $(el).text().trim() + const svgTitle = $(el).find('svg').attr('alt') + const name = (label || svgTitle || '').replace(site_id, '').trim() + + if (!name || !site_id) return + + channels.push({ + lang: 'en', + site_id, + name + }) + }) + + return channels + } +} + +function parseTitle($item) { + return $item('.card-body > .prog-contains > .card-title') + .clone() + .children() + .remove() + .end() + .text() + .trim() +} + +function parseDescription($item) { + return $item('.card-body > .card-text').clone().children().remove().end().text().trim() +} + +function parseStart($item) { + const date = $item('.card-body').clone().children().remove().end().text().trim() + const [time] = date.split(' - ') + + return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss [PST]', 'PST').utc() +} + +function parseStop($item) { + const date = $item('.card-body').clone().children().remove().end().text().trim() + const [, time] = date.split(' - ') + + return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss [PST]', 'PST').utc() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.container').toArray() +} diff --git a/sites/streamingtvguides.com/streamingtvguides.com.test.js b/sites/streamingtvguides.com/streamingtvguides.com.test.js index 1539f575..57240ba2 100644 --- a/sites/streamingtvguides.com/streamingtvguides.com.test.js +++ b/sites/streamingtvguides.com/streamingtvguides.com.test.js @@ -1,53 +1,53 @@ -const { parser, url } = require('./streamingtvguides.com.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('2023-06-27', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'GMAPNY', - xmltv_id: 'GMAPinoyTVUSACanada.ph' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://streamingtvguides.com/Channel/GMAPNY') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(38) - - expect(results[0]).toMatchObject({ - start: '2023-06-27T00:40:00.000Z', - stop: '2023-06-27T02:00:00.000Z', - title: '24 Oras', - description: 'Up to the minute news around the world.' - }) - - expect(results[37]).toMatchObject({ - start: '2023-06-27T21:50:00.000Z', - stop: '2023-06-28T00:00:00.000Z', - title: 'Eat Bulaga', - description: 'Rousing and engrossing segments with engaging hosts.' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - const result = parser({ - date, - content - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./streamingtvguides.com.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('2023-06-27', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'GMAPNY', + xmltv_id: 'GMAPinoyTVUSACanada.ph' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://streamingtvguides.com/Channel/GMAPNY') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(38) + + expect(results[0]).toMatchObject({ + start: '2023-06-27T00:40:00.000Z', + stop: '2023-06-27T02:00:00.000Z', + title: '24 Oras', + description: 'Up to the minute news around the world.' + }) + + expect(results[37]).toMatchObject({ + start: '2023-06-27T21:50:00.000Z', + stop: '2023-06-28T00:00:00.000Z', + title: 'Eat Bulaga', + description: 'Rousing and engrossing segments with engaging hosts.' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + const result = parser({ + date, + content + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/superguidatv.it/superguidatv.it.config.js b/sites/superguidatv.it/superguidatv.it.config.js index 6a89e9fe..b8d97142 100644 --- a/sites/superguidatv.it/superguidatv.it.config.js +++ b/sites/superguidatv.it/superguidatv.it.config.js @@ -1,117 +1,117 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const { DateTime } = require('luxon') - -module.exports = { - site: 'superguidatv.it', - days: 3, - url({ channel, date }) { - let diff = date.diff(DateTime.now().toUTC().startOf('day'), 'd') - let day = { - 0: 'oggi', - 1: 'domani', - 2: 'dopodomani' - } - - return `https://www.superguidatv.it/programmazione-canale/${day[diff]}/guida-programmi-tv-${channel.site_id}/` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ minutes: 30 }) - programs.push({ - title: parseTitle($item), - category: parseCategory($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const providers = [ - '', - 'premium/', - 'sky-intrattenimento/', - 'sky-sport/', - 'sky-cinema/', - 'sky-doc-e-lifestyle/', - 'sky-news/', - 'sky-bambini/', - 'sky-musica/', - 'sky-primafila/', - 'dazn/', - 'rsi/' - ] - const promises = providers.map(p => axios.get(`https://www.superguidatv.it/canali/${p}`)) - - const channels = [] - await Promise.all(promises) - .then(responses => { - responses.forEach(r => { - const $ = cheerio.load(r.data) - - $('.sgtvchannellist_mainContainer .sgtvchannel_divCell a').each((i, link) => { - let [, site_id] = $(link) - .attr('href') - .match(/guida-programmi-tv-(.*)\/$/) || [null, null] - let name = $(link).find('.pchannel').text().trim() - - channels.push({ - lang: 'it', - site_id, - name - }) - }) - }) - }) - .catch(console.log) - - return channels - } -} - -function parseStart($item, date) { - const hours = $item('.sgtvchannelplan_hoursCell') - .clone() - .children('.sgtvOnairSpan') - .remove() - .end() - .text() - .trim() - - return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${hours}`, 'yyyy-MM-dd HH:mm', { - zone: 'Europe/Rome' - }).toUTC() -} - -function parseTitle($item) { - return $item('.sgtvchannelplan_spanInfoNextSteps').text().trim() -} - -function parseCategory($item) { - const eventType = $item('.sgtvchannelplan_spanEventType').text().trim() - const [, category] = eventType.match(/(^[^(]+)/) || [null, ''] - - return category.trim() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.sgtvchannelplan_divContainer > .sgtvchannelplan_divTableRow') - .has('#containerInfoEvent') - .toArray() -} +const cheerio = require('cheerio') +const axios = require('axios') +const { DateTime } = require('luxon') + +module.exports = { + site: 'superguidatv.it', + days: 3, + url({ channel, date }) { + let diff = date.diff(DateTime.now().toUTC().startOf('day'), 'd') + let day = { + 0: 'oggi', + 1: 'domani', + 2: 'dopodomani' + } + + return `https://www.superguidatv.it/programmazione-canale/${day[diff]}/guida-programmi-tv-${channel.site_id}/` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ minutes: 30 }) + programs.push({ + title: parseTitle($item), + category: parseCategory($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const providers = [ + '', + 'premium/', + 'sky-intrattenimento/', + 'sky-sport/', + 'sky-cinema/', + 'sky-doc-e-lifestyle/', + 'sky-news/', + 'sky-bambini/', + 'sky-musica/', + 'sky-primafila/', + 'dazn/', + 'rsi/' + ] + const promises = providers.map(p => axios.get(`https://www.superguidatv.it/canali/${p}`)) + + const channels = [] + await Promise.all(promises) + .then(responses => { + responses.forEach(r => { + const $ = cheerio.load(r.data) + + $('.sgtvchannellist_mainContainer .sgtvchannel_divCell a').each((i, link) => { + let [, site_id] = $(link) + .attr('href') + .match(/guida-programmi-tv-(.*)\/$/) || [null, null] + let name = $(link).find('.pchannel').text().trim() + + channels.push({ + lang: 'it', + site_id, + name + }) + }) + }) + }) + .catch(console.log) + + return channels + } +} + +function parseStart($item, date) { + const hours = $item('.sgtvchannelplan_hoursCell') + .clone() + .children('.sgtvOnairSpan') + .remove() + .end() + .text() + .trim() + + return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${hours}`, 'yyyy-MM-dd HH:mm', { + zone: 'Europe/Rome' + }).toUTC() +} + +function parseTitle($item) { + return $item('.sgtvchannelplan_spanInfoNextSteps').text().trim() +} + +function parseCategory($item) { + const eventType = $item('.sgtvchannelplan_spanEventType').text().trim() + const [, category] = eventType.match(/(^[^(]+)/) || [null, ''] + + return category.trim() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.sgtvchannelplan_divContainer > .sgtvchannelplan_divTableRow') + .has('#containerInfoEvent') + .toArray() +} diff --git a/sites/superguidatv.it/superguidatv.it.test.js b/sites/superguidatv.it/superguidatv.it.test.js index 90b40564..9ef52015 100644 --- a/sites/superguidatv.it/superguidatv.it.test.js +++ b/sites/superguidatv.it/superguidatv.it.test.js @@ -1,64 +1,64 @@ -const { parser, url } = require('./superguidatv.it.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('2023-01-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'virgin-radio/461', - xmltv_id: 'VirginRadioTV.it' -} - -it('can generate valid url', () => { - expect(url({ channel, date: dayjs.utc().startOf('d') })).toBe( - 'https://www.superguidatv.it/programmazione-canale/oggi/guida-programmi-tv-virgin-radio/461/' - ) -}) - -it('can generate valid url for tomorrow', () => { - expect(url({ channel, date: dayjs.utc().startOf('d').add(1, 'd') })).toBe( - 'https://www.superguidatv.it/programmazione-canale/domani/guida-programmi-tv-virgin-radio/461/' - ) -}) - -it('can generate valid url for after tomorrow', () => { - expect(url({ channel, date: dayjs.utc().startOf('d').add(2, 'd') })).toBe( - 'https://www.superguidatv.it/programmazione-canale/dopodomani/guida-programmi-tv-virgin-radio/461/' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - 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: '2023-01-11T01:00:00.000Z', - stop: '2023-01-11T05:00:00.000Z', - title: 'All Nite Rock', - category: 'Musica' - }) - - expect(results[13]).toMatchObject({ - start: '2023-01-12T05:00:00.000Z', - stop: '2023-01-12T05:30:00.000Z', - title: 'Free Rock', - category: 'Musica' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./superguidatv.it.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('2023-01-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'virgin-radio/461', + xmltv_id: 'VirginRadioTV.it' +} + +it('can generate valid url', () => { + expect(url({ channel, date: dayjs.utc().startOf('d') })).toBe( + 'https://www.superguidatv.it/programmazione-canale/oggi/guida-programmi-tv-virgin-radio/461/' + ) +}) + +it('can generate valid url for tomorrow', () => { + expect(url({ channel, date: dayjs.utc().startOf('d').add(1, 'd') })).toBe( + 'https://www.superguidatv.it/programmazione-canale/domani/guida-programmi-tv-virgin-radio/461/' + ) +}) + +it('can generate valid url for after tomorrow', () => { + expect(url({ channel, date: dayjs.utc().startOf('d').add(2, 'd') })).toBe( + 'https://www.superguidatv.it/programmazione-canale/dopodomani/guida-programmi-tv-virgin-radio/461/' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + 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: '2023-01-11T01:00:00.000Z', + stop: '2023-01-11T05:00:00.000Z', + title: 'All Nite Rock', + category: 'Musica' + }) + + expect(results[13]).toMatchObject({ + start: '2023-01-12T05:00:00.000Z', + stop: '2023-01-12T05:30:00.000Z', + title: 'Free Rock', + category: 'Musica' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/taiwanplus.com/__data__/content.json b/sites/taiwanplus.com/__data__/content.json new file mode 100644 index 00000000..021decf9 --- /dev/null +++ b/sites/taiwanplus.com/__data__/content.json @@ -0,0 +1 @@ +{"data":[{"date":"2023/08/20","weekday":"SUN","schedule":[{"programId":30668,"dateTime":"2023/08/20 00:00","time":"00:00","image":"https://prod-img.taiwanplus.com/live-schedule/Single/S30668_20230810104937.webp","title":"Master Class","shortDescription":"From blockchain to Buddha statues, Taiwan’s culture is a kaleidoscope of old and new just waiting to be discovered.","description":"From blockchain to Buddha statues, Taiwan’s culture is a kaleidoscope of old and new just waiting to be discovered.","ageRating":"0+","programWebSiteType":"4","url":"","vodId":null,"categoryId":90000474,"categoryType":2,"categoryName":"TaiwanPlus ✕ Discovery","categoryFullPath":"Originals/TaiwanPlus ✕ Discovery","encodedCategoryFullPath":"originals/taiwanplus-discovery"}]}],"success":true,"code":"0000","message":""} \ No newline at end of file diff --git a/sites/taiwanplus.com/taiwanplus.com.config.js b/sites/taiwanplus.com/taiwanplus.com.config.js index d4ec5913..54bccc0c 100644 --- a/sites/taiwanplus.com/taiwanplus.com.config.js +++ b/sites/taiwanplus.com/taiwanplus.com.config.js @@ -1,68 +1,68 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const isSameOrAfter = require('dayjs/plugin/isSameOrAfter') -const isSameOrBefore = require('dayjs/plugin/isSameOrBefore') - -dayjs.extend(utc) -dayjs.extend(isSameOrAfter) -dayjs.extend(isSameOrBefore) - -module.exports = { - site: 'taiwanplus.com', - days: 7, - output: 'taiwanplus.com.guide.xml', - channels: 'taiwanplus.com.channels.xml', - lang: 'en', - - url: function () { - return 'https://www.taiwanplus.com/api/video/live/schedule/0' - }, - - request: { - method: 'GET', - timeout: 5000, - cache: { ttl: 60 * 60 * 1000 } // 60 * 60 seconds = 1 hour - }, - - logo: function (context) { - return context.channel.logo - }, - - parser: function (context) { - const programs = [] - const scheduleDates = parseItems(context.content) - const today = dayjs.utc(context.date).startOf('day') - - for (let scheduleDate of scheduleDates) { - const currentScheduleDate = new dayjs.utc(scheduleDate.date, 'YYYY/MM/DD') - - if (currentScheduleDate.isSame(today)) { - scheduleDate.schedule.forEach(function (program, i) { - programs.push({ - title: program.title, - start: dayjs.utc(program.dateTime, 'YYYY/MM/DD HH:mm'), - stop: - i != scheduleDate.schedule.length - 1 - ? dayjs.utc(scheduleDate.schedule[i + 1].dateTime, 'YYYY/MM/DD HH:mm') - : dayjs.utc(program.dateTime, 'YYYY/MM/DD HH:mm').add(1, 'day').startOf('day'), - description: program.description, - image: program.image, - category: program.categoryName, - rating: program.ageRating - }) - }) - } - } - - return programs - } -} - -function parseItems(content) { - if (content != '') { - const data = JSON.parse(content) - return !data || !data.data || !Array.isArray(data.data) ? [] : data.data - } else { - return [] - } -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const isSameOrAfter = require('dayjs/plugin/isSameOrAfter') +const isSameOrBefore = require('dayjs/plugin/isSameOrBefore') + +dayjs.extend(utc) +dayjs.extend(isSameOrAfter) +dayjs.extend(isSameOrBefore) + +module.exports = { + site: 'taiwanplus.com', + days: 7, + output: 'taiwanplus.com.guide.xml', + channels: 'taiwanplus.com.channels.xml', + lang: 'en', + + url: function () { + return 'https://www.taiwanplus.com/api/video/live/schedule/0' + }, + + request: { + method: 'GET', + timeout: 5000, + cache: { ttl: 60 * 60 * 1000 } // 60 * 60 seconds = 1 hour + }, + + logo: function (context) { + return context.channel.logo + }, + + parser: function (context) { + const programs = [] + const scheduleDates = parseItems(context.content) + const today = dayjs.utc(context.date).startOf('day') + + for (let scheduleDate of scheduleDates) { + const currentScheduleDate = new dayjs.utc(scheduleDate.date, 'YYYY/MM/DD') + + if (currentScheduleDate.isSame(today)) { + scheduleDate.schedule.forEach(function (program, i) { + programs.push({ + title: program.title, + start: dayjs.utc(program.dateTime, 'YYYY/MM/DD HH:mm'), + stop: + i != scheduleDate.schedule.length - 1 + ? dayjs.utc(scheduleDate.schedule[i + 1].dateTime, 'YYYY/MM/DD HH:mm') + : dayjs.utc(program.dateTime, 'YYYY/MM/DD HH:mm').add(1, 'day').startOf('day'), + description: program.description, + image: program.image, + category: program.categoryName, + rating: program.ageRating + }) + }) + } + } + + return programs + } +} + +function parseItems(content) { + if (content != '') { + const data = JSON.parse(content) + return !data || !data.data || !Array.isArray(data.data) ? [] : data.data + } else { + return [] + } +} diff --git a/sites/taiwanplus.com/taiwanplus.com.test.js b/sites/taiwanplus.com/taiwanplus.com.test.js index 57784598..6d846f5b 100644 --- a/sites/taiwanplus.com/taiwanplus.com.test.js +++ b/sites/taiwanplus.com/taiwanplus.com.test.js @@ -1,42 +1,43 @@ -const { url, parser } = require('./taiwanplus.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2023-08-20', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '#', - xmltv_id: 'TaiwanPlusTV.tw', - lang: 'en', - logo: 'https://i.imgur.com/SfcZyqm.png' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://www.taiwanplus.com/api/video/live/schedule/0') -}) - -it('can parse response', () => { - const content = - '{"data":[{"date":"2023/08/20","weekday":"SUN","schedule":[{"programId":30668,"dateTime":"2023/08/20 00:00","time":"00:00","image":"https://prod-img.taiwanplus.com/live-schedule/Single/S30668_20230810104937.webp","title":"Master Class","shortDescription":"From blockchain to Buddha statues, Taiwan’s culture is a kaleidoscope of old and new just waiting to be discovered.","description":"From blockchain to Buddha statues, Taiwan’s culture is a kaleidoscope of old and new just waiting to be discovered.","ageRating":"0+","programWebSiteType":"4","url":"","vodId":null,"categoryId":90000474,"categoryType":2,"categoryName":"TaiwanPlus ✕ Discovery","categoryFullPath":"Originals/TaiwanPlus ✕ Discovery","encodedCategoryFullPath":"originals/taiwanplus-discovery"}]}],"success":true,"code":"0000","message":""}' - - const results = parser({ content, date }) - - expect(results).toMatchObject([ - { - title: 'Master Class', - start: dayjs.utc('2023/08/20 00:00', 'YYYY/MM/DD HH:mm'), - stop: dayjs.utc('2023/08/21 00:00', 'YYYY/MM/DD HH:mm'), - description: - 'From blockchain to Buddha statues, Taiwan’s culture is a kaleidoscope of old and new just waiting to be discovered.', - image: 'https://prod-img.taiwanplus.com/live-schedule/Single/S30668_20230810104937.webp', - category: 'TaiwanPlus ✕ Discovery', - rating: '0+' - } - ]) -}) - -it('can handle empty guide', () => { - const results = parser({ content: '' }) - - expect(results).toMatchObject([]) -}) +const { url, parser } = require('./taiwanplus.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2023-08-20', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '#', + xmltv_id: 'TaiwanPlusTV.tw', + lang: 'en', + logo: 'https://i.imgur.com/SfcZyqm.png' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://www.taiwanplus.com/api/video/live/schedule/0') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + + const results = parser({ content, date }) + + expect(results).toMatchObject([ + { + title: 'Master Class', + start: dayjs.utc('2023/08/20 00:00', 'YYYY/MM/DD HH:mm'), + stop: dayjs.utc('2023/08/21 00:00', 'YYYY/MM/DD HH:mm'), + description: + 'From blockchain to Buddha statues, Taiwan’s culture is a kaleidoscope of old and new just waiting to be discovered.', + image: 'https://prod-img.taiwanplus.com/live-schedule/Single/S30668_20230810104937.webp', + category: 'TaiwanPlus ✕ Discovery', + rating: '0+' + } + ]) +}) + +it('can handle empty guide', () => { + const results = parser({ content: '' }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/tapdmv.com/__data__/content.json b/sites/tapdmv.com/__data__/content.json new file mode 100644 index 00000000..989997a5 --- /dev/null +++ b/sites/tapdmv.com/__data__/content.json @@ -0,0 +1 @@ +[{"id":"0afc3cc0-eab8-4960-a8b5-55d76edeb8f0","program":"The Bourne Ultimatum","episode":"The Bourne Ultimatum","description":"Jason Bourne dodges a ruthless C.I.A. official and his Agents from a new assassination program while searching for the origins of his life as a trained killer.","genre":"Action","thumbnailImage":"https://s3.ap-southeast-1.amazonaws.com/epg.tapdmv.com/tapactionflix.png","startTime":"2022-10-03T23:05:00.000Z","endTime":"2022-10-04T01:00:00.000Z","fileId":"94b7db9b-5bbd-47d3-a2d3-ce792342a756","createdAt":"2022-09-30T13:02:10.586Z","updatedAt":"2022-09-30T13:02:10.586Z"},{"id":"8dccd5e0-ab88-44b6-a2af-18d31c6e9ed7","program":"The Devil Inside ","episode":"The Devil Inside ","description":"In Italy, a woman becomes involved in a series of unauthorized exorcisms during her mission to discover what happened to her mother, who allegedly murdered three people during her own exorcism.","genre":"Horror","thumbnailImage":"https://s3.ap-southeast-1.amazonaws.com/epg.tapdmv.com/tapactionflix.png","startTime":"2022-10-04T01:00:00.000Z","endTime":"2022-10-04T02:25:00.000Z","fileId":"94b7db9b-5bbd-47d3-a2d3-ce792342a756","createdAt":"2022-09-30T13:02:24.031Z","updatedAt":"2022-09-30T13:02:24.031Z"}] \ No newline at end of file diff --git a/sites/tapdmv.com/__data__/no_content.json b/sites/tapdmv.com/__data__/no_content.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/sites/tapdmv.com/__data__/no_content.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/sites/tapdmv.com/tapdmv.com.config.js b/sites/tapdmv.com/tapdmv.com.config.js index 01581631..15d0255e 100644 --- a/sites/tapdmv.com/tapdmv.com.config.js +++ b/sites/tapdmv.com/tapdmv.com.config.js @@ -1,65 +1,65 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'tapdmv.com', - days: 2, - request: { - maxContentLength: 10485760 // 10 Mb - }, - url({ channel, date }) { - return `https://epg.tapdmv.com/calendar/${ - channel.site_id - }?%24limit=10000&%24sort%5BcreatedAt%5D=-1&start=${date.toJSON()}&end=${date - .add(1, 'd') - .toJSON()}` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content, date) - items.forEach(item => { - programs.push({ - title: item.program.trim(), - description: item.description, - category: item.genre, - image: item.thumbnailImage, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const items = await axios - .get('https://epg.tapdmv.com/calendar?$limit=10000&$sort[createdAt]=-1') - .then(r => r.data.data) - .catch(console.log) - - return items.map(item => { - const [, name] = item.name.match(/epg-tapgo-([^.]+).json/) - return { - lang: 'en', - site_id: item.id, - name - } - }) - } -} - -function parseStart(item) { - return dayjs(item.startTime) -} - -function parseStop(item) { - return dayjs(item.endTime) -} - -function parseItems(content, date) { - if (!content) return [] - const data = JSON.parse(content) - if (!Array.isArray(data)) return [] - const d = date.format('YYYY-MM-DD') - - return data.filter(i => i.startTime.includes(d)) -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'tapdmv.com', + days: 2, + request: { + maxContentLength: 10485760 // 10 Mb + }, + url({ channel, date }) { + return `https://epg.tapdmv.com/calendar/${ + channel.site_id + }?%24limit=10000&%24sort%5BcreatedAt%5D=-1&start=${date.toJSON()}&end=${date + .add(1, 'd') + .toJSON()}` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content, date) + items.forEach(item => { + programs.push({ + title: item.program.trim(), + description: item.description, + category: item.genre, + image: item.thumbnailImage, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const items = await axios + .get('https://epg.tapdmv.com/calendar?$limit=10000&$sort[createdAt]=-1') + .then(r => r.data.data) + .catch(console.log) + + return items.map(item => { + const [, name] = item.name.match(/epg-tapgo-([^.]+).json/) + return { + lang: 'en', + site_id: item.id, + name + } + }) + } +} + +function parseStart(item) { + return dayjs(item.startTime) +} + +function parseStop(item) { + return dayjs(item.endTime) +} + +function parseItems(content, date) { + if (!content) return [] + const data = JSON.parse(content) + if (!Array.isArray(data)) return [] + const d = date.format('YYYY-MM-DD') + + return data.filter(i => i.startTime.includes(d)) +} diff --git a/sites/tapdmv.com/tapdmv.com.test.js b/sites/tapdmv.com/tapdmv.com.test.js index be9d8d03..15c355dd 100644 --- a/sites/tapdmv.com/tapdmv.com.test.js +++ b/sites/tapdmv.com/tapdmv.com.test.js @@ -1,48 +1,49 @@ -const { parser, url } = require('./tapdmv.com.config.js') -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('2022-10-04', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '94b7db9b-5bbd-47d3-a2d3-ce792342a756', - xmltv_id: 'TAPActionFlix.ph' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://epg.tapdmv.com/calendar/94b7db9b-5bbd-47d3-a2d3-ce792342a756?%24limit=10000&%24sort%5BcreatedAt%5D=-1&start=2022-10-04T00:00:00.000Z&end=2022-10-05T00:00:00.000Z' - ) -}) - -it('can parse response', () => { - const content = - '[{"id":"0afc3cc0-eab8-4960-a8b5-55d76edeb8f0","program":"The Bourne Ultimatum","episode":"The Bourne Ultimatum","description":"Jason Bourne dodges a ruthless C.I.A. official and his Agents from a new assassination program while searching for the origins of his life as a trained killer.","genre":"Action","thumbnailImage":"https://s3.ap-southeast-1.amazonaws.com/epg.tapdmv.com/tapactionflix.png","startTime":"2022-10-03T23:05:00.000Z","endTime":"2022-10-04T01:00:00.000Z","fileId":"94b7db9b-5bbd-47d3-a2d3-ce792342a756","createdAt":"2022-09-30T13:02:10.586Z","updatedAt":"2022-09-30T13:02:10.586Z"},{"id":"8dccd5e0-ab88-44b6-a2af-18d31c6e9ed7","program":"The Devil Inside ","episode":"The Devil Inside ","description":"In Italy, a woman becomes involved in a series of unauthorized exorcisms during her mission to discover what happened to her mother, who allegedly murdered three people during her own exorcism.","genre":"Horror","thumbnailImage":"https://s3.ap-southeast-1.amazonaws.com/epg.tapdmv.com/tapactionflix.png","startTime":"2022-10-04T01:00:00.000Z","endTime":"2022-10-04T02:25:00.000Z","fileId":"94b7db9b-5bbd-47d3-a2d3-ce792342a756","createdAt":"2022-09-30T13:02:24.031Z","updatedAt":"2022-09-30T13:02:24.031Z"}]' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-10-04T01:00:00.000Z', - stop: '2022-10-04T02:25:00.000Z', - title: 'The Devil Inside', - description: - 'In Italy, a woman becomes involved in a series of unauthorized exorcisms during her mission to discover what happened to her mother, who allegedly murdered three people during her own exorcism.', - category: 'Horror', - image: 'https://s3.ap-southeast-1.amazonaws.com/epg.tapdmv.com/tapactionflix.png' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '[]', - date - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./tapdmv.com.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('2022-10-04', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '94b7db9b-5bbd-47d3-a2d3-ce792342a756', + xmltv_id: 'TAPActionFlix.ph' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://epg.tapdmv.com/calendar/94b7db9b-5bbd-47d3-a2d3-ce792342a756?%24limit=10000&%24sort%5BcreatedAt%5D=-1&start=2022-10-04T00:00:00.000Z&end=2022-10-05T00:00:00.000Z' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-10-04T01:00:00.000Z', + stop: '2022-10-04T02:25:00.000Z', + title: 'The Devil Inside', + description: + 'In Italy, a woman becomes involved in a series of unauthorized exorcisms during her mission to discover what happened to her mother, who allegedly murdered three people during her own exorcism.', + category: 'Horror', + image: 'https://s3.ap-southeast-1.amazonaws.com/epg.tapdmv.com/tapactionflix.png' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')), + date + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tataplay.com/__data__/content.json b/sites/tataplay.com/__data__/content.json new file mode 100644 index 00000000..07b9a7b2 --- /dev/null +++ b/sites/tataplay.com/__data__/content.json @@ -0,0 +1,22 @@ +{ + "data": { + "epg": [ + { + "title": "Yeh Rishta Kya Kehlata Hai", + "startTime": "2025-06-09T18:00:00.000Z", + "endTime": "2025-06-09T18:30:00.000Z", + "desc": "The story of the Rajshri family and their journey through life.", + "category": "Drama", + "boxCoverImage": "https://img.tataplay.com/thumbnails/1001/yeh-rishta.jpg" + }, + { + "title": "Anupamaa", + "startTime": "2025-06-09T18:30:00.000Z", + "endTime": "2025-06-09T19:00:00.000Z", + "desc": "The story of Anupamaa, a housewife who rediscovers herself.", + "category": "Drama", + "boxCoverImage": "https://img.tataplay.com/thumbnails/1001/anupamaa.jpg" + } + ] + } +} \ No newline at end of file diff --git a/sites/tataplay.com/tataplay.com.config.js b/sites/tataplay.com/tataplay.com.config.js index e8a8ed43..57142c3e 100644 --- a/sites/tataplay.com/tataplay.com.config.js +++ b/sites/tataplay.com/tataplay.com.config.js @@ -4,20 +4,23 @@ module.exports = { site: 'tataplay.com', days: 1, - url({ channel, date }) { - return `https://tm.tapi.videoready.tv/content-detail/pub/api/v2/channels/schedule?date=${date.format('DD-MM-YYYY')}` + url({ date }) { + return `https://tm.tapi.videoready.tv/content-detail/pub/api/v2/channels/schedule?date=${date.format( + 'DD-MM-YYYY' + )}` }, request: { method: 'POST', headers: { - 'Accept': '*/*', - 'Origin': 'https://watch.tataplay.com', - 'Referer': 'https://watch.tataplay.com/', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + Accept: '*/*', + Origin: 'https://watch.tataplay.com', + Referer: 'https://watch.tataplay.com/', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'content-type': 'application/json', - 'locale': 'ENG', - 'platform': 'web' + locale: 'ENG', + platform: 'web' }, data({ channel }) { return { id: channel.site_id } @@ -46,13 +49,14 @@ module.exports = { async channels() { const headers = { - 'Accept': '*/*', - 'Origin': 'https://watch.tataplay.com', - 'Referer': 'https://watch.tataplay.com/', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + Accept: '*/*', + Origin: 'https://watch.tataplay.com', + Referer: 'https://watch.tataplay.com/', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'content-type': 'application/json', - 'locale': 'ENG', - 'platform': 'web' + locale: 'ENG', + platform: 'web' } const baseUrl = 'https://tm.tapi.videoready.tv/portal-search/pub/api/v1/channels/schedule' diff --git a/sites/tataplay.com/tataplay.com.test.js b/sites/tataplay.com/tataplay.com.test.js index 5262f6cd..9f721491 100644 --- a/sites/tataplay.com/tataplay.com.test.js +++ b/sites/tataplay.com/tataplay.com.test.js @@ -1,4 +1,6 @@ const { parser, url, channels } = require('./tataplay.com.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') @@ -9,32 +11,13 @@ const date = dayjs.utc('2025-06-09', 'YYYY-MM-DD').startOf('d') const channel = { site_id: '1001' } it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://tm.tapi.videoready.tv/content-detail/pub/api/v2/channels/schedule?date=09-06-2025') + expect(url({ channel, date })).toBe( + 'https://tm.tapi.videoready.tv/content-detail/pub/api/v2/channels/schedule?date=09-06-2025' + ) }) it('can parse response', () => { - const content = JSON.stringify({ - data: { - epg: [ - { - title: 'Yeh Rishta Kya Kehlata Hai', - startTime: '2025-06-09T18:00:00.000Z', - endTime: '2025-06-09T18:30:00.000Z', - desc: 'The story of the Rajshri family and their journey through life.', - category: 'Drama', - boxCoverImage: 'https://img.tataplay.com/thumbnails/1001/yeh-rishta.jpg' - }, - { - title: 'Anupamaa', - startTime: '2025-06-09T18:30:00.000Z', - endTime: '2025-06-09T19:00:00.000Z', - desc: 'The story of Anupamaa, a housewife who rediscovers herself.', - category: 'Drama', - boxCoverImage: 'https://img.tataplay.com/thumbnails/1001/anupamaa.jpg' - } - ] - } - }) + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const results = parser({ content, date }) @@ -103,4 +86,4 @@ it('can parse channel list', async () => { lang: 'en', icon: 'https://img.tataplay.com/channels/1002/logo.png' }) -}) \ No newline at end of file +}) diff --git a/sites/telebilbao.es/telebilbao.es.config.js b/sites/telebilbao.es/telebilbao.es.config.js index 8355ce3f..81d8b9e4 100644 --- a/sites/telebilbao.es/telebilbao.es.config.js +++ b/sites/telebilbao.es/telebilbao.es.config.js @@ -1,70 +1,70 @@ -const dayjs = require('dayjs') -const cheerio = require('cheerio') -const table2array = require('table2array') -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) - -require('dayjs/locale/es') - -module.exports = { - site: 'telebilbao.es', - days: 1, - url: 'https://www.telebilbao.es/programacion-2/', - request: { - cache: { - ttl: 24 * 60 * 60 * 1000 // 1 day - } - }, - parser({ content, date }) { - let programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - - programs.push({ - title: item.title, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item, date) { - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${item.time}`, 'YYYY-MM-DD HH:mm', 'Europe/Madrid') -} - -function parseItems(content, date) { - const $ = cheerio.load(content) - const tableHtml = $('table.programacion').html() - let tableArray = table2array(`${tableHtml}
    `) - const day = date.locale('es').format('dddd\nD MMMM').toUpperCase() - if (!tableArray[0]) return [] - const indexOfColumn = tableArray[0].indexOf(day) - tableArray.pop() - const items = [] - tableArray.forEach(row => { - items.push({ - time: row[0], - title: row[indexOfColumn] - }) - }) - - return items.filter(i => Boolean(i.time)) -} +const dayjs = require('dayjs') +const cheerio = require('cheerio') +const table2array = require('table2array') +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) + +require('dayjs/locale/es') + +module.exports = { + site: 'telebilbao.es', + days: 1, + url: 'https://www.telebilbao.es/programacion-2/', + request: { + cache: { + ttl: 24 * 60 * 60 * 1000 // 1 day + } + }, + parser({ content, date }) { + let programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + + programs.push({ + title: item.title, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item, date) { + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${item.time}`, 'YYYY-MM-DD HH:mm', 'Europe/Madrid') +} + +function parseItems(content, date) { + const $ = cheerio.load(content) + const tableHtml = $('table.programacion').html() + let tableArray = table2array(`${tableHtml}
    `) + const day = date.locale('es').format('dddd\nD MMMM').toUpperCase() + if (!tableArray[0]) return [] + const indexOfColumn = tableArray[0].indexOf(day) + tableArray.pop() + const items = [] + tableArray.forEach(row => { + items.push({ + time: row[0], + title: row[indexOfColumn] + }) + }) + + return items.filter(i => Boolean(i.time)) +} diff --git a/sites/telebilbao.es/telebilbao.es.test.js b/sites/telebilbao.es/telebilbao.es.test.js index 6450fb7e..e00f4abb 100644 --- a/sites/telebilbao.es/telebilbao.es.test.js +++ b/sites/telebilbao.es/telebilbao.es.test.js @@ -1,44 +1,44 @@ -const { parser, url } = require('./telebilbao.es.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-16', 'YYYY-MM-DD').startOf('d') - -it('can generate valid url', () => { - expect(url).toBe('https://www.telebilbao.es/programacion-2/') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(50) - expect(results[0]).toMatchObject({ - start: '2025-01-16T06:00:00.000Z', - stop: '2025-01-16T06:30:00.000Z', - title: 'BAI HORIXE' - }) - expect(results[49]).toMatchObject({ - start: '2025-01-17T07:30:00.000Z', - stop: '2025-01-17T08:00:00.000Z', - title: 'LA KAPITAL' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - }) - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./telebilbao.es.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-16', 'YYYY-MM-DD').startOf('d') + +it('can generate valid url', () => { + expect(url).toBe('https://www.telebilbao.es/programacion-2/') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(50) + expect(results[0]).toMatchObject({ + start: '2025-01-16T06:00:00.000Z', + stop: '2025-01-16T06:30:00.000Z', + title: 'BAI HORIXE' + }) + expect(results[49]).toMatchObject({ + start: '2025-01-17T07:30:00.000Z', + stop: '2025-01-17T08:00:00.000Z', + title: 'LA KAPITAL' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + }) + expect(results).toMatchObject([]) +}) diff --git a/sites/teleboy.ch/teleboy.ch.config.js b/sites/teleboy.ch/teleboy.ch.config.js index 1e96375b..2c52dfd0 100644 --- a/sites/teleboy.ch/teleboy.ch.config.js +++ b/sites/teleboy.ch/teleboy.ch.config.js @@ -1,75 +1,75 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const API_KEY = 'e899f715940a209148f834702fc7f340b6b0496b62120b3ed9c9b3ec4d7dca00' - -module.exports = { - site: 'teleboy.ch', - url({ channel, date }) { - const begin = date.format('YYYY-MM-DD HH:mm:ss') - const end = date.add(1, 'd').format('YYYY-MM-DD HH:mm:ss') - - return `https://api.teleboy.ch/epg/broadcasts?begin=${begin}&end=${end}&expand=flags,primary_image&station=${channel.site_id}` - }, - request: { - headers: { - 'x-teleboy-apikey': API_KEY - } - }, - parser({ content }) { - const items = parseItems(content) - - return items.map(item => { - return { - start: dayjs(item.begin), - stop: dayjs(item.end), - title: item.title, - subtitle: item.subtitle || null, - description: item.short_description || null, - date: item.year ? item.year.toString() : null, - season: item.serie_season || null, - episode: item.serie_episode || null, - starRatings: parseRating(item), - image: parseImage(item) - } - }) - }, - async channels() { - const data = await axios - .get('https://api.teleboy.ch/epg/stations', module.exports.request) - .then(r => r.data) - .catch(console.error) - - return data.data.items.map(channel => ({ - lang: channel.language, - name: channel.name, - site_id: channel.id - })) - } -} - -function parseImage(item) { - if (!item?.primary_image?.base_path || !item?.primary_image?.hash) return null - - return `${item.primary_image.base_path}teleboyteaser6/${item.primary_image.hash}.jpg` -} - -function parseRating(item) { - if (!item.imdb_rating) return null - - return { - system: 'IMDb', - value: item.imdb_rating - } -} - -function parseItems(content) { - try { - const data = JSON.parse(content) - if (!data?.data?.items || !Array.isArray(data.data.items)) return [] - - return data.data.items - } catch { - return [] - } -} +const axios = require('axios') +const dayjs = require('dayjs') + +const API_KEY = 'e899f715940a209148f834702fc7f340b6b0496b62120b3ed9c9b3ec4d7dca00' + +module.exports = { + site: 'teleboy.ch', + url({ channel, date }) { + const begin = date.format('YYYY-MM-DD HH:mm:ss') + const end = date.add(1, 'd').format('YYYY-MM-DD HH:mm:ss') + + return `https://api.teleboy.ch/epg/broadcasts?begin=${begin}&end=${end}&expand=flags,primary_image&station=${channel.site_id}` + }, + request: { + headers: { + 'x-teleboy-apikey': API_KEY + } + }, + parser({ content }) { + const items = parseItems(content) + + return items.map(item => { + return { + start: dayjs(item.begin), + stop: dayjs(item.end), + title: item.title, + subtitle: item.subtitle || null, + description: item.short_description || null, + date: item.year ? item.year.toString() : null, + season: item.serie_season || null, + episode: item.serie_episode || null, + starRatings: parseRating(item), + image: parseImage(item) + } + }) + }, + async channels() { + const data = await axios + .get('https://api.teleboy.ch/epg/stations', module.exports.request) + .then(r => r.data) + .catch(console.error) + + return data.data.items.map(channel => ({ + lang: channel.language, + name: channel.name, + site_id: channel.id + })) + } +} + +function parseImage(item) { + if (!item?.primary_image?.base_path || !item?.primary_image?.hash) return null + + return `${item.primary_image.base_path}teleboyteaser6/${item.primary_image.hash}.jpg` +} + +function parseRating(item) { + if (!item.imdb_rating) return null + + return { + system: 'IMDb', + value: item.imdb_rating + } +} + +function parseItems(content) { + try { + const data = JSON.parse(content) + if (!data?.data?.items || !Array.isArray(data.data.items)) return [] + + return data.data.items + } catch { + return [] + } +} diff --git a/sites/teleboy.ch/teleboy.ch.test.js b/sites/teleboy.ch/teleboy.ch.test.js index cfa55518..c6fb93db 100644 --- a/sites/teleboy.ch/teleboy.ch.test.js +++ b/sites/teleboy.ch/teleboy.ch.test.js @@ -1,62 +1,62 @@ -const { parser, url, request } = require('./teleboy.ch.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-26', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: '303', xmltv_id: 'SRF1.ch' } - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://api.teleboy.ch/epg/broadcasts?begin=2025-01-26 00:00:00&end=2025-01-27 00:00:00&expand=flags,primary_image&station=303' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'x-teleboy-apikey': 'e899f715940a209148f834702fc7f340b6b0496b62120b3ed9c9b3ec4d7dca00' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - - let results = parser({ content }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - - return p - }) - - expect(results.length).toBe(35) - expect(results[0]).toMatchObject({ - title: 'Der Staatsanwalt', - description: - 'Der Tod eines beliebten Wasserretters konfrontiert Oberstaatsanwalt Bernd Reuther, Hauptkommissarin Kerstin Klar und Oberkommissar Max Fischer mit einem undurchsichtigen Geflecht aus Lügen.', - subtitle: 'Tod eines Helden', - episode: 6, - season: 16, - date: '2021', - image: - 'https://media.teleboy.ch/media/teleboyteaser6/bd01aed53c7a37399ae034c2a1a2cc8aa31943f2.jpg', - starRatings: { - system: 'IMDb', - value: 6 - }, - start: '2025-01-25T22:45:00.000Z', - stop: '2025-01-25T23:50:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - }) - - expect(results).toMatchObject([]) -}) +const { parser, url, request } = require('./teleboy.ch.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-26', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: '303', xmltv_id: 'SRF1.ch' } + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://api.teleboy.ch/epg/broadcasts?begin=2025-01-26 00:00:00&end=2025-01-27 00:00:00&expand=flags,primary_image&station=303' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'x-teleboy-apikey': 'e899f715940a209148f834702fc7f340b6b0496b62120b3ed9c9b3ec4d7dca00' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + + let results = parser({ content }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + + return p + }) + + expect(results.length).toBe(35) + expect(results[0]).toMatchObject({ + title: 'Der Staatsanwalt', + description: + 'Der Tod eines beliebten Wasserretters konfrontiert Oberstaatsanwalt Bernd Reuther, Hauptkommissarin Kerstin Klar und Oberkommissar Max Fischer mit einem undurchsichtigen Geflecht aus Lügen.', + subtitle: 'Tod eines Helden', + episode: 6, + season: 16, + date: '2021', + image: + 'https://media.teleboy.ch/media/teleboyteaser6/bd01aed53c7a37399ae034c2a1a2cc8aa31943f2.jpg', + starRatings: { + system: 'IMDb', + value: 6 + }, + start: '2025-01-25T22:45:00.000Z', + stop: '2025-01-25T23:50:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/telenet.tv/telenet.tv.config.js b/sites/telenet.tv/telenet.tv.config.js index 7f286001..b5329d4b 100644 --- a/sites/telenet.tv/telenet.tv.config.js +++ b/sites/telenet.tv/telenet.tv.config.js @@ -1,138 +1,138 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const API_STATIC_ENDPOINT = 'https://static.spark.telenet.tv/eng/web/epg-service-lite/be' -const API_PROD_ENDPOINT = 'https://spark-prod-be.gnp.cloud.telenet.tv/eng/web/linear-service/v2' -const API_IMAGE_ENDPOINT = 'https://staticqbr-prod-be.gnp.cloud.telenet.tv/image-service' - -module.exports = { - site: 'telenet.tv', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: function ({ date, channel }) { - return `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date.format('YYYYMMDDHHmmss')}` - }, - async parser({ content, channel, date }) { - let programs = [] - let items = parseItems(content, channel) - if (!items.length) return programs - const promises = [ - axios.get( - `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date - .add(6, 'h') - .format('YYYYMMDDHHmmss')}`, - { - responseType: 'arraybuffer' - } - ), - axios.get( - `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date - .add(12, 'h') - .format('YYYYMMDDHHmmss')}`, - { - responseType: 'arraybuffer' - } - ), - axios.get( - `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date - .add(18, 'h') - .format('YYYYMMDDHHmmss')}`, - { - responseType: 'arraybuffer' - } - ) - ] - - await Promise.allSettled(promises) - .then(results => { - results.forEach(r => { - if (r.status === 'fulfilled') { - const parsed = parseItems(r.value.data, channel) - - items = items.concat(parsed) - } - }) - }) - .catch(console.error) - - for (let item of items) { - const detail = await loadProgramDetails(item, channel) - programs.push({ - title: item.title, - icon: parseIcon(item), - description: detail.longDescription, - category: detail.genres, - actors: detail.actors, - season: parseSeason(detail), - episode: parseEpisode(detail), - start: parseStart(item), - stop: parseStop(item) - }) - } - - return programs - }, - async channels() { - const data = await axios - .get(`${API_PROD_ENDPOINT}/channels?cityId=28001&language=en&productClass=Orion-DASH`) - .then(r => r.data) - .catch(console.log) - - return data.map(item => { - return { - lang: 'nl', - site_id: item.id, - name: item.name - } - }) - } -} - -async function loadProgramDetails(item, channel) { - if (!item.id) return {} - const url = `${API_PROD_ENDPOINT}/replayEvent/${item.id}?returnLinearContent=true&language=${channel.lang}` - const data = await axios - .get(url) - .then(r => r.data) - .catch(console.log) - - return data || {} -} - -function parseStart(item) { - return dayjs.unix(item.startTime) -} - -function parseStop(item) { - return dayjs.unix(item.endTime) -} - -function parseItems(content, channel) { - if (!content) return [] - const data = JSON.parse(content) - if (!data || !Array.isArray(data.entries)) return [] - const channelData = data.entries.find(e => e.channelId === channel.site_id) - if (!channelData) return [] - - return Array.isArray(channelData.events) ? channelData.events : [] -} - -function parseSeason(detail) { - if (!detail.seasonNumber) return null - if (String(detail.seasonNumber).length > 2) return null - return detail.seasonNumber -} - -function parseEpisode(detail) { - if (!detail.episodeNumber) return null - if (String(detail.episodeNumber).length > 3) return null - return detail.episodeNumber -} - -function parseIcon(item) { - return `${API_IMAGE_ENDPOINT}/intent/${item.id}/posterTile` -} +const axios = require('axios') +const dayjs = require('dayjs') + +const API_STATIC_ENDPOINT = 'https://static.spark.telenet.tv/eng/web/epg-service-lite/be' +const API_PROD_ENDPOINT = 'https://spark-prod-be.gnp.cloud.telenet.tv/eng/web/linear-service/v2' +const API_IMAGE_ENDPOINT = 'https://staticqbr-prod-be.gnp.cloud.telenet.tv/image-service' + +module.exports = { + site: 'telenet.tv', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: function ({ date, channel }) { + return `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date.format('YYYYMMDDHHmmss')}` + }, + async parser({ content, channel, date }) { + let programs = [] + let items = parseItems(content, channel) + if (!items.length) return programs + const promises = [ + axios.get( + `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date + .add(6, 'h') + .format('YYYYMMDDHHmmss')}`, + { + responseType: 'arraybuffer' + } + ), + axios.get( + `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date + .add(12, 'h') + .format('YYYYMMDDHHmmss')}`, + { + responseType: 'arraybuffer' + } + ), + axios.get( + `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date + .add(18, 'h') + .format('YYYYMMDDHHmmss')}`, + { + responseType: 'arraybuffer' + } + ) + ] + + await Promise.allSettled(promises) + .then(results => { + results.forEach(r => { + if (r.status === 'fulfilled') { + const parsed = parseItems(r.value.data, channel) + + items = items.concat(parsed) + } + }) + }) + .catch(console.error) + + for (let item of items) { + const detail = await loadProgramDetails(item, channel) + programs.push({ + title: item.title, + icon: parseIcon(item), + description: detail.longDescription, + category: detail.genres, + actors: detail.actors, + season: parseSeason(detail), + episode: parseEpisode(detail), + start: parseStart(item), + stop: parseStop(item) + }) + } + + return programs + }, + async channels() { + const data = await axios + .get(`${API_PROD_ENDPOINT}/channels?cityId=28001&language=en&productClass=Orion-DASH`) + .then(r => r.data) + .catch(console.log) + + return data.map(item => { + return { + lang: 'nl', + site_id: item.id, + name: item.name + } + }) + } +} + +async function loadProgramDetails(item, channel) { + if (!item.id) return {} + const url = `${API_PROD_ENDPOINT}/replayEvent/${item.id}?returnLinearContent=true&language=${channel.lang}` + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + + return data || {} +} + +function parseStart(item) { + return dayjs.unix(item.startTime) +} + +function parseStop(item) { + return dayjs.unix(item.endTime) +} + +function parseItems(content, channel) { + if (!content) return [] + const data = JSON.parse(content) + if (!data || !Array.isArray(data.entries)) return [] + const channelData = data.entries.find(e => e.channelId === channel.site_id) + if (!channelData) return [] + + return Array.isArray(channelData.events) ? channelData.events : [] +} + +function parseSeason(detail) { + if (!detail.seasonNumber) return null + if (String(detail.seasonNumber).length > 2) return null + return detail.seasonNumber +} + +function parseEpisode(detail) { + if (!detail.episodeNumber) return null + if (String(detail.episodeNumber).length > 3) return null + return detail.episodeNumber +} + +function parseIcon(item) { + return `${API_IMAGE_ENDPOINT}/intent/${item.id}/posterTile` +} diff --git a/sites/telenet.tv/telenet.tv.test.js b/sites/telenet.tv/telenet.tv.test.js index 28baeff8..bf7fd0c3 100644 --- a/sites/telenet.tv/telenet.tv.test.js +++ b/sites/telenet.tv/telenet.tv.test.js @@ -1,89 +1,89 @@ -const { parser, url } = require('./telenet.tv.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) - -const API_STATIC_ENDPOINT = 'https://static.spark.telenet.tv/eng/web/epg-service-lite/be' -const API_PROD_ENDPOINT = 'https://spark-prod-be.gnp.cloud.telenet.tv/eng/web/linear-service/v2' - -jest.mock('axios') - -const date = dayjs.utc('2022-10-30', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'outtv', - xmltv_id: 'OutTV.nl', - lang: 'nl' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe(`${API_STATIC_ENDPOINT}/nl/events/segments/20221030000000`) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0000.json')) - - axios.get.mockImplementation(url => { - if (url === `${API_STATIC_ENDPOINT}/nl/events/segments/20221030060000`) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_0600.json')) - }) - } else if (url === `${API_STATIC_ENDPOINT}/nl/events/segments/20221030120000`) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1200.json')) - }) - } else if (url === `${API_STATIC_ENDPOINT}/nl/events/segments/20221030180000`) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1800.json')) - }) - } else if ( - url === - `${API_PROD_ENDPOINT}/replayEvent/crid:~~2F~~2Fgn.tv~~2F2459095~~2FEP036477800004,imi:0a2f4207b03c16c70b7fb3be8e07881aafe44106?returnLinearContent=true&language=nl` - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-29T23:56:00.000Z', - stop: '2022-10-30T01:44:00.000Z', - title: 'Queer as Folk USA', - icon: 'https://staticqbr-prod-be.gnp.cloud.telenet.tv/image-service/intent/crid:~~2F~~2Fgn.tv~~2F2459095~~2FEP036477800004,imi:0a2f4207b03c16c70b7fb3be8e07881aafe44106/posterTile', - description: - "Justin belandt in de gevangenis, Brian en Brandon banen zich een weg door de lijst, Ben treurt, Melanie en Lindsay proberen een interne scheiding en Emmett's stalker onthult zichzelf.", - category: ['Dramaserie', 'LHBTI'], - actors: [ - 'Gale Harold', - 'Hal Sparks', - 'Randy Harrison', - 'Peter Paige', - 'Scott Lowell', - 'Thea Gill', - 'Michelle Clunie', - 'Sharon Gless' - ], - season: 5, - episode: 8 - }) -}) - -it('can handle empty guide', async () => { - let results = await parser({ content: '', channel, date }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./telenet.tv.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) + +const API_STATIC_ENDPOINT = 'https://static.spark.telenet.tv/eng/web/epg-service-lite/be' +const API_PROD_ENDPOINT = 'https://spark-prod-be.gnp.cloud.telenet.tv/eng/web/linear-service/v2' + +jest.mock('axios') + +const date = dayjs.utc('2022-10-30', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'outtv', + xmltv_id: 'OutTV.nl', + lang: 'nl' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe(`${API_STATIC_ENDPOINT}/nl/events/segments/20221030000000`) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0000.json')) + + axios.get.mockImplementation(url => { + if (url === `${API_STATIC_ENDPOINT}/nl/events/segments/20221030060000`) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_0600.json')) + }) + } else if (url === `${API_STATIC_ENDPOINT}/nl/events/segments/20221030120000`) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1200.json')) + }) + } else if (url === `${API_STATIC_ENDPOINT}/nl/events/segments/20221030180000`) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1800.json')) + }) + } else if ( + url === + `${API_PROD_ENDPOINT}/replayEvent/crid:~~2F~~2Fgn.tv~~2F2459095~~2FEP036477800004,imi:0a2f4207b03c16c70b7fb3be8e07881aafe44106?returnLinearContent=true&language=nl` + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-29T23:56:00.000Z', + stop: '2022-10-30T01:44:00.000Z', + title: 'Queer as Folk USA', + icon: 'https://staticqbr-prod-be.gnp.cloud.telenet.tv/image-service/intent/crid:~~2F~~2Fgn.tv~~2F2459095~~2FEP036477800004,imi:0a2f4207b03c16c70b7fb3be8e07881aafe44106/posterTile', + description: + "Justin belandt in de gevangenis, Brian en Brandon banen zich een weg door de lijst, Ben treurt, Melanie en Lindsay proberen een interne scheiding en Emmett's stalker onthult zichzelf.", + category: ['Dramaserie', 'LHBTI'], + actors: [ + 'Gale Harold', + 'Hal Sparks', + 'Randy Harrison', + 'Peter Paige', + 'Scott Lowell', + 'Thea Gill', + 'Michelle Clunie', + 'Sharon Gless' + ], + season: 5, + episode: 8 + }) +}) + +it('can handle empty guide', async () => { + let results = await parser({ content: '', channel, date }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/teliatv.ee/__data__/content.json b/sites/teliatv.ee/__data__/content.json new file mode 100644 index 00000000..b65aebbf --- /dev/null +++ b/sites/teliatv.ee/__data__/content.json @@ -0,0 +1 @@ +{"categoryItems":{"1":[{"id":136227,"type":"epgSeries","name":"Inimjaht","originalName":"Manhunt","price":null,"owner":"ETV","ownerId":1,"images":{"webGuideItemLarge":"/resized/ri93Qj4OLXXvg7QAsUOcKMnIb3g=/570x330/filters:format(jpeg)/inet-static.mw.elion.ee/epg_images/9/b/17e48b3966e65c02.jpg"},"packetIds":[30,34,38,129,130,162,191,242,243,244,447,483,484,485,486],"related":{"programmeIds":[27224371]}}]},"relations":{"programmes":{"27224371":{"id":27224371,"startAt":"2021-11-20T00:05:00+02:00","endAt":"2021-11-20T00:55:00+02:00","publicTo":"2021-12-04T02:05:00+02:00","status":"default","channelId":1,"broadcastId":78248901,"hasMarkers":false,"catchup":false}}}} \ No newline at end of file diff --git a/sites/teliatv.ee/__data__/no_content.json b/sites/teliatv.ee/__data__/no_content.json new file mode 100644 index 00000000..8ef893c4 --- /dev/null +++ b/sites/teliatv.ee/__data__/no_content.json @@ -0,0 +1 @@ +{"categoryItems":{},"relations":{}} \ No newline at end of file diff --git a/sites/teliatv.ee/teliatv.ee.config.js b/sites/teliatv.ee/teliatv.ee.config.js index b6393c9b..2d7253db 100644 --- a/sites/teliatv.ee/teliatv.ee.config.js +++ b/sites/teliatv.ee/teliatv.ee.config.js @@ -1,67 +1,67 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'teliatv.ee', - days: 2, - url({ date, channel }) { - const [lang, channelId] = channel.site_id.split('#') - return `https://api.teliatv.ee/dtv-api/3.2/${lang}/epg/guide?channelIds=${channelId}&relations=programmes&images=webGuideItemLarge&startAt=${date - .add(1, 'd') - .format('YYYY-MM-DDTHH:mm')}&startAtOp=lte&endAt=${date.format( - 'YYYY-MM-DDTHH:mm' - )}&endAtOp=gt` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.name, - image: parseImage(item), - start: dayjs(item.startAt), - stop: dayjs(item.endAt) - }) - }) - - return programs - }, - async channels({ lang }) { - const data = await axios - .get(`https://api.teliatv.ee/dtv-api/3.0/${lang}/channel-lists?listClass=tv&ui=tv-web`) - .then(r => r.data) - .catch(console.log) - - return Object.values(data.channels).map(item => { - return { - lang, - site_id: `${lang}#${item.id}`, - name: item.title - } - }) - } -} - -function parseImage(item) { - return item.images.webGuideItemLarge - ? `https://inet-static.mw.elion.ee${item.images.webGuideItemLarge}` - : null -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !data.relations || !data.categoryItems) return [] - const [, channelId] = channel.site_id.split('#') - const items = data.categoryItems[channelId] || [] - - return items - .map(i => { - const programmeId = i.related.programmeIds[0] - if (!programmeId) return null - const progData = data.relations.programmes[programmeId] - if (!progData) return null - - return { ...i, ...progData } - }) - .filter(i => i) -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'teliatv.ee', + days: 2, + url({ date, channel }) { + const [lang, channelId] = channel.site_id.split('#') + return `https://api.teliatv.ee/dtv-api/3.2/${lang}/epg/guide?channelIds=${channelId}&relations=programmes&images=webGuideItemLarge&startAt=${date + .add(1, 'd') + .format('YYYY-MM-DDTHH:mm')}&startAtOp=lte&endAt=${date.format( + 'YYYY-MM-DDTHH:mm' + )}&endAtOp=gt` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.name, + image: parseImage(item), + start: dayjs(item.startAt), + stop: dayjs(item.endAt) + }) + }) + + return programs + }, + async channels({ lang }) { + const data = await axios + .get(`https://api.teliatv.ee/dtv-api/3.0/${lang}/channel-lists?listClass=tv&ui=tv-web`) + .then(r => r.data) + .catch(console.log) + + return Object.values(data.channels).map(item => { + return { + lang, + site_id: `${lang}#${item.id}`, + name: item.title + } + }) + } +} + +function parseImage(item) { + return item.images.webGuideItemLarge + ? `https://inet-static.mw.elion.ee${item.images.webGuideItemLarge}` + : null +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !data.relations || !data.categoryItems) return [] + const [, channelId] = channel.site_id.split('#') + const items = data.categoryItems[channelId] || [] + + return items + .map(i => { + const programmeId = i.related.programmeIds[0] + if (!programmeId) return null + const progData = data.relations.programmes[programmeId] + if (!progData) return null + + return { ...i, ...progData } + }) + .filter(i => i) +} diff --git a/sites/teliatv.ee/teliatv.ee.test.js b/sites/teliatv.ee/teliatv.ee.test.js index 2643046c..de2b8b63 100644 --- a/sites/teliatv.ee/teliatv.ee.test.js +++ b/sites/teliatv.ee/teliatv.ee.test.js @@ -1,59 +1,60 @@ -const { parser, url } = require('./teliatv.ee.config.js') -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('2021-11-20', 'YYYY-MM-DD').startOf('d') -const channel = { - lang: 'et', - site_id: 'et#1', - xmltv_id: 'ETV.ee' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://api.teliatv.ee/dtv-api/3.2/et/epg/guide?channelIds=1&relations=programmes&images=webGuideItemLarge&startAt=2021-11-21T00:00&startAtOp=lte&endAt=2021-11-20T00:00&endAtOp=gt' - ) -}) - -it('can generate valid url with different language', () => { - const ruChannel = { - lang: 'ru', - site_id: 'ru#1', - xmltv_id: 'ETV.ee' - } - expect(url({ date, channel: ruChannel })).toBe( - 'https://api.teliatv.ee/dtv-api/3.2/ru/epg/guide?channelIds=1&relations=programmes&images=webGuideItemLarge&startAt=2021-11-21T00:00&startAtOp=lte&endAt=2021-11-20T00:00&endAtOp=gt' - ) -}) - -it('can parse response', () => { - const content = - '{"categoryItems":{"1":[{"id":136227,"type":"epgSeries","name":"Inimjaht","originalName":"Manhunt","price":null,"owner":"ETV","ownerId":1,"images":{"webGuideItemLarge":"/resized/ri93Qj4OLXXvg7QAsUOcKMnIb3g=/570x330/filters:format(jpeg)/inet-static.mw.elion.ee/epg_images/9/b/17e48b3966e65c02.jpg"},"packetIds":[30,34,38,129,130,162,191,242,243,244,447,483,484,485,486],"related":{"programmeIds":[27224371]}}]},"relations":{"programmes":{"27224371":{"id":27224371,"startAt":"2021-11-20T00:05:00+02:00","endAt":"2021-11-20T00:55:00+02:00","publicTo":"2021-12-04T02:05:00+02:00","status":"default","channelId":1,"broadcastId":78248901,"hasMarkers":false,"catchup":false}}}}' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-19T22:05:00.000Z', - stop: '2021-11-19T22:55:00.000Z', - title: 'Inimjaht', - image: - 'https://inet-static.mw.elion.ee/resized/ri93Qj4OLXXvg7QAsUOcKMnIb3g=/570x330/filters:format(jpeg)/inet-static.mw.elion.ee/epg_images/9/b/17e48b3966e65c02.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"categoryItems":{},"relations":{}}' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./teliatv.ee.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('2021-11-20', 'YYYY-MM-DD').startOf('d') +const channel = { + lang: 'et', + site_id: 'et#1', + xmltv_id: 'ETV.ee' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://api.teliatv.ee/dtv-api/3.2/et/epg/guide?channelIds=1&relations=programmes&images=webGuideItemLarge&startAt=2021-11-21T00:00&startAtOp=lte&endAt=2021-11-20T00:00&endAtOp=gt' + ) +}) + +it('can generate valid url with different language', () => { + const ruChannel = { + lang: 'ru', + site_id: 'ru#1', + xmltv_id: 'ETV.ee' + } + expect(url({ date, channel: ruChannel })).toBe( + 'https://api.teliatv.ee/dtv-api/3.2/ru/epg/guide?channelIds=1&relations=programmes&images=webGuideItemLarge&startAt=2021-11-21T00:00&startAtOp=lte&endAt=2021-11-20T00:00&endAtOp=gt' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-19T22:05:00.000Z', + stop: '2021-11-19T22:55:00.000Z', + title: 'Inimjaht', + image: + 'https://inet-static.mw.elion.ee/resized/ri93Qj4OLXXvg7QAsUOcKMnIb3g=/570x330/filters:format(jpeg)/inet-static.mw.elion.ee/epg_images/9/b/17e48b3966e65c02.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/telkussa.fi/telkussa.fi.config.js b/sites/telkussa.fi/telkussa.fi.config.js index d9eeb6c2..f50d043a 100644 --- a/sites/telkussa.fi/telkussa.fi.config.js +++ b/sites/telkussa.fi/telkussa.fi.config.js @@ -1,45 +1,45 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'telkussa.fi', - days: 2, - url: function ({ date, channel }) { - return `https://telkussa.fi/API/Channel/${channel.site_id}/${date.format('YYYYMMDD')}` - }, - parser: function ({ content }) { - const programs = [] - const items = JSON.parse(content) - if (!items.length) return programs - - items.forEach(item => { - if (item.name && item.start && item.stop) { - const start = dayjs.unix(parseInt(item.start) * 60) - const stop = dayjs.unix(parseInt(item.stop) * 60) - - programs.push({ - title: item.name, - description: item.description, - start, - stop - }) - } - }) - - return programs - }, - async channels() { - const axios = require('axios') - const data = await axios - .get('https://telkussa.fi/API/Channels') - .then(r => r.data) - .catch(console.log) - - return data.map(item => { - return { - lang: 'fi', - site_id: item.id, - name: item.name - } - }) - } -} +const dayjs = require('dayjs') + +module.exports = { + site: 'telkussa.fi', + days: 2, + url: function ({ date, channel }) { + return `https://telkussa.fi/API/Channel/${channel.site_id}/${date.format('YYYYMMDD')}` + }, + parser: function ({ content }) { + const programs = [] + const items = JSON.parse(content) + if (!items.length) return programs + + items.forEach(item => { + if (item.name && item.start && item.stop) { + const start = dayjs.unix(parseInt(item.start) * 60) + const stop = dayjs.unix(parseInt(item.stop) * 60) + + programs.push({ + title: item.name, + description: item.description, + start, + stop + }) + } + }) + + return programs + }, + async channels() { + const axios = require('axios') + const data = await axios + .get('https://telkussa.fi/API/Channels') + .then(r => r.data) + .catch(console.log) + + return data.map(item => { + return { + lang: 'fi', + site_id: item.id, + name: item.name + } + }) + } +} diff --git a/sites/telkussa.fi/telkussa.fi.test.js b/sites/telkussa.fi/telkussa.fi.test.js index c4dfd77e..48748b0c 100644 --- a/sites/telkussa.fi/telkussa.fi.test.js +++ b/sites/telkussa.fi/telkussa.fi.test.js @@ -1,50 +1,50 @@ -const { parser, url } = require('./telkussa.fi.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('2023-11-30', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '88', - xmltv_id: 'TV5.fi' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe('https://telkussa.fi/API/Channel/88/20231130') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-11-29T20:40:00.000Z', - stop: '2023-11-29T23:20:00.000Z', - title: 'The Suicide Squad: Suicide Mission', - description: - 'SUOMEN TV-ENSI-ILTA Tervetuloa helvettiin - toisin sanoen Belle Reven vankilaan, missä henki on höllemmessä kuin missään muualla koko Amerikanmaalla. Missä pidetään pahimpia superroistoja ja missä ollaan valmiita tekemään mitä vain, jotta pääsisi pois - jopa liittymään supersalaiseen, superhämärään ryhmään nimeltä Task Force X. Ja mikä on päivän itsetuhoinen tehtävä? Kerää kokoon joukko vankeja, mukaan lukien Bloodsport, Peacemaker, Captain Boomerang, Ratcatcher 2, Savant, King Shark, Blackguard, Javelin ja kaikkien lempisekopää Harley Quinn. Anna heille raskas aseistus ja pudota heidät (kirjaimellisesti) Corto Maltesen syrjäiselle, vihollisia kuhisevalle saarelle. Halki viidakon, joka vilisee sotaisia vastustajia ja sissijoukkoja, ryhmä taivaltaa kohti tuhoamistehtäväänsä. Matkalla heitä kurissa yrittää pitää vain eversti Rick Flag… sekä Amanda Wallerin tekniikkavelhot, jotka antavat jatkuvasti ohjeita korvanappeihin. Ja kuten aina, yksikin väärä liike tietää kuolemaa (tuli se sitten vastustajan, toverin tai Wallerin itsensä toimesta). Jos joku haluaa lyödä vetoa, fiksuinta lienee veikata heitä vastaan - kaikkia heitä. 132 min. Ohjaus: James Gunn. Pääosissa: Margot Robbie, Idris Elba, John Cena, Joel Kinnaman ja Jai Courtney. (The Suicide Squad, Toiminta, Yhdysvallat, 2021)' - }) - - expect(results[31]).toMatchObject({ - start: '2023-12-01T03:25:00.000Z', - stop: '2023-12-01T03:55:00.000Z', - title: 'Asunnon metsästäjät', - description: - 'Sarjassa sinkut, pariskunnat ja perheet etsivät uutta kotia asunnonvälittäjän avustuksella Yhdysvalloissa. .(House Hunters, Tosi-tv, Yhdysvallat, 2018) S148E02' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '[]' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./telkussa.fi.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('2023-11-30', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '88', + xmltv_id: 'TV5.fi' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe('https://telkussa.fi/API/Channel/88/20231130') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-11-29T20:40:00.000Z', + stop: '2023-11-29T23:20:00.000Z', + title: 'The Suicide Squad: Suicide Mission', + description: + 'SUOMEN TV-ENSI-ILTA Tervetuloa helvettiin - toisin sanoen Belle Reven vankilaan, missä henki on höllemmessä kuin missään muualla koko Amerikanmaalla. Missä pidetään pahimpia superroistoja ja missä ollaan valmiita tekemään mitä vain, jotta pääsisi pois - jopa liittymään supersalaiseen, superhämärään ryhmään nimeltä Task Force X. Ja mikä on päivän itsetuhoinen tehtävä? Kerää kokoon joukko vankeja, mukaan lukien Bloodsport, Peacemaker, Captain Boomerang, Ratcatcher 2, Savant, King Shark, Blackguard, Javelin ja kaikkien lempisekopää Harley Quinn. Anna heille raskas aseistus ja pudota heidät (kirjaimellisesti) Corto Maltesen syrjäiselle, vihollisia kuhisevalle saarelle. Halki viidakon, joka vilisee sotaisia vastustajia ja sissijoukkoja, ryhmä taivaltaa kohti tuhoamistehtäväänsä. Matkalla heitä kurissa yrittää pitää vain eversti Rick Flag… sekä Amanda Wallerin tekniikkavelhot, jotka antavat jatkuvasti ohjeita korvanappeihin. Ja kuten aina, yksikin väärä liike tietää kuolemaa (tuli se sitten vastustajan, toverin tai Wallerin itsensä toimesta). Jos joku haluaa lyödä vetoa, fiksuinta lienee veikata heitä vastaan - kaikkia heitä. 132 min. Ohjaus: James Gunn. Pääosissa: Margot Robbie, Idris Elba, John Cena, Joel Kinnaman ja Jai Courtney. (The Suicide Squad, Toiminta, Yhdysvallat, 2021)' + }) + + expect(results[31]).toMatchObject({ + start: '2023-12-01T03:25:00.000Z', + stop: '2023-12-01T03:55:00.000Z', + title: 'Asunnon metsästäjät', + description: + 'Sarjassa sinkut, pariskunnat ja perheet etsivät uutta kotia asunnonvälittäjän avustuksella Yhdysvalloissa. .(House Hunters, Tosi-tv, Yhdysvallat, 2018) S148E02' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '[]' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/telsu.fi/telsu.fi.config.js b/sites/telsu.fi/telsu.fi.config.js index 057cccd1..73f50928 100644 --- a/sites/telsu.fi/telsu.fi.config.js +++ b/sites/telsu.fi/telsu.fi.config.js @@ -1,98 +1,98 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'telsu.fi', - days: 2, - url: function ({ date, channel }) { - return `https://www.telsu.fi/${date.format('YYYYMMDD')}/${channel.site_id}` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - let stop = parseStop($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - } - prev.stop = start - if (stop.isBefore(prev.stop)) { - stop = stop.add(1, 'd') - } - } - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - image: parseImage($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const html = await axios - .get('https://www.telsu.fi/') - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(html) - const items = $('.ch').toArray() - return items.map(item => { - const name = $(item).find('a').attr('title') - const site_id = $(item).attr('rel') - - return { - lang: 'fi', - site_id, - name - } - }) - } -} - -function parseTitle($item) { - return $item('h1 > b').text().trim() -} - -function parseDescription($item) { - return $item('.t > div').clone().children().remove().end().text().trim() -} - -function parseImage($item) { - const imgSrc = $item('.t > div > div.ps > a > img').attr('src') - - return imgSrc ? `https://www.telsu.fi${imgSrc}` : null -} - -function parseStart($item, date) { - const subtitle = $item('.h > h2').clone().children().remove().end().text().trim() - const [, HH, mm] = subtitle.match(/(\d{2})\.(\d{2}) - (\d{2})\.(\d{2})$/) || [null, null, null] - if (!HH || !mm) return null - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Europe/Helsinki') -} - -function parseStop($item, date) { - const subtitle = $item('.h > h2').clone().children().remove().end().text().trim() - const [, HH, mm] = subtitle.match(/ - (\d{2})\.(\d{2})$/) || [null, null, null] - if (!HH || !mm) return null - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Europe/Helsinki') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#res > div.dets').toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'telsu.fi', + days: 2, + url: function ({ date, channel }) { + return `https://www.telsu.fi/${date.format('YYYYMMDD')}/${channel.site_id}` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + let stop = parseStop($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + } + prev.stop = start + if (stop.isBefore(prev.stop)) { + stop = stop.add(1, 'd') + } + } + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + image: parseImage($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const html = await axios + .get('https://www.telsu.fi/') + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(html) + const items = $('.ch').toArray() + return items.map(item => { + const name = $(item).find('a').attr('title') + const site_id = $(item).attr('rel') + + return { + lang: 'fi', + site_id, + name + } + }) + } +} + +function parseTitle($item) { + return $item('h1 > b').text().trim() +} + +function parseDescription($item) { + return $item('.t > div').clone().children().remove().end().text().trim() +} + +function parseImage($item) { + const imgSrc = $item('.t > div > div.ps > a > img').attr('src') + + return imgSrc ? `https://www.telsu.fi${imgSrc}` : null +} + +function parseStart($item, date) { + const subtitle = $item('.h > h2').clone().children().remove().end().text().trim() + const [, HH, mm] = subtitle.match(/(\d{2})\.(\d{2}) - (\d{2})\.(\d{2})$/) || [null, null, null] + if (!HH || !mm) return null + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Europe/Helsinki') +} + +function parseStop($item, date) { + const subtitle = $item('.h > h2').clone().children().remove().end().text().trim() + const [, HH, mm] = subtitle.match(/ - (\d{2})\.(\d{2})$/) || [null, null, null] + if (!HH || !mm) return null + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Europe/Helsinki') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('#res > div.dets').toArray() +} diff --git a/sites/telsu.fi/telsu.fi.test.js b/sites/telsu.fi/telsu.fi.test.js index 99119899..dc83e74c 100644 --- a/sites/telsu.fi/telsu.fi.test.js +++ b/sites/telsu.fi/telsu.fi.test.js @@ -1,44 +1,44 @@ -const { parser, url } = require('./telsu.fi.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('2022-10-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'yle1', - xmltv_id: 'YleTV1.fi' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe('https://www.telsu.fi/20221029/yle1') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-29T04:00:00.000Z', - stop: '2022-10-29T04:28:00.000Z', - title: 'Antiikkikaksintaistelu', - description: - 'Kausi 6, osa 5/12. Antiikkikaksintaistelu jatkuu Løkkenissä. Uusi taistelupari Rikke Fog ja Lasse Franck saavat kumpikin 10 000 kruunua ja viisi tuntia aikaa ostaa alueelta hyvää tavaraa halvalla.', - image: 'https://www.telsu.fi/s/antiikkikaksintaistelu_11713730.jpg' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')), - date - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./telsu.fi.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('2022-10-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'yle1', + xmltv_id: 'YleTV1.fi' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe('https://www.telsu.fi/20221029/yle1') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-29T04:00:00.000Z', + stop: '2022-10-29T04:28:00.000Z', + title: 'Antiikkikaksintaistelu', + description: + 'Kausi 6, osa 5/12. Antiikkikaksintaistelu jatkuu Løkkenissä. Uusi taistelupari Rikke Fog ja Lasse Franck saavat kumpikin 10 000 kruunua ja viisi tuntia aikaa ostaa alueelta hyvää tavaraa halvalla.', + image: 'https://www.telsu.fi/s/antiikkikaksintaistelu_11713730.jpg' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')), + date + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/thesportplus.com/thesportplus.com.config.js b/sites/thesportplus.com/thesportplus.com.config.js index dd48e5e2..c5f0a432 100644 --- a/sites/thesportplus.com/thesportplus.com.config.js +++ b/sites/thesportplus.com/thesportplus.com.config.js @@ -1,73 +1,73 @@ -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) - -const timezones = { - usa: 'America/New_York', - aus: 'Australia/Sydney', - euro: 'UTC' -} - -module.exports = { - site: 'thesportplus.com', - days: 2, - url({ channel, date }) { - return `https://www.thesportplus.com/schedule_${channel.site_id}.php?d=${date.format( - 'YYYY-MM-DD' - )}` - }, - parser({ content, date, channel }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date, channel) - if (!start) return - if (prev) { - if (start.isBefore(prev.start) && start.hour() < 12) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(1, 'h') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('h5:last').text().trim() -} - -function parseDescription($item) { - return $item('p').text().trim() -} - -function parseStart($item, date, channel) { - const timezone = timezones[channel.site_id] - const time = $item('h4').text().trim() - const dateString = `${date.format('YYYY-MM-DD')} ${time}` - - return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', timezone) -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.resume-item').toArray() -} +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) + +const timezones = { + usa: 'America/New_York', + aus: 'Australia/Sydney', + euro: 'UTC' +} + +module.exports = { + site: 'thesportplus.com', + days: 2, + url({ channel, date }) { + return `https://www.thesportplus.com/schedule_${channel.site_id}.php?d=${date.format( + 'YYYY-MM-DD' + )}` + }, + parser({ content, date, channel }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date, channel) + if (!start) return + if (prev) { + if (start.isBefore(prev.start) && start.hour() < 12) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(1, 'h') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('h5:last').text().trim() +} + +function parseDescription($item) { + return $item('p').text().trim() +} + +function parseStart($item, date, channel) { + const timezone = timezones[channel.site_id] + const time = $item('h4').text().trim() + const dateString = `${date.format('YYYY-MM-DD')} ${time}` + + return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', timezone) +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.resume-item').toArray() +} diff --git a/sites/thesportplus.com/thesportplus.com.test.js b/sites/thesportplus.com/thesportplus.com.test.js index ed431fda..96bae2a5 100644 --- a/sites/thesportplus.com/thesportplus.com.test.js +++ b/sites/thesportplus.com/thesportplus.com.test.js @@ -1,50 +1,50 @@ -const { parser, url } = require('./thesportplus.com.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-19', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'usa', - xmltv_id: 'SportPlusUSA.us' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://www.thesportplus.com/schedule_usa.php?d=2025-01-19') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(13) - expect(results[0]).toMatchObject({ - start: '2025-01-19T06:00:00.000Z', - stop: '2025-01-19T08:00:00.000Z', - title: 'ASTERAS vs ATROMITOS', - description: 'Super League Season 24-25 MD 4' - }) - expect(results[12]).toMatchObject({ - start: '2025-01-20T04:00:00.000Z', - stop: '2025-01-20T05:00:00.000Z', - title: 'SPORTSHOW', - description: 'Super League' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - channel, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - }) - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./thesportplus.com.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-19', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'usa', + xmltv_id: 'SportPlusUSA.us' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://www.thesportplus.com/schedule_usa.php?d=2025-01-19') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(13) + expect(results[0]).toMatchObject({ + start: '2025-01-19T06:00:00.000Z', + stop: '2025-01-19T08:00:00.000Z', + title: 'ASTERAS vs ATROMITOS', + description: 'Super League Season 24-25 MD 4' + }) + expect(results[12]).toMatchObject({ + start: '2025-01-20T04:00:00.000Z', + stop: '2025-01-20T05:00:00.000Z', + title: 'SPORTSHOW', + description: 'Super League' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + }) + expect(results).toMatchObject([]) +}) 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/tivie.id/tivie.id.test.js b/sites/tivie.id/tivie.id.test.js index dcc4e0ee..11e47c05 100644 --- a/sites/tivie.id/tivie.id.test.js +++ b/sites/tivie.id/tivie.id.test.js @@ -1,75 +1,75 @@ -const { parser, url } = require('./tivie.id.config') -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('2024-12-31').startOf('d') -const channel = { - site_id: 'axn', - xmltv_id: 'AXN.id', - lang: 'id' -} - -axios.get.mockImplementation(url => { - const urls = { - 'https://tivie.id/film/white-house-down-nwzDnwz9nAv6': 'program01.html', - 'https://tivie.id/program/hudson-rex-s6-e14-nwzDnwvBmQr9': 'program02.html' - } - let data = '' - if (urls[url] !== undefined) { - data = fs.readFileSync(path.join(__dirname, '__data__', urls[url])).toString() - } - return Promise.resolve({ data }) -}) - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://tivie.id/channel/axn/20241231') -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.html')) - const results = (await parser({ date, content, channel })).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(27) - expect(results[0]).toMatchObject({ - start: '2024-12-30T17:00:00.000Z', - stop: '2024-12-30T17:05:00.000Z', - title: 'White House Down', - description: - 'Saat melakukan tur di Gedung Putih bersama putrinya yang masih kecil, seorang perwira polisi beraksi untuk melindungi anaknya dan presiden dari sekelompok penjajah paramiliter bersenjata lengkap.', - image: - 'https://i0.wp.com/is3.cloudhost.id/tivie/poster/2023/09/65116c78791c2-1695640694.jpg?resize=480,270' - }) - expect(results[2]).toMatchObject({ - start: '2024-12-30T18:00:00.000Z', - stop: '2024-12-30T18:55:00.000Z', - title: 'Hudson & Rex S6, Ep. 14', - description: - 'Saat guru musik Jesse terbunuh di studio rekamannya, Charlie dan Rex menghubungkan kejahatan tersebut dengan pembunuhan yang tampaknya tak ada hubungannya.', - image: - 'https://i0.wp.com/is3.cloudhost.id/tivie/poster/2024/07/668b7ced47b25-1720417517.jpg?resize=480,270', - season: 6, - episode: 14 - }) -}) - -it('can handle empty guide', async () => { - const results = await parser({ - date, - channel, - content: '' - }) - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./tivie.id.config') +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('2024-12-31').startOf('d') +const channel = { + site_id: 'axn', + xmltv_id: 'AXN.id', + lang: 'id' +} + +axios.get.mockImplementation(url => { + const urls = { + 'https://tivie.id/film/white-house-down-nwzDnwz9nAv6': 'program01.html', + 'https://tivie.id/program/hudson-rex-s6-e14-nwzDnwvBmQr9': 'program02.html' + } + let data = '' + if (urls[url] !== undefined) { + data = fs.readFileSync(path.join(__dirname, '__data__', urls[url])).toString() + } + return Promise.resolve({ data }) +}) + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://tivie.id/channel/axn/20241231') +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.html')) + const results = (await parser({ date, content, channel })).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(27) + expect(results[0]).toMatchObject({ + start: '2024-12-30T17:00:00.000Z', + stop: '2024-12-30T17:05:00.000Z', + title: 'White House Down', + description: + 'Saat melakukan tur di Gedung Putih bersama putrinya yang masih kecil, seorang perwira polisi beraksi untuk melindungi anaknya dan presiden dari sekelompok penjajah paramiliter bersenjata lengkap.', + image: + 'https://i0.wp.com/is3.cloudhost.id/tivie/poster/2023/09/65116c78791c2-1695640694.jpg?resize=480,270' + }) + expect(results[2]).toMatchObject({ + start: '2024-12-30T18:00:00.000Z', + stop: '2024-12-30T18:55:00.000Z', + title: 'Hudson & Rex S6, Ep. 14', + description: + 'Saat guru musik Jesse terbunuh di studio rekamannya, Charlie dan Rex menghubungkan kejahatan tersebut dengan pembunuhan yang tampaknya tak ada hubungannya.', + image: + 'https://i0.wp.com/is3.cloudhost.id/tivie/poster/2024/07/668b7ced47b25-1720417517.jpg?resize=480,270', + season: 6, + episode: 14 + }) +}) + +it('can handle empty guide', async () => { + const results = await parser({ + date, + channel, + content: '' + }) + expect(results).toMatchObject([]) +}) diff --git a/sites/tivu.tv/tivu.tv.config.js b/sites/tivu.tv/tivu.tv.config.js index 7849be39..dfec635d 100644 --- a/sites/tivu.tv/tivu.tv.config.js +++ b/sites/tivu.tv/tivu.tv.config.js @@ -1,89 +1,89 @@ -const cheerio = require('cheerio') -const { DateTime } = require('luxon') - -module.exports = { - site: 'tivu.tv', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url({ date }) { - const diff = date.diff(DateTime.now().toUTC().startOf('day'), 'd') - - return `https://www.tivu.tv/epg_ajax_sat.aspx?d=${diff}` - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel, date) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (!start) return - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ minutes: 30 }) - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - const html = await axios - .get('https://www.tivu.tv/epg_ajax_sat.aspx?d=0') - .then(r => r.data) - .catch(console.log) - - let channels = [] - - const $ = cheerio.load(html) - $('.q').each((i, el) => { - const site_id = $(el).attr('id') - const name = $(el).find('a').first().data('channel') - - if (!name) return - - channels.push({ - lang: 'it', - site_id, - name - }) - }) - - return channels - } -} - -function parseTitle($item) { - const [title] = $item('a').html().split('
    ') - - return title -} - -function parseStart($item, date) { - const [, , time] = $item('a').html().split('
    ') - if (!time) return null - - return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { - zone: 'Europe/Rome' - }).toUTC() -} - -function parseItems(content, channel) { - if (!content) return [] - const $ = cheerio.load(content) - - return $(`.q[id="${channel.site_id}"] > .p`).toArray() -} +const cheerio = require('cheerio') +const { DateTime } = require('luxon') + +module.exports = { + site: 'tivu.tv', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url({ date }) { + const diff = date.diff(DateTime.now().toUTC().startOf('day'), 'd') + + return `https://www.tivu.tv/epg_ajax_sat.aspx?d=${diff}` + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel, date) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (!start) return + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ minutes: 30 }) + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const axios = require('axios') + const html = await axios + .get('https://www.tivu.tv/epg_ajax_sat.aspx?d=0') + .then(r => r.data) + .catch(console.log) + + let channels = [] + + const $ = cheerio.load(html) + $('.q').each((i, el) => { + const site_id = $(el).attr('id') + const name = $(el).find('a').first().data('channel') + + if (!name) return + + channels.push({ + lang: 'it', + site_id, + name + }) + }) + + return channels + } +} + +function parseTitle($item) { + const [title] = $item('a').html().split('
    ') + + return title +} + +function parseStart($item, date) { + const [, , time] = $item('a').html().split('
    ') + if (!time) return null + + return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { + zone: 'Europe/Rome' + }).toUTC() +} + +function parseItems(content, channel) { + if (!content) return [] + const $ = cheerio.load(content) + + return $(`.q[id="${channel.site_id}"] > .p`).toArray() +} diff --git a/sites/tivu.tv/tivu.tv.test.js b/sites/tivu.tv/tivu.tv.test.js index b243072a..2bc57af1 100644 --- a/sites/tivu.tv/tivu.tv.test.js +++ b/sites/tivu.tv/tivu.tv.test.js @@ -1,56 +1,56 @@ -const { parser, url } = require('./tivu.tv.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) - -jest.mock('axios') - -const channel = { - site_id: '62', - xmltv_id: 'Rai1HD.it' -} - -it('can generate valid url for today', () => { - const date = dayjs.utc().startOf('d') - expect(url({ date })).toBe('https://www.tivu.tv/epg_ajax_sat.aspx?d=0') -}) - -it('can generate valid url for tomorrow', () => { - const date = dayjs.utc().startOf('d').add(1, 'd') - expect(url({ date })).toBe('https://www.tivu.tv/epg_ajax_sat.aspx?d=1') -}) - -it('can parse response', () => { - const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - - let results = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-03T22:02:00.000Z', - stop: '2022-10-03T22:45:00.000Z', - title: 'Cose Nostre - La figlia del boss' - }) - - expect(results[43]).toMatchObject({ - start: '2022-10-05T04:58:00.000Z', - stop: '2022-10-05T05:28:00.000Z', - title: 'Tgunomattina - in collaborazione con day' - }) -}) - -it('can handle empty guide', () => { - const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - const result = parser({ content, channel, date }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./tivu.tv.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) + +jest.mock('axios') + +const channel = { + site_id: '62', + xmltv_id: 'Rai1HD.it' +} + +it('can generate valid url for today', () => { + const date = dayjs.utc().startOf('d') + expect(url({ date })).toBe('https://www.tivu.tv/epg_ajax_sat.aspx?d=0') +}) + +it('can generate valid url for tomorrow', () => { + const date = dayjs.utc().startOf('d').add(1, 'd') + expect(url({ date })).toBe('https://www.tivu.tv/epg_ajax_sat.aspx?d=1') +}) + +it('can parse response', () => { + const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + + let results = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-03T22:02:00.000Z', + stop: '2022-10-03T22:45:00.000Z', + title: 'Cose Nostre - La figlia del boss' + }) + + expect(results[43]).toMatchObject({ + start: '2022-10-05T04:58:00.000Z', + stop: '2022-10-05T05:28:00.000Z', + title: 'Tgunomattina - in collaborazione con day' + }) +}) + +it('can handle empty guide', () => { + const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + const result = parser({ content, channel, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/toonamiaftermath.com/toonamiaftermath.com.config.js b/sites/toonamiaftermath.com/toonamiaftermath.com.config.js index 4be2308d..a1935733 100644 --- a/sites/toonamiaftermath.com/toonamiaftermath.com.config.js +++ b/sites/toonamiaftermath.com/toonamiaftermath.com.config.js @@ -1,60 +1,60 @@ -process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0' - -const dayjs = require('dayjs') -const axios = require('axios') - -const API_ENDPOINT = 'https://api.toonamiaftermath.com' - -module.exports = { - site: 'toonamiaftermath.com', - days: 3, - async url({ channel, date }) { - const playlists = await axios - .get( - `${API_ENDPOINT}/playlists?scheduleName=${channel.site_id}&startDate=${date - .add(1, 'd') - .toJSON()}&thisWeek=true&weekStartDay=monday` - ) - .then(r => r.data) - .catch(console.error) - - const playlist = playlists.find(p => date.isSame(p.startDate, 'day')) - - return `${API_ENDPOINT}/playlist?id=${playlist._id}&addInfo=true` - }, - parser({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.name, - sub_title: parseEpisode(item), - image: parseImage(item), - start: dayjs(item.startDate), - stop: dayjs(item.endDate) - }) - }) - - return programs - } -} - -function parseItems(content) { - if (!content) return [] - const data = JSON.parse(content) - if (!data || !data.playlist) return [] - - return data.playlist.blocks.reduce((acc, curr) => { - acc = acc.concat(curr.mediaList) - - return acc - }, []) -} - -function parseEpisode(item) { - return item && item.info && item.info.episode ? item.info.episode : null -} - -function parseImage(item) { - return item && item.info && item.info.image ? item.info.image : null -} +process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0' + +const dayjs = require('dayjs') +const axios = require('axios') + +const API_ENDPOINT = 'https://api.toonamiaftermath.com' + +module.exports = { + site: 'toonamiaftermath.com', + days: 3, + async url({ channel, date }) { + const playlists = await axios + .get( + `${API_ENDPOINT}/playlists?scheduleName=${channel.site_id}&startDate=${date + .add(1, 'd') + .toJSON()}&thisWeek=true&weekStartDay=monday` + ) + .then(r => r.data) + .catch(console.error) + + const playlist = playlists.find(p => date.isSame(p.startDate, 'day')) + + return `${API_ENDPOINT}/playlist?id=${playlist._id}&addInfo=true` + }, + parser({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.name, + sub_title: parseEpisode(item), + image: parseImage(item), + start: dayjs(item.startDate), + stop: dayjs(item.endDate) + }) + }) + + return programs + } +} + +function parseItems(content) { + if (!content) return [] + const data = JSON.parse(content) + if (!data || !data.playlist) return [] + + return data.playlist.blocks.reduce((acc, curr) => { + acc = acc.concat(curr.mediaList) + + return acc + }, []) +} + +function parseEpisode(item) { + return item && item.info && item.info.episode ? item.info.episode : null +} + +function parseImage(item) { + return item && item.info && item.info.image ? item.info.image : null +} diff --git a/sites/toonamiaftermath.com/toonamiaftermath.com.test.js b/sites/toonamiaftermath.com/toonamiaftermath.com.test.js index 139a557b..cc252b7c 100644 --- a/sites/toonamiaftermath.com/toonamiaftermath.com.test.js +++ b/sites/toonamiaftermath.com/toonamiaftermath.com.test.js @@ -1,61 +1,61 @@ -const { parser, url } = require('./toonamiaftermath.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 API_ENDPOINT = 'https://api.toonamiaftermath.com' - -const date = dayjs.utc('2022-11-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'Toonami Aftermath EST', - xmltv_id: 'ToonamiAftermathEast.us' -} - -it('can generate valid url', async () => { - axios.get.mockImplementation(url => { - if ( - url === - `${API_ENDPOINT}/playlists?scheduleName=Toonami Aftermath EST&startDate=2022-11-30T00:00:00.000Z&thisWeek=true&weekStartDay=monday` - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/playlists.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - const result = await url({ channel, date }) - - expect(result).toBe(`${API_ENDPOINT}/playlist?id=635fbd8117f6824d953a216e&addInfo=true`) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(62) - expect(results[0]).toMatchObject({ - start: '2022-11-29T17:00:30.231Z', - stop: '2022-11-29T17:20:54.031Z', - title: 'X-Men', - sub_title: 'Reunion (Part 1)', - image: 'https://i.imgur.com/ZSZ0x1m.gif' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ content: '', date }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./toonamiaftermath.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 API_ENDPOINT = 'https://api.toonamiaftermath.com' + +const date = dayjs.utc('2022-11-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'Toonami Aftermath EST', + xmltv_id: 'ToonamiAftermathEast.us' +} + +it('can generate valid url', async () => { + axios.get.mockImplementation(url => { + if ( + url === + `${API_ENDPOINT}/playlists?scheduleName=Toonami Aftermath EST&startDate=2022-11-30T00:00:00.000Z&thisWeek=true&weekStartDay=monday` + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/playlists.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + const result = await url({ channel, date }) + + expect(result).toBe(`${API_ENDPOINT}/playlist?id=635fbd8117f6824d953a216e&addInfo=true`) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(62) + expect(results[0]).toMatchObject({ + start: '2022-11-29T17:00:30.231Z', + stop: '2022-11-29T17:20:54.031Z', + title: 'X-Men', + sub_title: 'Reunion (Part 1)', + image: 'https://i.imgur.com/ZSZ0x1m.gif' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ content: '', date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/turksatkablo.com.tr/turksatkablo.com.tr.config.js b/sites/turksatkablo.com.tr/turksatkablo.com.tr.config.js index 12edc623..8938beee 100644 --- a/sites/turksatkablo.com.tr/turksatkablo.com.tr.config.js +++ b/sites/turksatkablo.com.tr/turksatkablo.com.tr.config.js @@ -1,88 +1,88 @@ -const { DateTime } = require('luxon') - -module.exports = { - site: 'turksatkablo.com.tr', - days: 2, - url({ date }) { - const dayOfMonth = date.format('DD') // Get the current day of the month (01-31) - - return `https://www.turksatkablo.com.tr/userUpload/EPG/${dayOfMonth}.json?_=${date.valueOf()}` - }, - request: { - timeout: 60000, - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, date) - if (prev && start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - let stop = parseStop(item, date) - if (prev && stop < start) { - stop = stop.plus({ days: 1 }) - date = date.add(1, 'd') - } - programs.push({ - title: item.b, - start, - stop - }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - const dayjs = require('dayjs') - const dayOfMonth = dayjs().format('DD') - - const data = await axios - .get(`https://www.turksatkablo.com.tr/userUpload/EPG/${dayOfMonth}.json`) - .then(r => r.data) - .catch(console.log) - - let channels = [] - - data.k.forEach(item => { - channels.push({ - lang: 'tr', - site_id: item.x, - name: item.n - }) - }) - - return channels - } -} - -function parseStart(item, date) { - const time = `${date.format('YYYY-MM-DD')} ${item.c}` - - return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Istanbul' }).toUTC() -} - -function parseStop(item, date) { - const time = `${date.format('YYYY-MM-DD')} ${item.d}` - - return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Istanbul' }).toUTC() -} - -function parseItems(content, channel) { - let parsed - try { - parsed = JSON.parse(content) - } catch { - return [] - } - if (!parsed || !parsed.k) return [] - const data = parsed.k.find(c => c.x == channel.site_id) - - return data ? data.p : [] -} +const { DateTime } = require('luxon') + +module.exports = { + site: 'turksatkablo.com.tr', + days: 2, + url({ date }) { + const dayOfMonth = date.format('DD') // Get the current day of the month (01-31) + + return `https://www.turksatkablo.com.tr/userUpload/EPG/${dayOfMonth}.json?_=${date.valueOf()}` + }, + request: { + timeout: 60000, + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, date) + if (prev && start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + let stop = parseStop(item, date) + if (prev && stop < start) { + stop = stop.plus({ days: 1 }) + date = date.add(1, 'd') + } + programs.push({ + title: item.b, + start, + stop + }) + }) + + return programs + }, + async channels() { + const axios = require('axios') + const dayjs = require('dayjs') + const dayOfMonth = dayjs().format('DD') + + const data = await axios + .get(`https://www.turksatkablo.com.tr/userUpload/EPG/${dayOfMonth}.json`) + .then(r => r.data) + .catch(console.log) + + let channels = [] + + data.k.forEach(item => { + channels.push({ + lang: 'tr', + site_id: item.x, + name: item.n + }) + }) + + return channels + } +} + +function parseStart(item, date) { + const time = `${date.format('YYYY-MM-DD')} ${item.c}` + + return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Istanbul' }).toUTC() +} + +function parseStop(item, date) { + const time = `${date.format('YYYY-MM-DD')} ${item.d}` + + return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Istanbul' }).toUTC() +} + +function parseItems(content, channel) { + let parsed + try { + parsed = JSON.parse(content) + } catch { + return [] + } + if (!parsed || !parsed.k) return [] + const data = parsed.k.find(c => c.x == channel.site_id) + + return data ? data.p : [] +} diff --git a/sites/turksatkablo.com.tr/turksatkablo.com.tr.test.js b/sites/turksatkablo.com.tr/turksatkablo.com.tr.test.js index 042efebf..0342a06f 100644 --- a/sites/turksatkablo.com.tr/turksatkablo.com.tr.test.js +++ b/sites/turksatkablo.com.tr/turksatkablo.com.tr.test.js @@ -1,48 +1,48 @@ -const { parser, url } = require('./turksatkablo.com.tr.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-05-30', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: '1908' } - -it('can generate valid url', () => { - const result = url({ date }) - - expect(result).toBe('https://www.turksatkablo.com.tr/userUpload/EPG/30.json?_=1748563200000') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const results = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(14) - expect(results[0]).toMatchObject({ - title: '-', - start: '2025-05-29T21:00:00.000Z', - stop: '2025-05-29T22:30:00.000Z' - }) - expect(results[1]).toMatchObject({ - title: 'Şeytanın Evi', - start: '2025-05-29T22:30:00.000Z', - stop: '2025-05-30T00:00:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - const result = parser({ - date, - channel, - content - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./turksatkablo.com.tr.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-05-30', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: '1908' } + +it('can generate valid url', () => { + const result = url({ date }) + + expect(result).toBe('https://www.turksatkablo.com.tr/userUpload/EPG/30.json?_=1748563200000') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(14) + expect(results[0]).toMatchObject({ + title: '-', + start: '2025-05-29T21:00:00.000Z', + stop: '2025-05-29T22:30:00.000Z' + }) + expect(results[1]).toMatchObject({ + title: 'Şeytanın Evi', + start: '2025-05-29T22:30:00.000Z', + stop: '2025-05-30T00:00:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + const result = parser({ + date, + channel, + content + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv-programme.telecablesat.fr/tv-programme.telecablesat.fr.config.js b/sites/tv-programme.telecablesat.fr/tv-programme.telecablesat.fr.config.js index 87f97693..041caab3 100644 --- a/sites/tv-programme.telecablesat.fr/tv-programme.telecablesat.fr.config.js +++ b/sites/tv-programme.telecablesat.fr/tv-programme.telecablesat.fr.config.js @@ -1,115 +1,115 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const { DateTime } = require('luxon') - -const API_ENDPOINT = 'https://tv-programme.telecablesat.fr/chaine' -const headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0' -} - -module.exports = { - site: 'tv-programme.telecablesat.fr', - days: 2, - delay: 5000, - request: { - headers - }, - url: function ({ channel, date }) { - return `${API_ENDPOINT}/${channel.site_id}/index.html?date=${date.format( - 'YYYY-MM-DD' - )}&period=morning` - }, - async parser({ content, date, channel }) { - let programs = [] - let items = parseItems(content) - if (!items.length) return programs - const url = `${API_ENDPOINT}/${channel.site_id}/index.html` - const promises = [ - axios.get(`${url}?date=${date.format('YYYY-MM-DD')}&period=noon`, { headers }), - axios.get(`${url}?date=${date.format('YYYY-MM-DD')}&period=afternoon`, { headers }) - ] - await Promise.allSettled(promises).then(results => { - results.forEach(r => { - if (r.status === 'fulfilled') { - items = items.concat(parseItems(r.value.data)) - } - }) - }) - for (let item of items) { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ hours: 1 }) - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - image: parseImage($item), - start, - stop - }) - } - - return programs - }, - async channels() { - const data = await axios - .get('https://tv-programme.telecablesat.fr/', { headers }) - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(data) - const items = $( - '#ptgv_left > section.main > div > div > div:nth-child(1) > div > div > div.linker.with_search > div.inside > div.scroller > a' - ).toArray() - - return items.map(item => { - const $item = cheerio.load(item) - const link = $item('*').attr('href') - const [, site_id] = link.match(/\/chaine\/(\d+)\//) || [null, null] - const name = $item('*').text().trim() - return { - lang: 'fr', - site_id, - name - } - }) - } -} - -function parseStart($item, date) { - const timeString = $item('.schedule-hour').text() - if (!timeString) return null - - return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${timeString}`, 'yyyy-MM-dd HH:mm', { - zone: 'Europe/Paris' - }).toUTC() -} - -function parseImage($item) { - const imgSrc = $item('img').attr('src') - - return imgSrc ? `https:${imgSrc}` : null -} - -function parseTitle($item) { - return $item('div.item-content > div.title-left').text().trim() -} - -function parseDescription($item) { - return $item('div.item-content > p').text() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $( - '#ptgv_left > div.container > div.row.no-gutter > div.col-md-8 > div > div > div > div > div > div > div.news' - ).toArray() -} +const cheerio = require('cheerio') +const axios = require('axios') +const { DateTime } = require('luxon') + +const API_ENDPOINT = 'https://tv-programme.telecablesat.fr/chaine' +const headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0' +} + +module.exports = { + site: 'tv-programme.telecablesat.fr', + days: 2, + delay: 5000, + request: { + headers + }, + url: function ({ channel, date }) { + return `${API_ENDPOINT}/${channel.site_id}/index.html?date=${date.format( + 'YYYY-MM-DD' + )}&period=morning` + }, + async parser({ content, date, channel }) { + let programs = [] + let items = parseItems(content) + if (!items.length) return programs + const url = `${API_ENDPOINT}/${channel.site_id}/index.html` + const promises = [ + axios.get(`${url}?date=${date.format('YYYY-MM-DD')}&period=noon`, { headers }), + axios.get(`${url}?date=${date.format('YYYY-MM-DD')}&period=afternoon`, { headers }) + ] + await Promise.allSettled(promises).then(results => { + results.forEach(r => { + if (r.status === 'fulfilled') { + items = items.concat(parseItems(r.value.data)) + } + }) + }) + for (let item of items) { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ hours: 1 }) + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + image: parseImage($item), + start, + stop + }) + } + + return programs + }, + async channels() { + const data = await axios + .get('https://tv-programme.telecablesat.fr/', { headers }) + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(data) + const items = $( + '#ptgv_left > section.main > div > div > div:nth-child(1) > div > div > div.linker.with_search > div.inside > div.scroller > a' + ).toArray() + + return items.map(item => { + const $item = cheerio.load(item) + const link = $item('*').attr('href') + const [, site_id] = link.match(/\/chaine\/(\d+)\//) || [null, null] + const name = $item('*').text().trim() + return { + lang: 'fr', + site_id, + name + } + }) + } +} + +function parseStart($item, date) { + const timeString = $item('.schedule-hour').text() + if (!timeString) return null + + return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${timeString}`, 'yyyy-MM-dd HH:mm', { + zone: 'Europe/Paris' + }).toUTC() +} + +function parseImage($item) { + const imgSrc = $item('img').attr('src') + + return imgSrc ? `https:${imgSrc}` : null +} + +function parseTitle($item) { + return $item('div.item-content > div.title-left').text().trim() +} + +function parseDescription($item) { + return $item('div.item-content > p').text() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $( + '#ptgv_left > div.container > div.row.no-gutter > div.col-md-8 > div > div > div > div > div > div > div.news' + ).toArray() +} diff --git a/sites/tv-programme.telecablesat.fr/tv-programme.telecablesat.fr.test.js b/sites/tv-programme.telecablesat.fr/tv-programme.telecablesat.fr.test.js index 7a0af792..2cf57d25 100644 --- a/sites/tv-programme.telecablesat.fr/tv-programme.telecablesat.fr.test.js +++ b/sites/tv-programme.telecablesat.fr/tv-programme.telecablesat.fr.test.js @@ -1,96 +1,96 @@ -const { parser, url, request } = require('./tv-programme.telecablesat.fr.config.js') -const axios = require('axios') -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('2023-11-30', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '13', - xmltv_id: 'DasErste.de' -} -const headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0' -} - -jest.mock('axios') - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://tv-programme.telecablesat.fr/chaine/13/index.html?date=2023-11-30&period=morning' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject(headers) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_morning.html')) - - axios.get.mockImplementation((url, config) => { - if ( - url === - 'https://tv-programme.telecablesat.fr/chaine/13/index.html?date=2023-11-30&period=noon' && - JSON.stringify(config.headers) === JSON.stringify(headers) - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_noon.html')) - }) - } else if ( - url === - 'https://tv-programme.telecablesat.fr/chaine/13/index.html?date=2023-11-30&period=afternoon' && - JSON.stringify(config.headers) === JSON.stringify(headers) - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_afternoon.html')) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-11-30T08:00:00.000Z', - stop: '2023-11-30T08:05:00.000Z', - title: 'Tagesschau', - description: - 'Die Tagesschau ist eine Institution in der deutschen Fernsehlandschaft. Seit 1952 wird kurz und bündig von aktuellen Geschehnissen in Deutschland und der Welt berichtet. Bis heute ist die Redaktion der sachlichen Berichterstattung treu geblieben und...', - image: - 'https://tv.cdnartwhere.eu/cache/i2/Dc5BDoMgEADAv3Cuwoqy4Fu4LLho24hEaNK06d_rcW7zFYEqi1lsrZU6e-nlXrqyHe2oXVxyT5_Xybys3GduXsYjN7pnPqdkI0CkJbk4gnKWMQFAQLQUtHZesuEwOgWa7DCkKV4cGFEBG0eQrCJSwY3YP8oqbmKn-rwexuBb20n8_g.jpg' - }) - - expect(results[36]).toMatchObject({ - start: '2023-12-01T04:30:00.000Z', - stop: '2023-12-01T05:30:00.000Z', - title: 'ZDF-Morgenmagazin', - description: 'Für einen guten Start in den Tag', - image: - 'https://tv.cdnartwhere.eu/cache/i2/Dc5BDoMgEADAv3CuIiIu-BYuu7Bo24hEaNK06d_rbY7zFYSVxSK21kpdvPRyL13ZjnbULsTc4-d1MseV-8zNy3DkhvfMp0k2KBUwJhcmNTjLkJRSBGCRtHZe2gQTYXRhJNCoL1NyyiTiSI6IhtHADOT6R1nFTexYn9djnuGtrRG_Pw.jpg' - }) -}) - -it('can handle empty guide', done => { - parser({ - content: - ' ', - date, - channel - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(done) -}) +const { parser, url, request } = require('./tv-programme.telecablesat.fr.config.js') +const axios = require('axios') +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('2023-11-30', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '13', + xmltv_id: 'DasErste.de' +} +const headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0' +} + +jest.mock('axios') + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://tv-programme.telecablesat.fr/chaine/13/index.html?date=2023-11-30&period=morning' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject(headers) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_morning.html')) + + axios.get.mockImplementation((url, config) => { + if ( + url === + 'https://tv-programme.telecablesat.fr/chaine/13/index.html?date=2023-11-30&period=noon' && + JSON.stringify(config.headers) === JSON.stringify(headers) + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_noon.html')) + }) + } else if ( + url === + 'https://tv-programme.telecablesat.fr/chaine/13/index.html?date=2023-11-30&period=afternoon' && + JSON.stringify(config.headers) === JSON.stringify(headers) + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_afternoon.html')) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-11-30T08:00:00.000Z', + stop: '2023-11-30T08:05:00.000Z', + title: 'Tagesschau', + description: + 'Die Tagesschau ist eine Institution in der deutschen Fernsehlandschaft. Seit 1952 wird kurz und bündig von aktuellen Geschehnissen in Deutschland und der Welt berichtet. Bis heute ist die Redaktion der sachlichen Berichterstattung treu geblieben und...', + image: + 'https://tv.cdnartwhere.eu/cache/i2/Dc5BDoMgEADAv3Cuwoqy4Fu4LLho24hEaNK06d_rcW7zFYEqi1lsrZU6e-nlXrqyHe2oXVxyT5_Xybys3GduXsYjN7pnPqdkI0CkJbk4gnKWMQFAQLQUtHZesuEwOgWa7DCkKV4cGFEBG0eQrCJSwY3YP8oqbmKn-rwexuBb20n8_g.jpg' + }) + + expect(results[36]).toMatchObject({ + start: '2023-12-01T04:30:00.000Z', + stop: '2023-12-01T05:30:00.000Z', + title: 'ZDF-Morgenmagazin', + description: 'Für einen guten Start in den Tag', + image: + 'https://tv.cdnartwhere.eu/cache/i2/Dc5BDoMgEADAv3CuIiIu-BYuu7Bo24hEaNK06d_rbY7zFYSVxSK21kpdvPRyL13ZjnbULsTc4-d1MseV-8zNy3DkhvfMp0k2KBUwJhcmNTjLkJRSBGCRtHZe2gQTYXRhJNCoL1NyyiTiSI6IhtHADOT6R1nFTexYn9djnuGtrRG_Pw.jpg' + }) +}) + +it('can handle empty guide', done => { + parser({ + content: + ' ', + date, + channel + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(done) +}) diff --git a/sites/tv-spored.siol.net/tv-spored.siol.net.config.js b/sites/tv-spored.siol.net/tv-spored.siol.net.config.js index 45b8031f..53dbdc89 100644 --- a/sites/tv-spored.siol.net/tv-spored.siol.net.config.js +++ b/sites/tv-spored.siol.net/tv-spored.siol.net.config.js @@ -1,81 +1,81 @@ -const axios = require('axios') -const cheerio = require('cheerio') - -module.exports = { - site: 'tv-spored.siol.net', - days: 2, - url({ channel, date }) { - return `https://tv-spored.siol.net/kanal/${channel.site_id}/datum/${date.format('YYYYMMDD')}` - }, - request: { - headers: { - Accept: 'text/html' - } - }, - parser({ content, date }) { - const items = parseItems(content, date) - - return items.map(item => ({ - title: item.title, - category: item.category, - season: item.season, - episode: item.episode, - start: item.startDateTime, - stop: item.stopDateTime - })) - }, - async channels() { - const content = await axios - .get('https://tv-spored.siol.net/', { - headers: { - Accept: 'text/html' - } - }) - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(content) - const script = $('script:contains(tvChannelsAsJson)').text() - const func = new Function(`const self = { __next_f: [] };${script};return self.__next_f`) - const __next_f = func() - if (!__next_f[0] || !__next_f[0][1]) return [] - const [, dataString] = __next_f[0][1].split(/:(.*)/s) - const data = JSON.parse(dataString) - const tvChannelsAsJson = findByKey(data, 'tvChannelsAsJson') - - return tvChannelsAsJson.map(item => ({ - name: item.name, - site_id: item.externalId.toLowerCase(), - lang: 'sl' - })) - } -} - -function parseItems(content, date) { - try { - const $ = cheerio.load(content) - const script = $('script:contains(channelsAsJson)').text() - const func = new Function(`const self = { __next_f: [] };${script};return self.__next_f`) - const __next_f = func() - if (!__next_f[0] || !__next_f[0][1]) return [] - const [, dataString] = __next_f[0][1].split(/:(.*)/s) - const data = JSON.parse(dataString) - const channelsAsJson = findByKey(data, 'channelsAsJson') - - if (!channelsAsJson[0] || !Array.isArray(channelsAsJson[0].events)) return [] - - return channelsAsJson[0].events.filter(p => date.isSame(p.startDateTime, 'day')) - } catch { - return [] - } -} - -function findByKey(arr, key) { - if (!Array.isArray(arr)) return - return arr.reduce((a, item) => { - if (a) return a - if (item && item[key]) return item[key] - if (item && item.children) return findByKey(item.children, key) - if (Array.isArray(item)) return findByKey(item, key) - }, null) -} +const axios = require('axios') +const cheerio = require('cheerio') + +module.exports = { + site: 'tv-spored.siol.net', + days: 2, + url({ channel, date }) { + return `https://tv-spored.siol.net/kanal/${channel.site_id}/datum/${date.format('YYYYMMDD')}` + }, + request: { + headers: { + Accept: 'text/html' + } + }, + parser({ content, date }) { + const items = parseItems(content, date) + + return items.map(item => ({ + title: item.title, + category: item.category, + season: item.season, + episode: item.episode, + start: item.startDateTime, + stop: item.stopDateTime + })) + }, + async channels() { + const content = await axios + .get('https://tv-spored.siol.net/', { + headers: { + Accept: 'text/html' + } + }) + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(content) + const script = $('script:contains(tvChannelsAsJson)').text() + const func = new Function(`const self = { __next_f: [] };${script};return self.__next_f`) + const __next_f = func() + if (!__next_f[0] || !__next_f[0][1]) return [] + const [, dataString] = __next_f[0][1].split(/:(.*)/s) + const data = JSON.parse(dataString) + const tvChannelsAsJson = findByKey(data, 'tvChannelsAsJson') + + return tvChannelsAsJson.map(item => ({ + name: item.name, + site_id: item.externalId.toLowerCase(), + lang: 'sl' + })) + } +} + +function parseItems(content, date) { + try { + const $ = cheerio.load(content) + const script = $('script:contains(channelsAsJson)').text() + const func = new Function(`const self = { __next_f: [] };${script};return self.__next_f`) + const __next_f = func() + if (!__next_f[0] || !__next_f[0][1]) return [] + const [, dataString] = __next_f[0][1].split(/:(.*)/s) + const data = JSON.parse(dataString) + const channelsAsJson = findByKey(data, 'channelsAsJson') + + if (!channelsAsJson[0] || !Array.isArray(channelsAsJson[0].events)) return [] + + return channelsAsJson[0].events.filter(p => date.isSame(p.startDateTime, 'day')) + } catch { + return [] + } +} + +function findByKey(arr, key) { + if (!Array.isArray(arr)) return + return arr.reduce((a, item) => { + if (a) return a + if (item && item[key]) return item[key] + if (item && item.children) return findByKey(item.children, key) + if (Array.isArray(item)) return findByKey(item, key) + }, null) +} diff --git a/sites/tv-spored.siol.net/tv-spored.siol.net.test.js b/sites/tv-spored.siol.net/tv-spored.siol.net.test.js index 846efbcf..431e152c 100644 --- a/sites/tv-spored.siol.net/tv-spored.siol.net.test.js +++ b/sites/tv-spored.siol.net/tv-spored.siol.net.test.js @@ -1,56 +1,56 @@ -const { parser, url, request } = require('./tv-spored.siol.net.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-15', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'exodustv', - xmltv_id: 'ExodusTV.si' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://tv-spored.siol.net/kanal/exodustv/datum/20250115') -}) - -it('can generate request headers', () => { - expect(request.headers).toMatchObject({ - Accept: 'text/html' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }) - - expect(results.length).toBe(41) - expect(results[0]).toMatchObject({ - start: '2025-01-15T00:00:00.000Z', - stop: '2025-01-15T00:30:00.000Z', - title: 'Novice iz Svete dežele', - category: 'informativni', - season: null, - episode: null - }) - - expect(results[40]).toMatchObject({ - start: '2025-01-15T23:00:00.000Z', - stop: '2025-01-15T23:45:00.000Z', - title: 'Sveta maša', - category: 'ostalo', - season: null, - episode: null - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./tv-spored.siol.net.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-15', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'exodustv', + xmltv_id: 'ExodusTV.si' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://tv-spored.siol.net/kanal/exodustv/datum/20250115') +}) + +it('can generate request headers', () => { + expect(request.headers).toMatchObject({ + Accept: 'text/html' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }) + + expect(results.length).toBe(41) + expect(results[0]).toMatchObject({ + start: '2025-01-15T00:00:00.000Z', + stop: '2025-01-15T00:30:00.000Z', + title: 'Novice iz Svete dežele', + category: 'informativni', + season: null, + episode: null + }) + + expect(results[40]).toMatchObject({ + start: '2025-01-15T23:00:00.000Z', + stop: '2025-01-15T23:45:00.000Z', + title: 'Sveta maša', + category: 'ostalo', + season: null, + episode: null + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv.blue.ch/__data__/content.json b/sites/tv.blue.ch/__data__/content.json new file mode 100644 index 00000000..682d337e --- /dev/null +++ b/sites/tv.blue.ch/__data__/content.json @@ -0,0 +1 @@ +{"Nodes":{"Count":1,"TotalItemCount":1,"Items":[{"Domain":"TV","Identifier":"1221","Kind":"Channel","Content":{"Description":{"Title":"blue Zoom D","Language":"de"},"Nodes":{"Count":29,"TotalItemCount":29,"Items":[{"Domain":"TV","Identifier":"t1221ddc59247d45","Kind":"Broadcast","Channel":"1221","Content":{"Description":{"Title":"Weekend on the Rocks","Summary":" - «R.E.S.P.E.C.T», lieber Charles Nguela. Der Comedian tourt fleissig durch die Schweiz, macht für uns aber einen Halt, um in der neuen Ausgabe von «Weekend on the Rocks» mit Moderatorin Vania Spescha über die Entertainment-News der Woche zu plaudern.","ShortSummary":"","Country":"CH","ReleaseDate":"2021-01-01T00:00:00Z","Source":"13","Language":"de","Duration":"00:30:00"},"Nodes":{"Count":1,"TotalItemCount":1,"Items":[{"Domain":"TV","Identifier":"t1221ddc59247d45_landscape","Kind":"Image","Role":"Landscape","ContentPath":"/tv/broadcast/1221/t1221ddc59247d45_landscape","Version":{"Date":"2022-01-04T08:55:22.567Z"}}]},"TechnicalAttributes":{"Stereo":true}},"Version":{"Hash":"60d3"},"Availabilities":[{"AvailabilityStart":"2022-01-16T23:30:00Z","AvailabilityEnd":"2022-01-17T00:00:00Z"}],"Relations":[{"Domain":"TV","Kind":"Reference","Role":"ChannelIdentifier","TargetIdentifier":"2b0898c7-3920-3200-7048-4ea5d9138921"},{"Domain":"TV","Kind":"Reference","Role":"OriginalAirSeries","TargetIdentifier":"false"},{"Domain":"TV","Kind":"Reference","Role":"ExternalBroadcastIdentifier","TargetIdentifier":"167324536-11"},{"Domain":"TV","Kind":"Reference","Role":"ProgramIdentifier","TargetIdentifier":"p12211351631155","Title":"Original"}]}]}}}]}} \ No newline at end of file diff --git a/sites/tv.blue.ch/__data__/content_invalid_siteid.json b/sites/tv.blue.ch/__data__/content_invalid_siteid.json new file mode 100644 index 00000000..974a9930 --- /dev/null +++ b/sites/tv.blue.ch/__data__/content_invalid_siteid.json @@ -0,0 +1 @@ +{"Status":{"Version":"7","Status":"OK","ProcessingTime":"00:00:00.0160674","ExecutionTime":"2022-01-17T13:47:30.584Z"},"Request":{"Domain":"TV","Resource":"Channels","Action":"List","Parameters":"(ids=12210;start=202201170000;end=202201180000;level=normal)","Identifiers":["12210"],"Start":"2022-01-17T00:00:00Z","End":"2022-01-18T00:00:00Z","DataLevel":"Normal"},"DataSource":{"Snapshot":"Tv_20220117114748","DbCreationTime":"2022-01-17T11:49:14.608Z","IncrementCreationTime":"0001-01-01T00:00:00Z"},"Nodes":{"Items":[]}} \ No newline at end of file diff --git a/sites/tv.blue.ch/__data__/content_without_image.json b/sites/tv.blue.ch/__data__/content_without_image.json new file mode 100644 index 00000000..8e698d46 --- /dev/null +++ b/sites/tv.blue.ch/__data__/content_without_image.json @@ -0,0 +1 @@ +{"Nodes":{"Count":1,"TotalItemCount":1,"Items":[{"Domain":"TV","Identifier":"1221","Kind":"Channel","Content":{"Description":{"Title":"blue Zoom D","Language":"de"},"Nodes":{"Count":29,"TotalItemCount":29,"Items":[{"Domain":"TV","Identifier":"t10014a78a8b0668","Kind":"Broadcast","Channel":"1001","Content":{"Description":{"Title":"Lorem ipsum","Language":"fr","Duration":"00:01:00"}},"Version":{"Hash":"440e"},"Availabilities":[{"AvailabilityStart":"2022-01-17T04:59:00Z","AvailabilityEnd":"2022-01-17T05:00:00Z"}],"Relations":[{"Domain":"TV","Kind":"Reference","Role":"ChannelIdentifier","TargetIdentifier":"3553a4f2-ff63-5200-7048-d8d59d805f81"},{"Domain":"TV","Kind":"Reference","Role":"Dummy","TargetIdentifier":"True"},{"Domain":"TV","Kind":"Reference","Role":"ProgramIdentifier","TargetIdentifier":"p1"}]}]}}}]}} \ No newline at end of file diff --git a/sites/tv.blue.ch/__data__/no_content.json b/sites/tv.blue.ch/__data__/no_content.json new file mode 100644 index 00000000..c4df608d --- /dev/null +++ b/sites/tv.blue.ch/__data__/no_content.json @@ -0,0 +1 @@ +{"Status":{"Version":"7","Status":"OK","ExecutionTime":"2022-01-17T15:30:37.97Z"},"Request":{"Domain":"TV","Resource":"Channels","Action":"List","Parameters":"(ids=1884;start=202201170000;end=202201180000;level=normal)","Identifiers":["1884"],"Start":"2022-01-17T00:00:00Z","End":"2022-01-18T00:00:00Z","DataLevel":"Normal"},"DataSource":{"Snapshot":"Tv_20220117144354","DbCreationTime":"2022-01-17T14:45:11.84Z","IncrementCreationTime":"0001-01-01T00:00:00Z"},"Nodes":{"Count":1,"TotalItemCount":1,"Items":[{"Domain":"TV","Identifier":"1884","Kind":"Channel","Content":{"Description":{"Title":"Fisu.tv 1","Language":"en"}}}]}} \ No newline at end of file diff --git a/sites/tv.blue.ch/tv.blue.ch.config.js b/sites/tv.blue.ch/tv.blue.ch.config.js index 0ae7acba..e1a09589 100644 --- a/sites/tv.blue.ch/tv.blue.ch.config.js +++ b/sites/tv.blue.ch/tv.blue.ch.config.js @@ -1,85 +1,85 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'tv.blue.ch', - days: 2, - url: function ({ channel, date }) { - return `https://services.sg101.prd.sctv.ch/catalog/tv/channels/list/(ids=${ - channel.site_id - };start=${date.format('YYYYMMDDHHss')};end=${date - .add(1, 'd') - .format('YYYYMMDDHHss')};level=normal)` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const title = parseTitle(item) - if (title === 'Sendepause') return - programs.push({ - title, - description: parseDescription(item), - image: parseImage(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const items = await axios - .get('https://services.sg101.prd.sctv.ch/portfolio/tv/channels') - .then(r => r.data) - .catch(console.log) - - return items.map(item => { - return { - lang: item.Languages[0] || 'de', - site_id: item.Identifier, - name: item.Title - } - }) - } -} - -function parseTitle(item) { - return item.Content.Description.Title -} - -function parseDescription(item) { - return item.Content.Description.Summary -} - -function parseImage(item) { - const image = item.Content.Nodes ? item.Content.Nodes.Items.find(i => i.Kind === 'Image') : null - const path = image ? image.ContentPath : null - - return path ? `https://services.sg101.prd.sctv.ch/content/images${path}_w1920.webp` : null -} - -function parseStart(item) { - const available = item.Availabilities.length ? item.Availabilities[0] : null - - return dayjs(available.AvailabilityStart) -} - -function parseStop(item) { - const available = item.Availabilities.length ? item.Availabilities[0] : null - - return dayjs(available.AvailabilityEnd) -} - -function parseItems(content) { - const data = JSON.parse(content) - const nodes = data.Nodes.Items.filter(i => i.Kind === 'Channel') - if (!nodes.length) return [] - - return nodes[0].Content.Nodes && Array.isArray(nodes[0].Content.Nodes.Items) - ? nodes[0].Content.Nodes.Items.filter(i => i.Kind === 'Broadcast') - : [] -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'tv.blue.ch', + days: 2, + url: function ({ channel, date }) { + return `https://services.sg101.prd.sctv.ch/catalog/tv/channels/list/(ids=${ + channel.site_id + };start=${date.format('YYYYMMDDHHss')};end=${date + .add(1, 'd') + .format('YYYYMMDDHHss')};level=normal)` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const title = parseTitle(item) + if (title === 'Sendepause') return + programs.push({ + title, + description: parseDescription(item), + image: parseImage(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const items = await axios + .get('https://services.sg101.prd.sctv.ch/portfolio/tv/channels') + .then(r => r.data) + .catch(console.log) + + return items.map(item => { + return { + lang: item.Languages[0] || 'de', + site_id: item.Identifier, + name: item.Title + } + }) + } +} + +function parseTitle(item) { + return item.Content.Description.Title +} + +function parseDescription(item) { + return item.Content.Description.Summary +} + +function parseImage(item) { + const image = item.Content.Nodes ? item.Content.Nodes.Items.find(i => i.Kind === 'Image') : null + const path = image ? image.ContentPath : null + + return path ? `https://services.sg101.prd.sctv.ch/content/images${path}_w1920.webp` : null +} + +function parseStart(item) { + const available = item.Availabilities.length ? item.Availabilities[0] : null + + return dayjs(available.AvailabilityStart) +} + +function parseStop(item) { + const available = item.Availabilities.length ? item.Availabilities[0] : null + + return dayjs(available.AvailabilityEnd) +} + +function parseItems(content) { + const data = JSON.parse(content) + const nodes = data.Nodes.Items.filter(i => i.Kind === 'Channel') + if (!nodes.length) return [] + + return nodes[0].Content.Nodes && Array.isArray(nodes[0].Content.Nodes.Items) + ? nodes[0].Content.Nodes.Items.filter(i => i.Kind === 'Broadcast') + : [] +} diff --git a/sites/tv.blue.ch/tv.blue.ch.test.js b/sites/tv.blue.ch/tv.blue.ch.test.js index e01ec286..0f268e65 100644 --- a/sites/tv.blue.ch/tv.blue.ch.test.js +++ b/sites/tv.blue.ch/tv.blue.ch.test.js @@ -1,74 +1,72 @@ -const { parser, url } = require('./tv.blue.ch.config.js') -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('2022-01-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1221', - xmltv_id: 'BlueZoomD.ch' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://services.sg101.prd.sctv.ch/catalog/tv/channels/list/(ids=1221;start=202201170000;end=202201180000;level=normal)' - ) -}) - -it('can parse response', () => { - const content = - '{"Nodes":{"Count":1,"TotalItemCount":1,"Items":[{"Domain":"TV","Identifier":"1221","Kind":"Channel","Content":{"Description":{"Title":"blue Zoom D","Language":"de"},"Nodes":{"Count":29,"TotalItemCount":29,"Items":[{"Domain":"TV","Identifier":"t1221ddc59247d45","Kind":"Broadcast","Channel":"1221","Content":{"Description":{"Title":"Weekend on the Rocks","Summary":" - «R.E.S.P.E.C.T», lieber Charles Nguela. Der Comedian tourt fleissig durch die Schweiz, macht für uns aber einen Halt, um in der neuen Ausgabe von «Weekend on the Rocks» mit Moderatorin Vania Spescha über die Entertainment-News der Woche zu plaudern.","ShortSummary":"","Country":"CH","ReleaseDate":"2021-01-01T00:00:00Z","Source":"13","Language":"de","Duration":"00:30:00"},"Nodes":{"Count":1,"TotalItemCount":1,"Items":[{"Domain":"TV","Identifier":"t1221ddc59247d45_landscape","Kind":"Image","Role":"Landscape","ContentPath":"/tv/broadcast/1221/t1221ddc59247d45_landscape","Version":{"Date":"2022-01-04T08:55:22.567Z"}}]},"TechnicalAttributes":{"Stereo":true}},"Version":{"Hash":"60d3"},"Availabilities":[{"AvailabilityStart":"2022-01-16T23:30:00Z","AvailabilityEnd":"2022-01-17T00:00:00Z"}],"Relations":[{"Domain":"TV","Kind":"Reference","Role":"ChannelIdentifier","TargetIdentifier":"2b0898c7-3920-3200-7048-4ea5d9138921"},{"Domain":"TV","Kind":"Reference","Role":"OriginalAirSeries","TargetIdentifier":"false"},{"Domain":"TV","Kind":"Reference","Role":"ExternalBroadcastIdentifier","TargetIdentifier":"167324536-11"},{"Domain":"TV","Kind":"Reference","Role":"ProgramIdentifier","TargetIdentifier":"p12211351631155","Title":"Original"}]}]}}}]}}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-01-16T23:30:00.000Z', - stop: '2022-01-17T00:00:00.000Z', - title: 'Weekend on the Rocks', - description: - ' - «R.E.S.P.E.C.T», lieber Charles Nguela. Der Comedian tourt fleissig durch die Schweiz, macht für uns aber einen Halt, um in der neuen Ausgabe von «Weekend on the Rocks» mit Moderatorin Vania Spescha über die Entertainment-News der Woche zu plaudern.', - image: - 'https://services.sg101.prd.sctv.ch/content/images/tv/broadcast/1221/t1221ddc59247d45_landscape_w1920.webp' - } - ]) -}) - -it('can parse response without image', () => { - const content = - '{"Nodes":{"Count":1,"TotalItemCount":1,"Items":[{"Domain":"TV","Identifier":"1221","Kind":"Channel","Content":{"Description":{"Title":"blue Zoom D","Language":"de"},"Nodes":{"Count":29,"TotalItemCount":29,"Items":[{"Domain":"TV","Identifier":"t10014a78a8b0668","Kind":"Broadcast","Channel":"1001","Content":{"Description":{"Title":"Lorem ipsum","Language":"fr","Duration":"00:01:00"}},"Version":{"Hash":"440e"},"Availabilities":[{"AvailabilityStart":"2022-01-17T04:59:00Z","AvailabilityEnd":"2022-01-17T05:00:00Z"}],"Relations":[{"Domain":"TV","Kind":"Reference","Role":"ChannelIdentifier","TargetIdentifier":"3553a4f2-ff63-5200-7048-d8d59d805f81"},{"Domain":"TV","Kind":"Reference","Role":"Dummy","TargetIdentifier":"True"},{"Domain":"TV","Kind":"Reference","Role":"ProgramIdentifier","TargetIdentifier":"p1"}]}]}}}]}}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-01-17T04:59:00.000Z', - stop: '2022-01-17T05:00:00.000Z', - title: 'Lorem ipsum' - } - ]) -}) - -it('can handle wrong site id', () => { - const result = parser({ - content: - '{"Status":{"Version":"7","Status":"OK","ProcessingTime":"00:00:00.0160674","ExecutionTime":"2022-01-17T13:47:30.584Z"},"Request":{"Domain":"TV","Resource":"Channels","Action":"List","Parameters":"(ids=12210;start=202201170000;end=202201180000;level=normal)","Identifiers":["12210"],"Start":"2022-01-17T00:00:00Z","End":"2022-01-18T00:00:00Z","DataLevel":"Normal"},"DataSource":{"Snapshot":"Tv_20220117114748","DbCreationTime":"2022-01-17T11:49:14.608Z","IncrementCreationTime":"0001-01-01T00:00:00Z"},"Nodes":{"Items":[]}}' - }) - expect(result).toMatchObject([]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: - '{"Status":{"Version":"7","Status":"OK","ExecutionTime":"2022-01-17T15:30:37.97Z"},"Request":{"Domain":"TV","Resource":"Channels","Action":"List","Parameters":"(ids=1884;start=202201170000;end=202201180000;level=normal)","Identifiers":["1884"],"Start":"2022-01-17T00:00:00Z","End":"2022-01-18T00:00:00Z","DataLevel":"Normal"},"DataSource":{"Snapshot":"Tv_20220117144354","DbCreationTime":"2022-01-17T14:45:11.84Z","IncrementCreationTime":"0001-01-01T00:00:00Z"},"Nodes":{"Count":1,"TotalItemCount":1,"Items":[{"Domain":"TV","Identifier":"1884","Kind":"Channel","Content":{"Description":{"Title":"Fisu.tv 1","Language":"en"}}}]}}' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./tv.blue.ch.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const fs = require('fs') +const path = require('path') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-01-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1221', + xmltv_id: 'BlueZoomD.ch' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://services.sg101.prd.sctv.ch/catalog/tv/channels/list/(ids=1221;start=202201170000;end=202201180000;level=normal)' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-01-16T23:30:00.000Z', + stop: '2022-01-17T00:00:00.000Z', + title: 'Weekend on the Rocks', + description: + ' - «R.E.S.P.E.C.T», lieber Charles Nguela. Der Comedian tourt fleissig durch die Schweiz, macht für uns aber einen Halt, um in der neuen Ausgabe von «Weekend on the Rocks» mit Moderatorin Vania Spescha über die Entertainment-News der Woche zu plaudern.', + image: + 'https://services.sg101.prd.sctv.ch/content/images/tv/broadcast/1221/t1221ddc59247d45_landscape_w1920.webp' + } + ]) +}) + +it('can parse response without image', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_without_image.json')) + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-01-17T04:59:00.000Z', + stop: '2022-01-17T05:00:00.000Z', + title: 'Lorem ipsum' + } + ]) +}) + +it('can handle wrong site id', () => { + const result = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/content_invalid_siteid.json')) + }) + expect(result).toMatchObject([]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv.cctv.com/tv.cctv.com.config.js b/sites/tv.cctv.com/tv.cctv.com.config.js index 12cf7335..c455a6d0 100644 --- a/sites/tv.cctv.com/tv.cctv.com.config.js +++ b/sites/tv.cctv.com/tv.cctv.com.config.js @@ -1,42 +1,42 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'tv.cctv.com', - days: 2, - url({ channel, date }) { - return `https://api.cntv.cn/epg/getEpgInfoByChannelNew?serviceId=tvcctv&c=${ - channel.site_id - }&d=${date.format('YYYYMMDD')}` - }, - parser({ content, channel }) { - const programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const title = item.title - const start = parseStart(item) - const stop = parseStop(item) - programs.push({ - title, - start, - stop - }) - }) - - return programs - } -} - -function parseStop(item) { - return dayjs.unix(item.endTime) -} - -function parseStart(item) { - return dayjs.unix(item.startTime) -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !data.data) return [] - - return data.data[channel.site_id].list || [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'tv.cctv.com', + days: 2, + url({ channel, date }) { + return `https://api.cntv.cn/epg/getEpgInfoByChannelNew?serviceId=tvcctv&c=${ + channel.site_id + }&d=${date.format('YYYYMMDD')}` + }, + parser({ content, channel }) { + const programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const title = item.title + const start = parseStart(item) + const stop = parseStop(item) + programs.push({ + title, + start, + stop + }) + }) + + return programs + } +} + +function parseStop(item) { + return dayjs.unix(item.endTime) +} + +function parseStart(item) { + return dayjs.unix(item.startTime) +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !data.data) return [] + + return data.data[channel.site_id].list || [] +} diff --git a/sites/tv.cctv.com/tv.cctv.com.test.js b/sites/tv.cctv.com/tv.cctv.com.test.js index 0189f648..35b41e43 100644 --- a/sites/tv.cctv.com/tv.cctv.com.test.js +++ b/sites/tv.cctv.com/tv.cctv.com.test.js @@ -1,51 +1,51 @@ -const { parser, url } = require('./tv.cctv.com.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('2023-11-30', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'cctv1', - xmltv_id: 'CCTV1.cn' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://api.cntv.cn/epg/getEpgInfoByChannelNew?serviceId=tvcctv&c=cctv1&d=20231130' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const results = parser({ channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(37) - - expect(results[0]).toMatchObject({ - start: '2023-11-29T17:13:00.000Z', - stop: '2023-11-29T17:41:15.000Z', - title: '今日说法-2023-302' - }) - - expect(results[36]).toMatchObject({ - start: '2023-11-30T15:30:15.000Z', - stop: '2023-11-30T15:59:00.000Z', - title: '非遗里的中国-4' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - channel, - content: '{"errcode":"1001","msg":"params error"}' - }) - expect(results.length).toBe(0) -}) +const { parser, url } = require('./tv.cctv.com.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('2023-11-30', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'cctv1', + xmltv_id: 'CCTV1.cn' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://api.cntv.cn/epg/getEpgInfoByChannelNew?serviceId=tvcctv&c=cctv1&d=20231130' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = parser({ channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(37) + + expect(results[0]).toMatchObject({ + start: '2023-11-29T17:13:00.000Z', + stop: '2023-11-29T17:41:15.000Z', + title: '今日说法-2023-302' + }) + + expect(results[36]).toMatchObject({ + start: '2023-11-30T15:30:15.000Z', + stop: '2023-11-30T15:59:00.000Z', + title: '非遗里的中国-4' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + channel, + content: '{"errcode":"1001","msg":"params error"}' + }) + expect(results.length).toBe(0) +}) diff --git a/sites/tv.dir.bg/__data__/content.json b/sites/tv.dir.bg/__data__/content.json new file mode 100644 index 00000000..1c75a02e --- /dev/null +++ b/sites/tv.dir.bg/__data__/content.json @@ -0,0 +1,4 @@ +{ + "status": true, + "html": "
    \n
    \n
    \n

    \n 29.06\n

    \n
    \n
    \n
    \n \"Светът\n
    \n
    \n 06:00\n
    \n
    \n Светът на здравето\n
    \n
    \n
    \n
    \n \"Сестра\n
    \n
    \n 06:30\n
    \n
    \n Сестра Бети\n , сезон 7\n , епизод 12\n
    \n
    \n
    \n
    \n \"Тази\n
    \n
    \n 07:30\n
    \n
    \n Тази събота и неделя\n
    \n
    \n
    \n
    \n \"Богатствата\n
    \n
    \n 11:00\n
    \n
    \n Богатствата на България\n
    \n
    \n
    \n
    \n \"Светът\n
    \n
    \n 11:30\n
    \n
    \n Светът на здравето\n
    \n
    \n
    \n
    \n \"bTV\n
    \n
    \n 11:59\n
    \n
    \n bTV Новините\n
    \n
    \n
    \n
    \n \"НепознатиТЕ\"\n
    \n
    \n 12:30\n
    \n
    \n НепознатиТЕ\n
    \n
    \n
    \n
    \n \"MasterChef\"\n
    \n
    \n 13:00\n
    \n
    \n MasterChef\n , сезон 8\n , епизод 8\n
    \n
    \n
    \n
    \n \"Бригада\n
    \n
    \n 15:00\n
    \n
    \n Бригада Нов дом\n
    \n
    \n
    \n
    \n \"120\n
    \n
    \n 16:30\n
    \n
    \n 120 минути\n
    \n
    \n
    \n
    \n \"bTV\n
    \n
    \n 18:55\n
    \n
    \n bTV Новините\n
    \n
    \n
    \n
    \n \"Защо,\n
    \n
    \n 19:40\n
    \n
    \n Защо, господин министър?\n
    \n
    \n
    \n
    \n \"Аз\n
    \n
    \n 20:00\n
    \n
    \n Аз обичам България!\n
    \n
    \n
    \n
    \n \"Мафия\n
    \n
    \n 22:30\n
    \n
    \n Мафия Мама\n
    \n
    \n
    \n
    \n \"Мъртвите\n
    \n
    \n 00:30\n
    \n
    \n Мъртвите не умират\n
    \n
    \n
    \n
    \n \"120\n
    \n
    \n 02:40\n
    \n
    \n 120 минути\n
    \n
    \n
    \n
    \n \"Убийства\n
    \n
    \n 05:00\n
    \n
    \n Убийства в Рая\n , сезон 1\n , епизод 5\n
    \n
    \n
    \n
    \n
    \n
    \n
    \n

    \n 30.06\n

    \n
    \n
    \n
    \n \"Лице\n
    \n
    \n 06:15\n
    \n
    \n Лице в лице\n
    \n
    \n
    \n
    \n \"Тази\n
    \n
    \n 06:57\n
    \n
    \n Тази сутрин\n
    \n
    \n
    \n
    \n \"Преди\n
    \n
    \n 09:30\n
    \n
    \n Преди обед\n
    \n
    \n
    \n
    \n \"Хороскопът\n
    \n
    \n 11:52\n
    \n
    \n Хороскопът на Алена\n
    \n
    \n
    \n
    \n \"bTV\n
    \n
    \n 11:59\n
    \n
    \n bTV Новините\n
    \n
    \n
    \n
    \n \"Комиците\n
    \n
    \n 12:45\n
    \n
    \n Комиците и приятели\n
    \n
    \n
    \n
    \n \"Вражда\"\n
    \n
    \n 13:20\n
    \n
    \n Вражда\n , сезон 1\n , епизод 15\n
    \n
    \n
    \n
    \n \"Плен\"\n
    \n
    \n 14:50\n
    \n
    \n Плен\n , сезон 2\n , епизод 47\n
    \n
    \n
    \n
    \n \"Моите\n
    \n
    \n 15:55\n
    \n
    \n Моите братя и сестри\n , сезон 2\n , епизод 118\n
    \n
    \n
    \n
    \n \"bTV\n
    \n
    \n 16:59\n
    \n
    \n bTV Новините\n
    \n
    \n
    \n
    \n \"Лице\n
    \n
    \n 17:20\n
    \n
    \n Лице в лице\n
    \n
    \n
    \n
    \n \"Стани\n
    \n
    \n 18:00\n
    \n
    \n Стани богат\n , сезон 6\n , епизод 207\n
    \n
    \n
    \n
    \n \"bTV\n
    \n
    \n 18:55\n
    \n
    \n bTV Новините\n
    \n
    \n
    \n
    \n \"Лице\n
    \n
    \n 19:50\n
    \n
    \n Лице в лице след Новините\n
    \n
    \n
    \n
    \n \"Кой\n
    \n
    \n 20:00\n
    \n
    \n Кой да знае?\n , сезон 4\n , епизод 40\n
    \n
    \n
    \n
    \n \"Колко\n
    \n
    \n 21:30\n
    \n
    \n Колко ми даваш?\n , сезон 2\n , епизод 40\n
    \n
    \n
    \n
    \n \"bTV\n
    \n
    \n 22:30\n
    \n
    \n bTV Новините\n
    \n
    \n
    \n
    \n \"От\n
    \n
    \n 23:00\n
    \n
    \n От местопрестъплението: Маями\n , сезон 2\n , епизод 21\n
    \n
    \n
    \n
    \n \"Убийства\n
    \n
    \n 00:00\n
    \n
    \n Убийства в Рая\n , сезон 1\n , епизод 8\n
    \n
    \n
    \n
    \n \"Естествен\n
    \n
    \n 01:00\n
    \n
    \n Естествен интелект\n , сезон 2\n , епизод 4\n
    \n
    \n
    \n
    \n \"bTV\n
    \n
    \n 02:10\n
    \n
    \n bTV Новините\n
    \n
    \n
    \n
    \n \"Преди\n
    \n
    \n 02:50\n
    \n
    \n Преди обед\n
    \n
    \n
    \n
    \n \"Убийства\n
    \n
    \n 05:00\n
    \n
    \n Убийства в Рая\n , сезон 1\n , епизод 6\n
    \n
    \n
    \n
    \n
    \n
    \n
    \n

    \n 01.07\n

    \n
    \n
    \n
    \n \"Лице\n
    \n
    \n 06:15\n
    \n
    \n Лице в лице\n
    \n
    \n
    \n
    \n \"Тази\n
    \n
    \n 06:57\n
    \n
    \n Тази сутрин\n
    \n
    \n
    \n
    \n \"Преди\n
    \n
    \n 09:30\n
    \n
    \n Преди обед\n
    \n
    \n
    \n
    \n \"Хороскопът\n
    \n
    \n 11:52\n
    \n
    \n Хороскопът на Алена\n
    \n
    \n
    \n
    \n \"bTV\n
    \n
    \n 11:59\n
    \n
    \n bTV Новините\n
    \n
    \n
    \n
    \n \"Комиците\n
    \n
    \n 12:45\n
    \n
    \n Комиците и приятели\n
    \n
    \n
    \n
    \n \"Вражда\"\n
    \n
    \n 13:20\n
    \n
    \n Вражда\n , сезон 1\n , епизод 16\n
    \n
    \n
    \n
    \n \"Плен\"\n
    \n
    \n 14:50\n
    \n
    \n Плен\n , сезон 2\n , епизод 48\n
    \n
    \n
    \n
    \n \"Моите\n
    \n
    \n 15:55\n
    \n
    \n Моите братя и сестри\n , сезон 2\n , епизод 119\n
    \n
    \n
    \n
    \n \"bTV\n
    \n
    \n 16:59\n
    \n
    \n bTV Новините\n
    \n
    \n
    \n
    \n \"Лице\n
    \n
    \n 17:20\n
    \n
    \n Лице в лице\n
    \n
    \n
    \n
    \n \"Стани\n
    \n
    \n 18:00\n
    \n
    \n Стани богат\n , сезон 6\n , епизод 208\n
    \n
    \n
    \n
    \n \"bTV\n
    \n
    \n 18:55\n
    \n
    \n bTV Новините\n
    \n
    \n
    \n
    \n \"Лице\n
    \n
    \n 19:50\n
    \n
    \n Лице в лице след Новините\n
    \n
    \n
    \n
    \n \"Кой\n
    \n
    \n 20:00\n
    \n
    \n Кой да знае?\n , сезон 4\n , епизод 41\n
    \n
    \n
    \n
    \n \"Колко\n
    \n
    \n 21:30\n
    \n
    \n Колко ми даваш?\n , сезон 2\n , епизод 41\n
    \n
    \n
    \n
    \n \"bTV\n
    \n
    \n 22:30\n
    \n
    \n bTV Новините\n
    \n
    \n
    \n
    \n \"От\n
    \n
    \n 23:00\n
    \n
    \n От местопрестъплението: Маями\n , сезон 2\n , епизод 22\n
    \n
    \n
    \n
    \n \"Убийства\n
    \n
    \n 00:00\n
    \n
    \n Убийства в Рая\n , сезон 2\n , епизод 1\n
    \n
    \n
    \n
    \n \"Естествен\n
    \n
    \n 01:00\n
    \n
    \n Естествен интелект\n , сезон 2\n , епизод 5\n
    \n
    \n
    \n
    \n \"bTV\n
    \n
    \n 02:10\n
    \n
    \n bTV Новините\n
    \n
    \n
    \n
    \n \"Преди\n
    \n
    \n 02:50\n
    \n
    \n Преди обед\n
    \n
    \n
    \n
    \n \"Убийства\n
    \n
    \n 05:00\n
    \n
    \n Убийства в Рая\n , сезон 1\n , епизод 7\n
    \n
    \n
    \n
    \n
    \n
    " +} \ No newline at end of file diff --git a/sites/tv.dir.bg/__data__/no_content.json b/sites/tv.dir.bg/__data__/no_content.json new file mode 100644 index 00000000..b47923e3 --- /dev/null +++ b/sites/tv.dir.bg/__data__/no_content.json @@ -0,0 +1,4 @@ +{ + "status": true, + "html": "
    \n
    \n
    \n

    \n 29.07\n

    \n
    \n
    \n
    \n
    \n
    \n
    \n

    \n 30.07\n

    \n
    \n
    \n
    \n
    \n
    \n
    \n

    \n 31.07\n

    \n
    \n
    \n
    \n
    \n
    " +} \ No newline at end of file diff --git a/sites/tv.dir.bg/tv.dir.bg.channels.xml b/sites/tv.dir.bg/tv.dir.bg.channels.xml index af9fcd6a..c3ee6584 100644 --- a/sites/tv.dir.bg/tv.dir.bg.channels.xml +++ b/sites/tv.dir.bg/tv.dir.bg.channels.xml @@ -1,114 +1,103 @@ - - - MTV 00s - History - MTV Live HD - Eurosport HD - History HD - Nick HD - Планета HD - Disney Junior - Nat Geo HD - MTV 80s - Comedy Central - ID Xtra - MovieSTAR - FilmBox - MTV 90s - 24kitchen - ТВ 7/8 - Алфа - AMC - Animal Planet - AXN Black - AXN - AXN White - Балканика МТВ - Bloomberg TV Bulgaria - БНТ1 - БНТ2 - БНТ3 - БНТ4 - bTV - bTV Action - bTV Cinema - bTV Comedy - bTV Lady - Bulgaria ON AIR - Cartoon Network - CBS Reality - Первый канал - Cinemax2 - Cinemax - Crime + Investigation - Da Vinci - Diema - Diema Family - Diema Sport 2 - Diema Sport 3 - Diema Sport - Discovery Channel - Discovery Science - Disney Channel - DocuBox - E-Kids - Epic Drama - Евроком - Eurosport 1 - Eurosport 2 - Extreme Sports Channel - Фен Фолк - Фен - FilmBox Extra - FilmBox+ - Food Network - UA TV - HBO2 - HBO3 - HBO - HG TV - Хоби - Investigation Discovery - JimJam - Kino Nova - Love Nature - MAX Sport 1 - MAX Sport 2 - MAX Sport 3 - MAX Sport 4 - MCM - National Geographic - Nat Geo Wild - Nickelodeon - Nick Jr. - NOVA - Nova News - Nova Sport - НТВ Мир - Охота и рыбалка - Планета Фолк - Планета - Тракия (Пловдив) - RING - RM TV - Скат - FOX - FOX Crime - FOX Life - Телемедиа - TLC - Travel Channel - TV1 - Euronews Bulgaria - Враца - Viasat Explore - Viasat History - Viasat Nature - TV 1000 - Fashion TV - FightBox - Fuel TV - Mezzo Live HD - MTV Hits - Trace Sport Stars - + + + 24 Kitchen + 7/8 TV + Al Jazeera + Animal Planet + AXN + AXN Black + AXN White + Baby TV + BBC News (former BBC World News) + Bloomberg TV Bulgaria + BNT1 (БНТ1) + BNT2 (БНТ2) + BNT3 (БНТ3) + BNT4 (БНТ4) + bTV + bTV Action + bTV Cinema + bTV Comedy + bTV Story (f.k.a bTV Lady) + Bulgaria ON AIR (България Он Еър) + Cartoon Network Bulgaria + Cartoonito (f.k.a Boomerang TV) + Cinemania + Cinemax + Cinemax 2 + CineStar TV + CineStar TV Action&Thriller + CNN + Code Fashion TV + Code Health TV + Crime & Investigation + Diema + Diema Family + Diema Sport + Diema Sport 2 + Diema Sport 3 + Discovery Channel + Disney Channel + Dizi (Timeless Drama Channel) + Duck TV + DW TV + Epic Drama + Eurocom + Euronews + Euronews Bulgaria (f.k.a Evropa TV) + Eurosport + Eurosport 2 + Eurosport 4K + Fightklub HD (Bulgaria) + FilmBox Basic + FilmBox Extra + FilmBox Stars (FilmBox Plus) + Food Network HD + France 24 + HBO + HBO2 + HBO3 + HGTV (Discovery Home & Garden) + History Bulgaria + ID (Investigation Discovery) + Kanal 3 (Канал 3) + Kanal 4 (Канал 4) + Kino Nova + Love Nature + Magic TV + MAX Sport 1 + MAX Sport 2 + MAX Sport 3 + MAX Sport 4 + MovieSTAR + MTV Europe + National Geographic + National Geographic Wild + Nick Jr + Nickelodeon + Nicktoons + Nostalgia TV + Nova News HD + NOVA Sport + NOVA TV + Ring.bg (bTV Sport) + RTL + SKAT TV + Skyshowtime 1 + Skyshowtime 2 + STAR Channel (f.k.a. FOX) + STAR Crime (f.k.a FOX Crime) + STAR Life (f.k.a. FOX Life) + Super Toons + The History Channel 2 + The Voice TV + TLC + Travel Channel + TV1 Bulgaria + Viasat Explore + Viasat History + Viasat Kino (TV1000) + Viasat Nature + Viasat True Crime + Vivacom Arena (Виваком Арена) + diff --git a/sites/tv.dir.bg/tv.dir.bg.config.js b/sites/tv.dir.bg/tv.dir.bg.config.js index c1a15fbf..adf780d0 100644 --- a/sites/tv.dir.bg/tv.dir.bg.config.js +++ b/sites/tv.dir.bg/tv.dir.bg.config.js @@ -1,88 +1,216 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const { DateTime } = require('luxon') - -module.exports = { - site: 'tv.dir.bg', - days: 2, - url({ channel, date }) { - return `https://tv.dir.bg/tv_channel.php?id=${channel.site_id}&dd=${date.format('DD.MM')}` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (!start) return - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ minutes: 30 }) - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const requests = [ - axios.get('https://tv.dir.bg/programata.php?t=0'), - axios.get('https://tv.dir.bg/programata.php?t=1') - ] - - const items = await Promise.all(requests) - .then(r => { - return r - .map(i => { - const html = i.data - const $ = cheerio.load(html) - return $('#programa-left > div > div > div > a').toArray() - }) - .reduce((acc, curr) => { - acc = acc.concat(curr) - return acc - }, []) - }) - .catch(console.log) - - const $ = cheerio.load('') - return items.map(item => { - const $item = $(item) - return { - lang: 'bg', - site_id: $item.attr('href').replace('tv_channel.php?id=', ''), - name: $item.find('div.thumbnail > img').attr('alt') - } - }) - } -} - -function parseStart($item, date) { - const time = $item('i').text() - if (!time) return null - const dateString = `${date.format('MM/DD/YYYY')} ${time}` - - return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH.mm', { zone: 'Europe/Sofia' }).toUTC() -} - -function parseTitle($item) { - return $item - .text() - .replace(/^\d{2}.\d{2}/, '') - .trim() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#events > li').toArray() -} +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') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +let sessionCache = null + +async function getSession(forceRefresh = false) { + if (sessionCache && !forceRefresh) { + return sessionCache + } + + try { + const initResponse = await axios.get('https://tv.dir.bg/init') + + if (!initResponse.data) { + throw new Error('No response data from init endpoint') + } + + // Extract cookies from response headers + const setCookieHeader = initResponse.headers['set-cookie'] + let xsrfToken = null + let dirSessionCookie = null + + if (setCookieHeader) { + setCookieHeader.forEach(cookie => { + // Extract XSRF token from cookie + const xsrfMatch = cookie.match(/XSRF-TOKEN=([^;]+)/) + if (xsrfMatch) { + xsrfToken = decodeURIComponent(xsrfMatch[1]) + } + + // Extract dir_session cookie + const sessionMatch = cookie.match(/dir_session=([^;]+)/) + if (sessionMatch) { + dirSessionCookie = sessionMatch[1] + } + }) + } + + const csrfToken = initResponse.data.csrfToken + + if (!csrfToken) { + throw new Error('No CSRF/XSRF token found in response') + } + + // Build cookie string + let cookieString = '' + if (xsrfToken) { + cookieString += `XSRF-TOKEN=${encodeURIComponent(xsrfToken)}` + } + if (dirSessionCookie) { + if (cookieString) cookieString += '; ' + cookieString += `dir_session=${dirSessionCookie}` + } + + sessionCache = { + csrfToken, + cookieString, + timestamp: Date.now() + } + + return sessionCache + + } catch (error) { + console.error('Error getting session:', error.message) + throw error + } +} + +module.exports = { + site: 'tv.dir.bg', + days: 2, + url: 'https://tv.dir.bg/load/programs', + request: { + maxContentLength: 125000000, // 10 MB + method: 'POST', + async headers() { + try { + const session = await getSession() + return { + 'Cookie': session.cookieString, + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'X-Requested-With': 'XMLHttpRequest' + } + + } catch (error) { + console.error('Error getting headers:', error.message) + throw error + } + }, + async data({ channel, date }) { + try { + const session = await getSession() + + const params = new URLSearchParams() + params.append('_token', session.csrfToken) + params.append('channel', channel.site_id) + params.append('day', date.format('YYYY-MM-DD')) + + return params + + } catch (error) { + console.error('Error preparing request data:', error.message) + throw error + } + }, + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + } + prev.stop = start + } + + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + }, + + async channels() { + try { + const response = await axios.get('https://tv.dir.bg/channels') + const $ = cheerio.load(response.data) + + const channels = [] + + $('.channel_cont').each((_index, element) => { + const $element = $(element) + + const $link = $element.find('a.channel_link') + const href = $link.attr('href') + + const $img = $element.find('img') + const name = $img.attr('alt') + const logo = $img.attr('src') + + const site_id = href ? href.match(/\/programa\/(\d+)/)?.[1] : '' + + if (site_id && name) { + channels.push({ + lang: 'bg', + site_id: site_id, + name: name.trim(), + logo: logo ? (logo.startsWith('http') ? logo : `https://tv.dir.bg${logo}`) : null + }) + } + }) + + return channels + + } catch (error) { + console.error('Error fetching channels:', error.message) + return [] + } + }, + + clearSession() { + sessionCache = null + } +} + +function parseStart($item, date) { + const time = $item('.broadcast-time').text().trim() + const dateString = `${date.format('YYYY-MM-DD')} ${time}` + + return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Europe/Sofia') +} + + +function parseTitle($item) { + return $item('.broadcast-title').text() + .replace(/\s+/g, ' ') + .trim() +} + +function parseItems(content) { + try { + const json = JSON.parse(content) + + if (!json || json.status !== true) { + return [] + } + + const $ = cheerio.load(json.html) + const items = $('.broadcast-item').toArray() + + return items + + } catch (error) { + console.error('❌ Error parsing items:', error.message) + console.error('Error stack:', error.stack) + return [] + } +} \ No newline at end of file diff --git a/sites/tv.dir.bg/tv.dir.bg.test.js b/sites/tv.dir.bg/tv.dir.bg.test.js index 46330544..4f35d3a0 100644 --- a/sites/tv.dir.bg/tv.dir.bg.test.js +++ b/sites/tv.dir.bg/tv.dir.bg.test.js @@ -1,54 +1,50 @@ -const { parser, url } = require('./tv.dir.bg.config.js') -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('2022-01-20', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '12', - xmltv_id: 'BTV.bg' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://tv.dir.bg/tv_channel.php?id=12&dd=20.01') -}) - -it('can parse response', () => { - const content = - '' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-01-20T04:00:00.000Z', - stop: '2022-01-20T13:00:00.000Z', - title: '„Тази сутрин” - информационно предаване с водещи Златимир Йочеви Биляна Гавазова' - }, - { - start: '2022-01-20T13:00:00.000Z', - stop: '2022-01-21T03:30:00.000Z', - title: '„Доктор Чудо” - сериал, еп.71' - }, - { - start: '2022-01-21T03:30:00.000Z', - stop: '2022-01-21T04:00:00.000Z', - title: '„Лице в лице” /п./' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '
      ' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./tv.dir.bg.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-30', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '61', + xmltv_id: 'BTV.bg' +} + +it('can generate valid url', () => { + expect(url).toBe('https://tv.dir.bg/load/programs') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(63) + + expect(results[0]).toMatchObject({ + start: '2025-06-30T03:00:00.000Z', + stop: '2025-06-30T03:30:00.000Z', + title: 'Светът на здравето' + }) + + expect(results[62]).toMatchObject({ + start: '2025-07-01T02:00:00.000Z', + stop: '2025-07-01T02:30:00.000Z', + title: 'Убийства в Рая , сезон 1 , епизод 7' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv.lv/__data__/no_content.json b/sites/tv.lv/__data__/no_content.json new file mode 100644 index 00000000..60a75b51 --- /dev/null +++ b/sites/tv.lv/__data__/no_content.json @@ -0,0 +1 @@ +{"schedule":{"programme":[],"dayName":"Sestdiena","date":"30.11.2024"},"diff":368,"nextDate":"01-12-2024","previousDate":"29-11-2024","current_timestamp":1701194084} \ No newline at end of file diff --git a/sites/tv.lv/tv.lv.config.js b/sites/tv.lv/tv.lv.config.js index c7989235..91677cb9 100644 --- a/sites/tv.lv/tv.lv.config.js +++ b/sites/tv.lv/tv.lv.config.js @@ -1,64 +1,64 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'tv.lv', - days: 2, - url: function ({ date, channel }) { - return `https://www.tv.lv/programme/listing/none/${date.format( - 'DD-MM-YYYY' - )}?filter=channel&subslug=${channel.site_id}` - }, - parser: function ({ content }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const start = parseStart(item) - const stop = parseStop(item) - programs.push({ - title: item.title, - description: item.description_long, - category: item.categorystring, - image: item.image, - start, - stop - }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - const groups = await axios - .get('https://www.tv.lv/data/channels/lvall') - .then(r => r.data) - .catch(console.log) - - let channels = [] - - groups.forEach(group => { - group.channels.forEach(item => { - channels.push({ - lang: 'lv', - site_id: item.slug, - name: item.name - }) - }) - }) - - return channels - } -} - -function parseStart(item) { - return item.start_unix ? dayjs.unix(item.start_unix) : null -} - -function parseStop(item) { - return item.stop_unix ? dayjs.unix(item.stop_unix) : null -} - -function parseItems(content) { - const data = JSON.parse(content) - - return data.schedule.programme || [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'tv.lv', + days: 2, + url: function ({ date, channel }) { + return `https://www.tv.lv/programme/listing/none/${date.format( + 'DD-MM-YYYY' + )}?filter=channel&subslug=${channel.site_id}` + }, + parser: function ({ content }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const start = parseStart(item) + const stop = parseStop(item) + programs.push({ + title: item.title, + description: item.description_long, + category: item.categorystring, + image: item.image, + start, + stop + }) + }) + + return programs + }, + async channels() { + const axios = require('axios') + const groups = await axios + .get('https://www.tv.lv/data/channels/lvall') + .then(r => r.data) + .catch(console.log) + + let channels = [] + + groups.forEach(group => { + group.channels.forEach(item => { + channels.push({ + lang: 'lv', + site_id: item.slug, + name: item.name + }) + }) + }) + + return channels + } +} + +function parseStart(item) { + return item.start_unix ? dayjs.unix(item.start_unix) : null +} + +function parseStop(item) { + return item.stop_unix ? dayjs.unix(item.stop_unix) : null +} + +function parseItems(content) { + const data = JSON.parse(content) + + return data.schedule.programme || [] +} diff --git a/sites/tv.lv/tv.lv.test.js b/sites/tv.lv/tv.lv.test.js index e406789a..57e4cd74 100644 --- a/sites/tv.lv/tv.lv.test.js +++ b/sites/tv.lv/tv.lv.test.js @@ -1,55 +1,54 @@ -const { parser, url } = require('./tv.lv.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('2023-11-30', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'ltv1', - xmltv_id: 'LTV1.lv' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.tv.lv/programme/listing/none/30-11-2023?filter=channel&subslug=ltv1' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(40) - - expect(results[0]).toMatchObject({ - start: '2023-11-29T22:05:00.000Z', - stop: '2023-11-29T22:35:00.000Z', - title: 'Ielas garumā. Pārdaugavas koka arhitektūra', - description: '', - category: '' - }) - - expect(results[39]).toMatchObject({ - start: '2023-11-30T21:30:00.000Z', - stop: '2023-11-30T22:30:00.000Z', - title: 'Latvijas Sirdsdziesma', - description: '', - category: '' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: - '{"schedule":{"programme":[],"dayName":"Sestdiena","date":"30.11.2024"},"diff":368,"nextDate":"01-12-2024","previousDate":"29-11-2024","current_timestamp":1701194084}' - }) - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./tv.lv.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('2023-11-30', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'ltv1', + xmltv_id: 'LTV1.lv' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.tv.lv/programme/listing/none/30-11-2023?filter=channel&subslug=ltv1' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(40) + + expect(results[0]).toMatchObject({ + start: '2023-11-29T22:05:00.000Z', + stop: '2023-11-29T22:35:00.000Z', + title: 'Ielas garumā. Pārdaugavas koka arhitektūra', + description: '', + category: '' + }) + + expect(results[39]).toMatchObject({ + start: '2023-11-30T21:30:00.000Z', + stop: '2023-11-30T22:30:00.000Z', + title: 'Latvijas Sirdsdziesma', + description: '', + category: '' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + }) + expect(results).toMatchObject([]) +}) diff --git a/sites/tv.magenta.at/tv.magenta.at.config.js b/sites/tv.magenta.at/tv.magenta.at.config.js index b56c02e9..a9bcbb72 100644 --- a/sites/tv.magenta.at/tv.magenta.at.config.js +++ b/sites/tv.magenta.at/tv.magenta.at.config.js @@ -1,147 +1,147 @@ -const axios = require('axios') -const crypto = require('crypto') -const dayjs = require('dayjs') - -const API_ENDPOINT = 'https://tv-at-prod.yo-digital.com/at-bifrost' - -const headers = { - 'Device-Id': crypto.randomUUID(), - app_key: 'CTnKA63ruKM0JM1doxAXwwyQLLmQiEiy', - app_version: '02.0.830', - 'X-User-Agent': 'web|web|Firefox-120|02.0.830|1', - 'x-request-tracking-id': crypto.randomUUID() -} - -module.exports = { - site: 'tv.magenta.at', - days: 2, - request: { - headers, - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: function ({ channel, date }) { - return `${API_ENDPOINT}/epg/channel/schedules/v2?station_ids=${ - channel.site_id - }&date=${date.format('YYYY-MM-DD')}&hour_offset=${date.format('H')}&hour_range=3&natco_code=at` - }, - async parser({ content, channel, date }) { - let programs = [] - if (!content) return programs - - let items = parseItems(JSON.parse(content), channel) - if (!items.length) return programs - - const promises = [3, 6, 9, 12, 15, 18, 21].map(i => - axios.get( - `${API_ENDPOINT}/epg/channel/schedules/v2?station_ids=${channel.site_id}&date=${date.format( - 'YYYY-MM-DD' - )}&hour_offset=${i}&hour_range=3&natco_code=at`, - { headers } - ) - ) - - await Promise.allSettled(promises) - .then(results => { - results.forEach(r => { - if (r.status === 'fulfilled') { - const parsed = parseItems(r.value.data, channel) - - items = items.concat(parsed) - } - }) - }) - .catch(console.error) - - for (let item of items) { - const detail = await loadProgramDetails(item) - programs.push({ - title: item.description, - description: parseDescription(detail), - date: parseDate(item), - category: parseCategory(item), - image: detail.poster_image_url, - actors: parseRoles(detail, 'Schauspieler'), - directors: parseRoles(detail, 'Regisseur'), - producers: parseRoles(detail, 'Produzent'), - season: parseSeason(item), - episode: parseEpisode(item), - start: parseStart(item), - stop: parseStop(item) - }) - } - - return programs - }, - async channels() { - const data = await axios - .get(`${API_ENDPOINT}/epg/channel?natco_code=at`, { headers }) - .then(r => r.data) - .catch(console.log) - - return data.channels.map(item => { - return { - lang: 'de', - site_id: item.station_id, - name: item.title - } - }) - } -} - -async function loadProgramDetails(item) { - if (!item.program_id) return {} - const url = `${API_ENDPOINT}/details/series/${item.program_id}?natco_code=at` - const data = await axios - .get(url, { headers }) - .then(r => r.data) - .catch(console.log) - - return data || {} -} - -function parseDate(item) { - return item && item.release_year ? item.release_year.toString() : null -} - -function parseStart(item) { - return dayjs(item.start_time) -} - -function parseStop(item) { - return dayjs(item.end_time) -} - -function parseItems(data, channel) { - if (!data || !data.channels) return [] - const channelData = data.channels[channel.site_id] - if (!channelData) return [] - return channelData -} - -function parseCategory(item) { - if (!item.genres) return null - return item.genres.map(genre => genre.id) -} - -function parseSeason(item) { - if (item.season_display_number === 'Folgen') return null - return item.season_number -} - -function parseEpisode(item) { - if (item.episode_number) return parseInt(item.episode_number) - if (item.season_display_number === 'Folgen') return item.season_number - return null -} - -function parseDescription(item) { - if (!item.details) return null - return item.details.description -} - -function parseRoles(item, role_name) { - if (!item.roles) return null - return item.roles.filter(role => role.role_name === role_name).map(role => role.person_name) -} +const axios = require('axios') +const crypto = require('crypto') +const dayjs = require('dayjs') + +const API_ENDPOINT = 'https://tv-at-prod.yo-digital.com/at-bifrost' + +const headers = { + 'Device-Id': crypto.randomUUID(), + app_key: 'CTnKA63ruKM0JM1doxAXwwyQLLmQiEiy', + app_version: '02.0.1260', + 'X-User-Agent': 'web|web|Firefox-120|02.0.1260|1', + 'x-request-tracking-id': crypto.randomUUID() +} + +module.exports = { + site: 'tv.magenta.at', + days: 2, + request: { + headers, + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: function ({ channel, date }) { + return `${API_ENDPOINT}/epg/channel/schedules/v2?station_ids=${ + channel.site_id + }&date=${date.format('YYYY-MM-DD')}&hour_offset=${date.format('H')}&hour_range=3&natco_code=at` + }, + async parser({ content, channel, date }) { + let programs = [] + if (!content) return programs + + let items = parseItems(JSON.parse(content), channel) + if (!items.length) return programs + + const promises = [3, 6, 9, 12, 15, 18, 21].map(i => + axios.get( + `${API_ENDPOINT}/epg/channel/schedules/v2?station_ids=${channel.site_id}&date=${date.format( + 'YYYY-MM-DD' + )}&hour_offset=${i}&hour_range=3&natco_code=at`, + { headers } + ) + ) + + await Promise.allSettled(promises) + .then(results => { + results.forEach(r => { + if (r.status === 'fulfilled') { + const parsed = parseItems(r.value.data, channel) + + items = items.concat(parsed) + } + }) + }) + .catch(console.error) + + for (let item of items) { + const detail = await loadProgramDetails(item) + programs.push({ + title: item.description, + description: parseDescription(detail), + date: parseDate(item), + category: parseCategory(item), + image: detail.poster_image_url, + actors: parseRoles(detail, 'Schauspieler'), + directors: parseRoles(detail, 'Regisseur'), + producers: parseRoles(detail, 'Produzent'), + season: parseSeason(item), + episode: parseEpisode(item), + start: parseStart(item), + stop: parseStop(item) + }) + } + + return programs + }, + async channels() { + const data = await axios + .get(`${API_ENDPOINT}/epg/channel?natco_code=at`, { headers }) + .then(r => r.data) + .catch(console.log) + + return data.channels.map(item => { + return { + lang: 'de', + site_id: item.station_id, + name: item.title + } + }) + } +} + +async function loadProgramDetails(item) { + if (!item.program_id) return {} + const url = `${API_ENDPOINT}/details/series/${item.program_id}?natco_code=at` + const data = await axios + .get(url, { headers }) + .then(r => r.data) + .catch(console.log) + + return data || {} +} + +function parseDate(item) { + return item && item.release_year ? item.release_year.toString() : null +} + +function parseStart(item) { + return dayjs(item.start_time) +} + +function parseStop(item) { + return dayjs(item.end_time) +} + +function parseItems(data, channel) { + if (!data || !data.channels) return [] + const channelData = data.channels[channel.site_id] + if (!channelData) return [] + return channelData +} + +function parseCategory(item) { + if (!item.genres) return null + return item.genres.map(genre => genre.id) +} + +function parseSeason(item) { + if (item.season_display_number === 'Folgen') return null + return item.season_number +} + +function parseEpisode(item) { + if (item.episode_number) return parseInt(item.episode_number) + if (item.season_display_number === 'Folgen') return item.season_number + return null +} + +function parseDescription(item) { + if (!item.details) return null + return item.details.description +} + +function parseRoles(item, role_name) { + if (!item.roles) return null + return item.roles.filter(role => role.role_name === role_name).map(role => role.person_name) +} diff --git a/sites/tv.magenta.at/tv.magenta.at.test.js b/sites/tv.magenta.at/tv.magenta.at.test.js index 66f858ba..43824084 100644 --- a/sites/tv.magenta.at/tv.magenta.at.test.js +++ b/sites/tv.magenta.at/tv.magenta.at.test.js @@ -1,135 +1,135 @@ -const { parser, url } = require('./tv.magenta.at.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) - -const API_ENDPOINT = 'https://tv-at-prod.yo-digital.com/at-bifrost' - -jest.mock('axios') - -const date = dayjs.utc('2022-10-30', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '206969383991', - xmltv_id: '13thStreet.de', - lang: 'de' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - `${API_ENDPOINT}/epg/channel/schedules/v2?station_ids=206969383991&date=2022-10-30&hour_offset=0&hour_range=3&natco_code=at` - ) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0000.json')) - - axios.get.mockImplementation(url => { - if ( - url === - `${API_ENDPOINT}/epg/channel/schedules/v2?date=2023-11-15&hour_offset=0&hour_range=3&station_ids=206969383991&natco_code=at` - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_0300.json')) - }) - } else if ( - url === - `${API_ENDPOINT}/epg/channel/schedules/v2?date=2023-11-15&hour_offset=3&hour_range=3&station_ids=206969383991&natco_code=at` - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_0600.json')) - }) - } else if ( - url === - `${API_ENDPOINT}/epg/channel/schedules/v2?date=2023-11-15&hour_offset=6&hour_range=3&station_ids=206969383991&natco_code=at` - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_0900.json')) - }) - } else if ( - url === - `${API_ENDPOINT}/epg/channel/schedules/v2?date=2023-11-15&hour_offset=9&hour_range=3&station_ids=206969383991&natco_code=at` - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1200.json')) - }) - } else if ( - url === - `${API_ENDPOINT}/epg/channel/schedules/v2?date=2023-11-15&hour_offset=12&hour_range=3&station_ids=206969383991&natco_code=at` - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1500.json')) - }) - } else if ( - url === - `${API_ENDPOINT}/epg/channel/schedules/v2?date=2023-11-15&hour_offset=15&hour_range=3&station_ids=206969383991&natco_code=at` - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1800.json')) - }) - } else if ( - url === - `${API_ENDPOINT}/epg/channel/schedules/v2?date=2023-11-15&hour_offset=18&hour_range=3&station_ids=206969383991&natco_code=at` - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_2100.json')) - }) - } else if ( - url === `${API_ENDPOINT}/details/series/gn.tv-24101298-EP048489190016?natco_code=at` - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-11-14T23:20:00.000Z', - stop: '2023-11-15T00:05:00.000Z', - title: 'So Help Me Todd', - description: - 'Ava ist 17 und eine geniale Hackerin. Jetzt steht die Teenagerin vor Gericht, weil sie sich illegal Zugang zum Verteidigungsministerium verschafft hat. Todd soll das IT-Genie überwachen.', - date: '2023', - category: ['Kriminaldrama'], - actors: [ - 'Marcia Gay Harden', - 'Skylar Astin', - 'Madeline Wise', - 'Tristen J. Winger', - 'Inga Schlingmann', - 'Rosa Evangelina Arredondo', - 'Laila Robins' - ], - directors: ['Jay Karas'], - producers: [ - 'Scott Prendergast', - 'Liz Kruger', - 'Elizabeth Klaviter', - 'Dr. Phil McGraw', - 'Jay McGraw', - 'Julia Eisenman', - 'Amy York Rubin' - ], - season: 1, - episode: 15 - }) -}) - -it('can handle empty guide', async () => { - let results = await parser({ content: '', channel, date }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./tv.magenta.at.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) + +const API_ENDPOINT = 'https://tv-at-prod.yo-digital.com/at-bifrost' + +jest.mock('axios') + +const date = dayjs.utc('2022-10-30', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '206969383991', + xmltv_id: '13thStreet.de', + lang: 'de' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + `${API_ENDPOINT}/epg/channel/schedules/v2?station_ids=206969383991&date=2022-10-30&hour_offset=0&hour_range=3&natco_code=at` + ) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0000.json')) + + axios.get.mockImplementation(url => { + if ( + url === + `${API_ENDPOINT}/epg/channel/schedules/v2?date=2023-11-15&hour_offset=0&hour_range=3&station_ids=206969383991&natco_code=at` + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_0300.json')) + }) + } else if ( + url === + `${API_ENDPOINT}/epg/channel/schedules/v2?date=2023-11-15&hour_offset=3&hour_range=3&station_ids=206969383991&natco_code=at` + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_0600.json')) + }) + } else if ( + url === + `${API_ENDPOINT}/epg/channel/schedules/v2?date=2023-11-15&hour_offset=6&hour_range=3&station_ids=206969383991&natco_code=at` + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_0900.json')) + }) + } else if ( + url === + `${API_ENDPOINT}/epg/channel/schedules/v2?date=2023-11-15&hour_offset=9&hour_range=3&station_ids=206969383991&natco_code=at` + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1200.json')) + }) + } else if ( + url === + `${API_ENDPOINT}/epg/channel/schedules/v2?date=2023-11-15&hour_offset=12&hour_range=3&station_ids=206969383991&natco_code=at` + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1500.json')) + }) + } else if ( + url === + `${API_ENDPOINT}/epg/channel/schedules/v2?date=2023-11-15&hour_offset=15&hour_range=3&station_ids=206969383991&natco_code=at` + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1800.json')) + }) + } else if ( + url === + `${API_ENDPOINT}/epg/channel/schedules/v2?date=2023-11-15&hour_offset=18&hour_range=3&station_ids=206969383991&natco_code=at` + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_2100.json')) + }) + } else if ( + url === `${API_ENDPOINT}/details/series/gn.tv-24101298-EP048489190016?natco_code=at` + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-11-14T23:20:00.000Z', + stop: '2023-11-15T00:05:00.000Z', + title: 'So Help Me Todd', + description: + 'Ava ist 17 und eine geniale Hackerin. Jetzt steht die Teenagerin vor Gericht, weil sie sich illegal Zugang zum Verteidigungsministerium verschafft hat. Todd soll das IT-Genie überwachen.', + date: '2023', + category: ['Kriminaldrama'], + actors: [ + 'Marcia Gay Harden', + 'Skylar Astin', + 'Madeline Wise', + 'Tristen J. Winger', + 'Inga Schlingmann', + 'Rosa Evangelina Arredondo', + 'Laila Robins' + ], + directors: ['Jay Karas'], + producers: [ + 'Scott Prendergast', + 'Liz Kruger', + 'Elizabeth Klaviter', + 'Dr. Phil McGraw', + 'Jay McGraw', + 'Julia Eisenman', + 'Amy York Rubin' + ], + season: 1, + episode: 15 + }) +}) + +it('can handle empty guide', async () => { + let results = await parser({ content: '', channel, date }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/tv.mail.ru/__data__/content.json b/sites/tv.mail.ru/__data__/content.json new file mode 100644 index 00000000..616c52ba --- /dev/null +++ b/sites/tv.mail.ru/__data__/content.json @@ -0,0 +1,65 @@ +{ + "status":"OK", + "schedule":[ + { + "channel":{ + "name":"21TV", + "pic_url":"https://resizer.mail.ru/p/1234c5ac-c19c-5cf2-9c6a-fc0efca920ac/AAACm2w9aDpGPSWXzsH7PBq2X3I6pbxqmrj-yeuVppAKyyBHXE_dH_7pHQ2rOavyKiC4iHIWTab9SeKo7pKgr71lqVA.png", + "pic_url_128":"https://resizer.mail.ru/p/1234c5ac-c19c-5cf2-9c6a-fc0efca920ac/AAACwjJ45j9sTP8fcjPJnJ4xk5e_ILr5iXwjLMhWhzlVnIJkrtT42vEp9walcgpXRKDq9KFoliEPR0xI-LEh96C_izY.png", + "pic_url_64":"https://resizer.mail.ru/p/1234c5ac-c19c-5cf2-9c6a-fc0efca920ac/dpr:200/AAACm2w9aDpGPSWXzsH7PBq2X3I6pbxqmrj-yeuVppAKyyBHXE_dH_7pHQ2rOavyKiC4iHIWTab9SeKo7pKgr71lqVA.png" + }, + "event":{ + "current":[ + { + "channel_id":"2785", + "name":"Պրոֆեսիոնալները", + "category_id":8, + "episode_title":"", + "url":"/moskva/channel/2785/173593246/", + "id":"173593246", + "start":"02:40", + "episode_num":0 + }, + { + "channel_id":"2785", + "name":"Նոնստոպ․ Տեսահոլովակներ", + "category_id":23, + "episode_title":"", + "url":"/moskva/channel/2785/173593142/", + "id":"173593142", + "start":"03:25", + "episode_num":0 + } + ], + "past":[ + { + "channel_id":"2785", + "name":"Նոնստոպ․ Տեսահոլովակներ", + "category_id":23, + "episode_title":"", + "url":"/moskva/channel/2785/173593328/", + "id":"173593328", + "start":"23:35", + "episode_num":0 + }, + { + "channel_id":"2785", + "video":{ + "currency":"RUB", + "price_min":"249.00", + "price_txt":"249 р." + }, + "name":"Վերջին թագավորությունը", + "category_id":2, + "episode_title":"", + "url":"/moskva/channel/2785/173593318/", + "id":"173593318", + "start":"01:40", + "our_event_id":"890224", + "episode_num":0 + } + ] + } + } + ] +} \ No newline at end of file diff --git a/sites/tv.mail.ru/__data__/no_content.json b/sites/tv.mail.ru/__data__/no_content.json new file mode 100644 index 00000000..67fcc17b --- /dev/null +++ b/sites/tv.mail.ru/__data__/no_content.json @@ -0,0 +1,23 @@ +{ + "status":"OK", + "current_ts":1637788593, + "form":{ + "values":[ + + ] + }, + "current_offset":10800, + "schedule":[ + { + "channel":null, + "event":{ + "current":[ + + ], + "past":[ + + ] + } + } + ] +} \ No newline at end of file diff --git a/sites/tv.mail.ru/tv.mail.ru.config.js b/sites/tv.mail.ru/tv.mail.ru.config.js index 92c19623..ec046c40 100644 --- a/sites/tv.mail.ru/tv.mail.ru.config.js +++ b/sites/tv.mail.ru/tv.mail.ru.config.js @@ -1,123 +1,122 @@ -const { DateTime } = require('luxon') -const axios = require('axios') - -module.exports = { - site: 'tv.mail.ru', - days: 2, - delay: 1000, - url({ channel, date }) { - return `https://tv.mail.ru/ajax/channel/?region_id=70&channel_id=${ - channel.site_id - }&date=${date.format('YYYY-MM-DD')}` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ hours: 1 }) - programs.push({ - title: item.name, - category: parseCategory(item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const _ = require('lodash') - - const regions = [5506, 1096, 1125, 285] - - let channels = [] - for (let region of regions) { - const totalPages = await getTotalPageCount(region) - const pages = Array.from(Array(totalPages).keys()) - for (let page of pages) { - const data = await axios - .get('https://tv.mail.ru/ajax/channel/list/', { - params: { page }, - headers: { - cookie: `s=fver=0|geo=${region};` - } - }) - .then(r => r.data) - .catch(console.log) - - data.channels.forEach(item => { - channels.push({ - lang: 'ru', - name: item.name, - site_id: item.id - }) - }) - } - } - - return _.uniqBy(channels, 'site_id') - } -} - -async function getTotalPageCount(region) { - const data = await axios - .get('https://tv.mail.ru/ajax/channel/list/', { - params: { page: 0 }, - headers: { - cookie: `s=fver=0|geo=${region};` - } - }) - .then(r => r.data) - .catch(console.log) - - return data.total -} - -function parseStart(item, date) { - const dateString = `${date.format('YYYY-MM-DD')} ${item.start}` - - return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Moscow' }).toUTC() -} - -function parseCategory(item) { - const categories = { - 1: 'Фильм', - 2: 'Сериал', - 6: 'Документальное', - 7: 'Телемагазин', - 8: 'Позновательное', - 10: 'Другое', - 14: 'ТВ-шоу', - 16: 'Досуг,Хобби', - 17: 'Ток-шоу', - 18: 'Юмористическое', - 23: 'Музыка', - 24: 'Развлекательное', - 25: 'Игровое', - 26: 'Новости' - } - - return categories[item.category_id] - ? { - lang: 'ru', - value: categories[item.category_id] - } - : null -} - -function parseItems(content) { - const json = JSON.parse(content) - if (!Array.isArray(json.schedule) || !json.schedule[0]) return [] - const event = json.schedule[0].event || [] - - return [...event.past, ...event.current] -} +const { DateTime } = require('luxon') +const axios = require('axios') +const uniqBy = require('lodash.uniqby') + +module.exports = { + site: 'tv.mail.ru', + days: 2, + delay: 1000, + url({ channel, date }) { + return `https://tv.mail.ru/ajax/channel/?region_id=70&channel_id=${ + channel.site_id + }&date=${date.format('YYYY-MM-DD')}` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ hours: 1 }) + programs.push({ + title: item.name, + category: parseCategory(item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const regions = [5506, 1096, 1125, 285] + + let channels = [] + for (let region of regions) { + const totalPages = await getTotalPageCount(region) + const pages = Array.from(Array(totalPages).keys()) + for (let page of pages) { + const data = await axios + .get('https://tv.mail.ru/ajax/channel/list/', { + params: { page }, + headers: { + cookie: `s=fver=0|geo=${region};` + } + }) + .then(r => r.data) + .catch(console.log) + + data.channels.forEach(item => { + channels.push({ + lang: 'ru', + name: item.name, + site_id: item.id + }) + }) + } + } + + return uniqBy(channels, 'site_id') + } +} + +async function getTotalPageCount(region) { + const data = await axios + .get('https://tv.mail.ru/ajax/channel/list/', { + params: { page: 0 }, + headers: { + cookie: `s=fver=0|geo=${region};` + } + }) + .then(r => r.data) + .catch(console.log) + + return data.total +} + +function parseStart(item, date) { + const dateString = `${date.format('YYYY-MM-DD')} ${item.start}` + + return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Moscow' }).toUTC() +} + +function parseCategory(item) { + const categories = { + 1: 'Фильм', + 2: 'Сериал', + 6: 'Документальное', + 7: 'Телемагазин', + 8: 'Позновательное', + 10: 'Другое', + 14: 'ТВ-шоу', + 16: 'Досуг,Хобби', + 17: 'Ток-шоу', + 18: 'Юмористическое', + 23: 'Музыка', + 24: 'Развлекательное', + 25: 'Игровое', + 26: 'Новости' + } + + return categories[item.category_id] + ? { + lang: 'ru', + value: categories[item.category_id] + } + : null +} + +function parseItems(content) { + const json = JSON.parse(content) + if (!Array.isArray(json.schedule) || !json.schedule[0]) return [] + const event = json.schedule[0].event || [] + + return [...event.past, ...event.current] +} diff --git a/sites/tv.mail.ru/tv.mail.ru.test.js b/sites/tv.mail.ru/tv.mail.ru.test.js index d40d3aff..f2fa2c96 100644 --- a/sites/tv.mail.ru/tv.mail.ru.test.js +++ b/sites/tv.mail.ru/tv.mail.ru.test.js @@ -1,77 +1,77 @@ -const { parser, url } = require('./tv.mail.ru.config.js') -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('2021-11-24', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2785', - xmltv_id: '21TV.am' -} -const content = - '{"status":"OK","schedule":[{"channel":{"name":"21TV","pic_url":"https://resizer.mail.ru/p/1234c5ac-c19c-5cf2-9c6a-fc0efca920ac/AAACm2w9aDpGPSWXzsH7PBq2X3I6pbxqmrj-yeuVppAKyyBHXE_dH_7pHQ2rOavyKiC4iHIWTab9SeKo7pKgr71lqVA.png","pic_url_128":"https://resizer.mail.ru/p/1234c5ac-c19c-5cf2-9c6a-fc0efca920ac/AAACwjJ45j9sTP8fcjPJnJ4xk5e_ILr5iXwjLMhWhzlVnIJkrtT42vEp9walcgpXRKDq9KFoliEPR0xI-LEh96C_izY.png","pic_url_64":"https://resizer.mail.ru/p/1234c5ac-c19c-5cf2-9c6a-fc0efca920ac/dpr:200/AAACm2w9aDpGPSWXzsH7PBq2X3I6pbxqmrj-yeuVppAKyyBHXE_dH_7pHQ2rOavyKiC4iHIWTab9SeKo7pKgr71lqVA.png"},"event":{"current":[{"channel_id":"2785","name":"Պրոֆեսիոնալները","category_id":8,"episode_title":"","url":"/moskva/channel/2785/173593246/","id":"173593246","start":"02:40","episode_num":0},{"channel_id":"2785","name":"Նոնստոպ․ Տեսահոլովակներ","category_id":23,"episode_title":"","url":"/moskva/channel/2785/173593142/","id":"173593142","start":"03:25","episode_num":0}],"past":[{"channel_id":"2785","name":"Նոնստոպ․ Տեսահոլովակներ","category_id":23,"episode_title":"","url":"/moskva/channel/2785/173593328/","id":"173593328","start":"23:35","episode_num":0},{"channel_id":"2785","video":{"currency":"RUB","price_min":"249.00","price_txt":"249 р."},"name":"Վերջին թագավորությունը","category_id":2,"episode_title":"","url":"/moskva/channel/2785/173593318/","id":"173593318","start":"01:40","our_event_id":"890224","episode_num":0}]}}]}' - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://tv.mail.ru/ajax/channel/?region_id=70&channel_id=2785&date=2021-11-24' - ) -}) - -it('can parse response', () => { - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-24T20:35:00.000Z', - stop: '2021-11-24T22:40:00.000Z', - title: 'Նոնստոպ․ Տեսահոլովակներ', - category: { - lang: 'ru', - value: 'Музыка' - } - }, - { - start: '2021-11-24T22:40:00.000Z', - stop: '2021-11-24T23:40:00.000Z', - title: 'Վերջին թագավորությունը', - category: { - lang: 'ru', - value: 'Сериал' - } - }, - { - start: '2021-11-24T23:40:00.000Z', - stop: '2021-11-25T00:25:00.000Z', - title: 'Պրոֆեսիոնալները', - category: { - lang: 'ru', - value: 'Позновательное' - } - }, - { - start: '2021-11-25T00:25:00.000Z', - stop: '2021-11-25T01:25:00.000Z', - title: 'Նոնստոպ․ Տեսահոլովակներ', - category: { - lang: 'ru', - value: 'Музыка' - } - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '{"status":"OK","current_ts":1637788593,"form":{"values":[]},"current_offset":10800,"schedule":[{"channel":null,"event":{"current":[],"past":[]}}]}' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./tv.mail.ru.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const fs = require('fs') +const path = require('path') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2785', + xmltv_id: '21TV.am' +} +const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.json'), 'utf8') + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://tv.mail.ru/ajax/channel/?region_id=70&channel_id=2785&date=2021-11-24' + ) +}) + +it('can parse response', () => { + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-24T20:35:00.000Z', + stop: '2021-11-24T22:40:00.000Z', + title: 'Նոնստոպ․ Տեսահոլովակներ', + category: { + lang: 'ru', + value: 'Музыка' + } + }, + { + start: '2021-11-24T22:40:00.000Z', + stop: '2021-11-24T23:40:00.000Z', + title: 'Վերջին թագավորությունը', + category: { + lang: 'ru', + value: 'Сериал' + } + }, + { + start: '2021-11-24T23:40:00.000Z', + stop: '2021-11-25T00:25:00.000Z', + title: 'Պրոֆեսիոնալները', + category: { + lang: 'ru', + value: 'Позновательное' + } + }, + { + start: '2021-11-25T00:25:00.000Z', + stop: '2021-11-25T01:25:00.000Z', + title: 'Նոնստոպ․ Տեսահոլովակներ', + category: { + lang: 'ru', + value: 'Музыка' + } + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: fs.readFileSync(path.join(__dirname, '__data__', 'no_content.json'), 'utf8') + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv.movistar.com.pe/tv.movistar.com.pe.config.js b/sites/tv.movistar.com.pe/tv.movistar.com.pe.config.js index 47aee387..78f3931e 100644 --- a/sites/tv.movistar.com.pe/tv.movistar.com.pe.config.js +++ b/sites/tv.movistar.com.pe/tv.movistar.com.pe.config.js @@ -1,57 +1,57 @@ -const dayjs = require('dayjs') -const axios = require('axios') - -module.exports = { - site: 'tv.movistar.com.pe', - days: 2, - url({ channel, date }) { - return `https://contentapi-pe.cdn.telefonica.com/28/default/es-PE/schedules?fields=Pid,Title,Description,ChannelName,LiveChannelPid,Start,End,images.videoFrame,AgeRatingPid&orderBy=START_TIME%3Aa&filteravailability=false&starttime=${date.unix()}&endtime=${date - .add(1, 'd') - .unix()}&livechannelpids=${channel.site_id}` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.Title, - description: item.Description, - image: parseImage(item), - start: parseTime(item.Start), - stop: parseTime(item.End) - }) - }) - - return programs - }, - async channels() { - const items = await axios - .get( - 'https://contentapi-pe.cdn.telefonica.com/28/default/es-PE/contents/all?contentTypes=LCH&fields=Pid,Name&orderBy=contentOrder&limit=1000' - ) - .then(r => r.data.Content.List) - .catch(console.error) - - return items.map(i => { - return { - lang: 'es', - name: i.Name, - site_id: i.Pid.toLowerCase() - } - }) - } -} - -function parseImage(item) { - return item.Images?.VideoFrame?.[0]?.Url -} - -function parseTime(timestamp) { - return dayjs.unix(timestamp) -} - -function parseItems(content) { - const data = JSON.parse(content) - - return data.Content || [] -} +const dayjs = require('dayjs') +const axios = require('axios') + +module.exports = { + site: 'tv.movistar.com.pe', + days: 2, + url({ channel, date }) { + return `https://contentapi-pe.cdn.telefonica.com/28/default/es-PE/schedules?fields=Pid,Title,Description,ChannelName,LiveChannelPid,Start,End,images.videoFrame,AgeRatingPid&orderBy=START_TIME%3Aa&filteravailability=false&starttime=${date.unix()}&endtime=${date + .add(1, 'd') + .unix()}&livechannelpids=${channel.site_id}` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.Title, + description: item.Description, + image: parseImage(item), + start: parseTime(item.Start), + stop: parseTime(item.End) + }) + }) + + return programs + }, + async channels() { + const items = await axios + .get( + 'https://contentapi-pe.cdn.telefonica.com/28/default/es-PE/contents/all?contentTypes=LCH&fields=Pid,Name&orderBy=contentOrder&limit=1000' + ) + .then(r => r.data.Content.List) + .catch(console.error) + + return items.map(i => { + return { + lang: 'es', + name: i.Name, + site_id: i.Pid.toLowerCase() + } + }) + } +} + +function parseImage(item) { + return item.Images?.VideoFrame?.[0]?.Url +} + +function parseTime(timestamp) { + return dayjs.unix(timestamp) +} + +function parseItems(content) { + const data = JSON.parse(content) + + return data.Content || [] +} diff --git a/sites/tv.movistar.com.pe/tv.movistar.com.pe.test.js b/sites/tv.movistar.com.pe/tv.movistar.com.pe.test.js index 06f2aa9a..426eda97 100644 --- a/sites/tv.movistar.com.pe/tv.movistar.com.pe.test.js +++ b/sites/tv.movistar.com.pe/tv.movistar.com.pe.test.js @@ -1,45 +1,45 @@ -const { parser, url } = require('./tv.movistar.com.pe.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('2022-11-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'lch2219', - xmltv_id: 'WillaxTV.pe' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://contentapi-pe.cdn.telefonica.com/28/default/es-PE/schedules?fields=Pid,Title,Description,ChannelName,LiveChannelPid,Start,End,images.videoFrame,AgeRatingPid&orderBy=START_TIME%3Aa&filteravailability=false&starttime=1669680000&endtime=1669766400&livechannelpids=lch2219' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-28T23:50:00.000Z', - stop: '2022-11-29T00:50:00.000Z', - title: 'Willax noticias edición central', - description: - 'Edición central con el desarrollo y cobertura noticiosa de todos los acontecimientos nacionales e internacionales.', - image: - 'http://media.gvp.telefonica.com/storagearea0/IMAGES/00/13/00/13003906_281B2DAB18B01955.jpg' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const result = parser({ content, channel, date }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./tv.movistar.com.pe.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('2022-11-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'lch2219', + xmltv_id: 'WillaxTV.pe' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://contentapi-pe.cdn.telefonica.com/28/default/es-PE/schedules?fields=Pid,Title,Description,ChannelName,LiveChannelPid,Start,End,images.videoFrame,AgeRatingPid&orderBy=START_TIME%3Aa&filteravailability=false&starttime=1669680000&endtime=1669766400&livechannelpids=lch2219' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-28T23:50:00.000Z', + stop: '2022-11-29T00:50:00.000Z', + title: 'Willax noticias edición central', + description: + 'Edición central con el desarrollo y cobertura noticiosa de todos los acontecimientos nacionales e internacionales.', + image: + 'http://media.gvp.telefonica.com/storagearea0/IMAGES/00/13/00/13003906_281B2DAB18B01955.jpg' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const result = parser({ content, channel, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv.nu/tv.nu.config.js b/sites/tv.nu/tv.nu.config.js index 5dd28959..b514a1dc 100644 --- a/sites/tv.nu/tv.nu.config.js +++ b/sites/tv.nu/tv.nu.config.js @@ -1,87 +1,87 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'tv.nu', - days: 2, - url: function ({ channel, date }) { - return `https://web-api.tv.nu/channels/${channel.site_id}/schedule?date=${date.format( - 'YYYY-MM-DD' - )}&fullDay=true` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.description, - category: Array.isArray(item.genres) ? item.genres.map(genre => genre.name) : null, - season: item.seasonNumber || null, - episode: item.episodeNumber || null, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const channels = [] - const axios = require('axios') - const result = await axios - .get('https://www.tv.nu/alla-kanaler') - .then(response => response.data) - .catch(console.error) - - if (result) { - const [, data] = result.match(/\\"allModules\\":(\[(.*?)\])/i) || [null, null] - const modules = JSON.parse(data.replace(/\\/g, '')) - if (Array.isArray(modules) && modules.length) { - let offset = 0 - while (offset !== undefined) { - const data = await axios - .get('https://web-api.tv.nu/tableauLinearChannels', { - params: { - modules, - date: dayjs().format('YYYY-MM-DD'), - limit: 12, - offset - } - }) - .then(r => r.data) - .catch(console.error) - - data.data.modules.forEach(item => { - channels.push({ - lang: 'sv', - name: item.content.name, - site_id: item.content.slug - }) - }) - offset = data.data.nextOffset - } - } - } - - return channels - } -} - -function parseStart(item) { - if (!item.broadcast || !item.broadcast.startTime) return null - - return dayjs(item.broadcast.startTime) -} - -function parseStop(item) { - if (!item.broadcast || !item.broadcast.endTime) return null - - return dayjs(item.broadcast.endTime) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !data.data || !Array.isArray(data.data.broadcasts)) return [] - - return data.data.broadcasts -} +const dayjs = require('dayjs') + +module.exports = { + site: 'tv.nu', + days: 2, + url: function ({ channel, date }) { + return `https://web-api.tv.nu/channels/${channel.site_id}/schedule?date=${date.format( + 'YYYY-MM-DD' + )}&fullDay=true` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.description, + category: Array.isArray(item.genres) ? item.genres.map(genre => genre.name) : null, + season: item.seasonNumber || null, + episode: item.episodeNumber || null, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const channels = [] + const axios = require('axios') + const result = await axios + .get('https://www.tv.nu/alla-kanaler') + .then(response => response.data) + .catch(console.error) + + if (result) { + const [, data] = result.match(/\\"allModules\\":(\[(.*?)\])/i) || [null, null] + const modules = JSON.parse(data.replace(/\\/g, '')) + if (Array.isArray(modules) && modules.length) { + let offset = 0 + while (offset !== undefined) { + const data = await axios + .get('https://web-api.tv.nu/tableauLinearChannels', { + params: { + modules, + date: dayjs().format('YYYY-MM-DD'), + limit: 12, + offset + } + }) + .then(r => r.data) + .catch(console.error) + + data.data.modules.forEach(item => { + channels.push({ + lang: 'sv', + name: item.content.name, + site_id: item.content.slug + }) + }) + offset = data.data.nextOffset + } + } + } + + return channels + } +} + +function parseStart(item) { + if (!item.broadcast || !item.broadcast.startTime) return null + + return dayjs(item.broadcast.startTime) +} + +function parseStop(item) { + if (!item.broadcast || !item.broadcast.endTime) return null + + return dayjs(item.broadcast.endTime) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !data.data || !Array.isArray(data.data.broadcasts)) return [] + + return data.data.broadcasts +} diff --git a/sites/tv.nu/tv.nu.test.js b/sites/tv.nu/tv.nu.test.js index 8683ce5d..8e37de2c 100644 --- a/sites/tv.nu/tv.nu.test.js +++ b/sites/tv.nu/tv.nu.test.js @@ -1,48 +1,48 @@ -const fs = require('fs') -const path = require('path') -const { parser, url } = require('./tv.nu.config.js') -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('2024-12-03', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '3sat', - xmltv_id: '3sat.de' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://web-api.tv.nu/channels/3sat/schedule?date=2024-12-03&fullDay=true' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.json')) - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2024-12-03T11:50:00.000Z', - stop: '2024-12-03T12:15:00.000Z', - title: 'Natur im Garten', - description: - 'Der Gartenbuchautor Karl Ploberger gibt in der Sendung Tipps und Tricks zur Gartenpflege.', - category: ['Konsument', 'Underhållning', 'Trädgård'], - season: 29, - episode: 9 - } - ]) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.join(__dirname, '__data__', 'no_content.json')) - const result = parser({ content }) - expect(result).toMatchObject([]) -}) +const fs = require('fs') +const path = require('path') +const { parser, url } = require('./tv.nu.config.js') +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('2024-12-03', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '3sat', + xmltv_id: '3sat.de' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://web-api.tv.nu/channels/3sat/schedule?date=2024-12-03&fullDay=true' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.json')) + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2024-12-03T11:50:00.000Z', + stop: '2024-12-03T12:15:00.000Z', + title: 'Natur im Garten', + description: + 'Der Gartenbuchautor Karl Ploberger gibt in der Sendung Tipps und Tricks zur Gartenpflege.', + category: ['Konsument', 'Underhållning', 'Trädgård'], + season: 29, + episode: 9 + } + ]) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.join(__dirname, '__data__', 'no_content.json')) + const result = parser({ content }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv.post.lu/tv.post.lu.config.js b/sites/tv.post.lu/tv.post.lu.config.js index 6d7a49be..fb840e0f 100644 --- a/sites/tv.post.lu/tv.post.lu.config.js +++ b/sites/tv.post.lu/tv.post.lu.config.js @@ -1,56 +1,56 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'tv.post.lu', - days: 2, - url({ channel, date }) { - return `https://tv.post.lu/api/channels?id=${channel.site_id}&date=${date.format('YYYY-MM-DD')}` - }, - parser({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.description, - category: item.program_type, - image: item.image_url, - start: dayjs.unix(item.tsStart), - stop: dayjs.unix(item.tsEnd) - }) - }) - - return programs - }, - async channels() { - const promises = [...Array(17).keys()].map(i => - axios.get(`https://tv.post.lu/api/channels/?page=${i + 1}`) - ) - - const channels = [] - await Promise.all(promises).then(values => { - values.forEach(r => { - let items = r.data.result.data - items.forEach(item => { - channels.push({ - lang: item.language.code, - name: item.name, - site_id: item.id - }) - }) - }) - }) - - return channels - } -} - -function parseItems(content) { - if (!content) return [] - const data = JSON.parse(content) - if (!data || !data.result || !data.result.epg || !Array.isArray(data.result.epg.programme)) - return [] - - return data.result.epg.programme -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'tv.post.lu', + days: 2, + url({ channel, date }) { + return `https://tv.post.lu/api/channels?id=${channel.site_id}&date=${date.format('YYYY-MM-DD')}` + }, + parser({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.description, + category: item.program_type, + image: item.image_url, + start: dayjs.unix(item.tsStart), + stop: dayjs.unix(item.tsEnd) + }) + }) + + return programs + }, + async channels() { + const promises = [...Array(17).keys()].map(i => + axios.get(`https://tv.post.lu/api/channels/?page=${i + 1}`) + ) + + const channels = [] + await Promise.all(promises).then(values => { + values.forEach(r => { + let items = r.data.result.data + items.forEach(item => { + channels.push({ + lang: item.language.code, + name: item.name, + site_id: item.id + }) + }) + }) + }) + + return channels + } +} + +function parseItems(content) { + if (!content) return [] + const data = JSON.parse(content) + if (!data || !data.result || !data.result.epg || !Array.isArray(data.result.epg.programme)) + return [] + + return data.result.epg.programme +} diff --git a/sites/tv.post.lu/tv.post.lu.test.js b/sites/tv.post.lu/tv.post.lu.test.js index a113a2e5..6a0a4a30 100644 --- a/sites/tv.post.lu/tv.post.lu.test.js +++ b/sites/tv.post.lu/tv.post.lu.test.js @@ -1,47 +1,47 @@ -const { parser, url } = require('./tv.post.lu.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('2023-01-16', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '269695d0-8076-11e9-b5ca-f345a2ed0fbe', - xmltv_id: 'DasErste.de' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://tv.post.lu/api/channels?id=269695d0-8076-11e9-b5ca-f345a2ed0fbe&date=2023-01-16' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - title: 'Tagesschau', - description: - 'Das Flaggschiff unter den deutschen Nachrichtensendungen ist gleichzeitig die "dienstälteste" noch bestehende Sendung im deutschen Fernsehen. In bis zu 20 am Tag produzierten Sendungen wird die Komplexität des Weltgeschehens verständlich erklärt und in komprimierter Form über aktuelle politische, wirtschaftliche, soziale, kulturelle, sportliche und sonstige Ereignisse berichtet.', - category: 'Nachrichten', - image: - 'https://mp-photos-cdn.azureedge.net/container3cc71e4948ac40ab803c26e0abc2e3e5/original/e6eb49013a822f5c6eb2e7701e69a1f80aa0b947.jpg', - start: '2023-01-16T00:05:00.000Z', - stop: '2023-01-16T00:10:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ content: '' }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./tv.post.lu.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('2023-01-16', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '269695d0-8076-11e9-b5ca-f345a2ed0fbe', + xmltv_id: 'DasErste.de' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://tv.post.lu/api/channels?id=269695d0-8076-11e9-b5ca-f345a2ed0fbe&date=2023-01-16' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + title: 'Tagesschau', + description: + 'Das Flaggschiff unter den deutschen Nachrichtensendungen ist gleichzeitig die "dienstälteste" noch bestehende Sendung im deutschen Fernsehen. In bis zu 20 am Tag produzierten Sendungen wird die Komplexität des Weltgeschehens verständlich erklärt und in komprimierter Form über aktuelle politische, wirtschaftliche, soziale, kulturelle, sportliche und sonstige Ereignisse berichtet.', + category: 'Nachrichten', + image: + 'https://mp-photos-cdn.azureedge.net/container3cc71e4948ac40ab803c26e0abc2e3e5/original/e6eb49013a822f5c6eb2e7701e69a1f80aa0b947.jpg', + start: '2023-01-16T00:05:00.000Z', + stop: '2023-01-16T00:10:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ content: '' }) + + expect(results).toMatchObject([]) +}) 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/tv.trueid.net/__data__/data.json b/sites/tv.trueid.net/__data__/data.json index dff30854..a66974ae 100644 --- a/sites/tv.trueid.net/__data__/data.json +++ b/sites/tv.trueid.net/__data__/data.json @@ -1 +1,5094 @@ -{"pageProps":{"currentLang":{"country":"th","lang":"en"},"isBotPerformance":false,"titleH1":"Watch Live TV Online 24 hours","metaData":{"title":"ดูทีวีออนไลน์ True Movie Hits - TrueID TV","description":"Watch Live TV Online 24 hours, Thai Drama, Full HD","imageURL":"https://cms.dmpcdn.com/livetv/2023/04/28/45345d10-e599-11ed-86b8-bb40638e3c49_webp_original.png","currentUrl":"https://tv.trueid.net/th-en/live/true-movie-hits","metaTitle":"ดูทีวีออนไลน์ True Movie Hits - TrueID TV"},"channelList":[{"id":"nQlqONGyoa4","thumb":"https://cms.dmpcdn.com/livetv/2023/07/24/5ff3e270-29cc-11ee-b2f4-e9de482d866e_webp_original.webp","slug":"ch3-hd","title":"Channel 3","content_type":"livetv","category":"livetv-ca|digitaltv-ca|entertainment-ca|freetv-ca","content_provider":"","channel_code":"c03","content_rights":null,"channel_info":{"channel_name_cbd":"ប៉ុស្តិ៍ 3 HD","channel_name_eng":"CH3 HD","channel_name_mm":"CH3 HD","channel_name_th":"ช่อง 3 HD"},"views":96184,"isLiveChat":false},{"id":"wKngqJ2Vqnl","thumb":"https://cms.dmpcdn.com/livetv/2019/01/10/35a35017-8473-4953-8474-5c58d805b74a.png","slug":"mono29","title":"MONO 29","content_type":"livetv","category":"livetv-ca|digitaltv-ca|freetv-ca|movies-series-ca","content_provider":"","channel_code":"d43","content_rights":null,"channel_info":{"channel_name_cbd":"មូណូ ធីវី","channel_name_eng":"Mono 29","channel_name_mm":"Mono 29","channel_name_th":"โมโน 29"},"views":33721,"isLiveChat":false},{"id":"8v732AYomo9","thumb":"https://cms.dmpcdn.com/livetv/2023/07/18/7dc7a180-2515-11ee-b8b2-77e2a8f4c31e_webp_original.webp","slug":"thairathtv-hd","title":"Thairath TV","content_type":"livetv","category":"livetv-ca|digitaltv-ca|freetv-ca|news-ca","content_provider":"","channel_code":"d05","content_rights":null,"channel_info":{"channel_name_cbd":"ថៃរ៉ាត់ ធីវី HD","channel_name_eng":"Thairath TV HD","channel_name_mm":"Thairath TV HD","channel_name_th":"ไทยรัฐ ทีวี HD"},"views":17228,"isLiveChat":false},{"id":"9O54lyP5Rqx","thumb":"https://cms.dmpcdn.com/livetv/2023/07/19/212d15e0-25e7-11ee-bfc1-85e95548413c_webp_original.webp","slug":"ch7-hd","title":"Channel 7HD","content_type":"livetv","category":"livetv-ca|digitaltv-ca|entertainment-ca|freetv-ca","content_provider":"","channel_code":"c07","content_rights":null,"channel_info":{"channel_name_cbd":"ប៉ុស្តិ៍​ 7","channel_name_eng":"CH 7HD","channel_name_mm":"Channel 7","channel_name_th":"ช่อง 7HD"},"views":12092,"isLiveChat":false},{"id":"0z4lvq6Xwoa","thumb":"https://cms.dmpcdn.com/livetv/2019/01/16/396384be-35dc-4d11-bf04-06c9546ec7bc.png","slug":"one-hd","title":"One31","content_type":"livetv","category":"livetv-ca|digitaltv-ca|entertainment-ca|freetv-ca","content_provider":"","channel_code":"d56","content_rights":null,"channel_info":{"channel_name_cbd":"វ័ន HD","channel_name_eng":"One HD","channel_name_mm":"One HD","channel_name_th":"วัน HD"},"views":10182,"isLiveChat":false},{"id":"vqbr1WgEnGQ","thumb":"https://cms.dmpcdn.com/livetv/2023/09/15/5408a390-5377-11ee-8e1b-194edbb69638_webp_original.webp","slug":"ch8","title":"Channel 8","content_type":"livetv","category":"livetv-ca|digitaltv-ca|entertainment-ca|freetv-ca","content_provider":"","channel_code":"d62","content_rights":null,"channel_info":{"channel_name_cbd":"ប៉ុស្តិ៍ 8","channel_name_eng":"CH8","channel_name_th":"ช่อง 8"},"views":8295,"isLiveChat":false},{"id":"OVKwZle4eop","thumb":"https://cms.dmpcdn.com/livetv/2023/09/15/84504210-5377-11ee-aaa1-7d584d8ca7a4_webp_original.webp","slug":"true4u","title":"True4U","content_type":"livetv","category":"livetv-ca|digitaltv-ca|entertainment-ca|freetv-ca|movies-series-ca","content_provider":"","channel_code":"207","content_rights":null,"channel_info":{"channel_name_cbd":"ទ្រូ4យូ","channel_name_chi":"True4U","channel_name_eng":"True4U","channel_name_mm":"True4U","channel_name_rus":"True4U","channel_name_th":"ทรูโฟร์ยู","channel_name_vie":"True4U"},"views":6489,"isLiveChat":false},{"id":"OBb6NzoJX7O","thumb":"https://cms.dmpcdn.com/livetv/2023/10/02/d2ec4b30-60f1-11ee-92a4-8597bcef0049_webp_original.webp","slug":"amarintv-hd","title":"Amarin TV","content_type":"livetv","category":"livetv-ca|digitaltv-ca|freetv-ca","content_provider":"","channel_code":"da0","content_rights":null,"channel_info":{"channel_name_cbd":"អាម៉ារិន","channel_name_eng":"Amarin TV","channel_name_mm":"Amarin TV","channel_name_th":"อมรินทร์"},"views":6407,"isLiveChat":false},{"id":"yYk6PvXwXDb","thumb":"https://cms.dmpcdn.com/livetv/2023/11/17/2a1de990-852d-11ee-bf98-41acc8fd04fc_webp_original.webp","slug":"workpointtv","title":"WorkPoint TV","content_type":"livetv","category":"livetv-ca|digitaltv-ca|entertainment-ca|freetv-ca","content_provider":"","channel_code":"d83","content_rights":null,"channel_info":{"channel_name_cbd":"វើកភ័ញ គ្រីអ៊ែតធិវ ធីវី​","channel_name_eng":"Workpoint TV","channel_name_mm":"Workpoint TV","channel_name_th":"เวิร์คพอยท์ ทีวี"},"views":6075,"isLiveChat":false},{"id":"qvgeWLPGMY6","thumb":"https://cms.dmpcdn.com/livetv/2020/11/19/ed873d50-2a22-11eb-bed4-0972e345f90c_original.png","slug":"gmm25","title":"GMM 25","content_type":"livetv","category":"livetv-ca|digitaltv-ca|entertainment-ca|freetv-ca","content_provider":"","channel_code":"d76","content_rights":null,"channel_info":{"channel_name_cbd":"GMM 25","channel_name_eng":"GMM 25","channel_name_mm":"GMM 25","channel_name_th":"จีเอ็มเอ็ม 25"},"views":4861,"isLiveChat":false},{"id":"zMLBpX7AWmk","thumb":"https://cms.dmpcdn.com/livetv/2023/09/09/9c6f59c0-4ebe-11ee-99a7-832609069236_webp_original.webp","slug":"nationtv","title":"Nation TV","content_type":"livetv","category":"livetv-ca|digitaltv-ca|freetv-ca|news-ca","content_provider":"","channel_code":"d78","content_rights":null,"channel_info":{"channel_name_cbd":"Nation TV 22","channel_name_chi":"Nation TV 22","channel_name_eng":"Nation TV 22","channel_name_mm":"Nation TV 22","channel_name_rus":"Nation TV 22","channel_name_th":"เนชั่น ทีวี","channel_name_vie":"Nation TV 22"},"views":4733,"isLiveChat":false},{"id":"QNBwOpdaxpQ","thumb":"https://cms.dmpcdn.com/livetv/2023/08/28/012eed00-458a-11ee-bd2b-6734a2d9e428_webp_original.webp","slug":"pptv-hd","title":"PPTV","content_type":"livetv","category":"livetv-ca|digitaltv-ca|freetv-ca","content_provider":"","channel_code":"da7","content_rights":null,"channel_info":{"channel_name_cbd":"ភីភីធីវី","channel_name_eng":"PPTV","channel_name_mm":"PPTV","channel_name_th":"พีพีทีวี"},"views":4723,"isLiveChat":false},{"id":"xqY73dWBoZye","thumb":"https://cms.dmpcdn.com/livetv/2023/05/03/ba425a00-e966-11ed-be07-cbff4c6d2c94_webp_original.png","slug":"truepremierfootballhd1","title":"True Premier Football 1","content_type":"livetv","category":"livetv-ca|football-ca|sports-ca|tvsnow|sports","content_provider":"true_vision","channel_code":"ht111","content_rights":null,"channel_info":{"channel_name_eng":"True Premier Football 1","channel_name_th":"ทรู พรีเมียร์ ฟุตบอล 1"},"views":3123,"isLiveChat":true},{"id":"QRP2K658b7G","thumb":"https://cms.dmpcdn.com/livetv/2023/09/15/ab170410-5377-11ee-8e1b-194edbb69638_webp_original.webp","slug":"thaipbs","title":"Thai PBS","content_type":"livetv","category":"livetv-ca|digitaltv-ca|freetv-ca|news-ca","content_provider":"","channel_code":"c12","content_rights":null,"channel_info":{"channel_name_cbd":"ធីភីបីអេស","channel_name_eng":"TPBS","channel_name_mm":"TPBS","channel_name_th":"ไทยพีบีเอส"},"views":2526,"isLiveChat":false},{"id":"OZeq8ZLPldY","thumb":"https://cms.dmpcdn.com/livetv/2023/10/02/75023d90-60f1-11ee-935a-5d4eba985103_webp_original.webp","slug":"tnn16","title":"TNN 16","content_type":"livetv","category":"livetv-ca|digitaltv-ca|freetv-ca|news-ca|tvsnow|tvsnews","content_provider":"true_vision","channel_code":"135","content_rights":null,"channel_info":{"channel_name_cbd":"ធីអិនអិន 16","channel_name_eng":"TNN​ 16","channel_name_mm":"TNN​ 16","channel_name_th":"ทีเอ็นเอ็น 16"},"views":2444,"isLiveChat":false},{"id":"LY2j6Pyxbla","thumb":"https://cms.dmpcdn.com/livetv/2023/10/02/4a2afc60-60f1-11ee-a78e-f70ba0052fab_webp_original.webp","slug":"nbt","title":"NBT","content_type":"livetv","category":"livetv-ca|digitaltv-ca|freetv-ca","content_provider":"","channel_code":"c11","content_rights":null,"channel_info":{"channel_name_cbd":"អិនបីធី​","channel_name_eng":"NBT","channel_name_mm":"์NBT","channel_name_th":"เอ็นบีที"},"views":2043,"isLiveChat":false},{"id":"Z9E4LnAbgjKy","thumb":"https://cms.dmpcdn.com/livetv/2021/06/15/3e4e0540-cdb4-11eb-9a22-7958179a38a7_original.png","slug":"jkn18","title":"JKN 18","content_type":"livetv","category":"livetv-ca|digitaltv-ca|freetv-ca","content_provider":"","channel_code":"d11","content_rights":null,"channel_info":{"channel_name_eng":"JKN 18","channel_name_mm":"JKN 18","channel_name_th":"เจเคเอ็น 18"},"views":1725,"isLiveChat":false},{"id":"rBWOx89v9Rk","thumb":"https://cms.dmpcdn.com/livetv/2023/09/09/9cc40970-4ebe-11ee-9801-97f95b5eed9a_webp_original.webp","slug":"9mcot-hd","title":"9 MCOT","content_type":"livetv","category":"livetv-ca|digitaltv-ca|freetv-ca","content_provider":"","channel_code":"c09","content_rights":null,"channel_info":{"channel_name_cbd":"ប៉ុស្តិ៍ 9 HD","channel_name_eng":"9 MCOT HD","channel_name_mm":"9 MCOT HD","channel_name_th":"9 เอ็มคอต HD"},"views":1323,"isLiveChat":false},{"id":"5PKobQk5gLOP","thumb":"https://cms.dmpcdn.com/livetv/2023/07/05/b74a2460-1b05-11ee-8ce6-b102b53cb4a2_webp_original.webp","slug":"boomerang-hd","title":"Boomerang","content_type":"livetv","category":"livetv-ca|freetv-ca|kids-ca","content_provider":"","channel_code":"i007","content_rights":null,"channel_info":{"channel_name_eng":"Boomerang","channel_name_th":"บูมเมอแรง"},"views":707,"isLiveChat":false},{"id":"KEN52vz3o6M","thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/296e96a0-e593-11ed-8507-4fc0b025fedb_webp_original.png","slug":"truesport-hd-3","title":"True Sports 3","content_type":"livetv","category":"livetv-ca|sports-ca|tvsnow|sports","content_provider":"true_vision","channel_code":"ht117","content_rights":null,"channel_info":{"channel_name_cbd":"True Sports 3","channel_name_chi":"True Sports 3","channel_name_eng":"True Sports 3","channel_name_mm":"True Sports 3","channel_name_rus":"True Sports 3","channel_name_th":"ทรูสปอร์ต 3","channel_name_vie":"True Sports 3"},"views":652,"isLiveChat":true},{"id":"1KDEkNJDZ9r","thumb":"https://cms.dmpcdn.com/livetv/2023/07/18/7e060a10-2515-11ee-864f-a52221dad038_webp_original.webp","slug":"ch5","title":"TV5 HD","content_type":"livetv","category":"livetv-ca|digitaltv-ca|freetv-ca|news-ca","content_provider":"","channel_code":"c05","content_rights":null,"channel_info":{"channel_name_cbd":"ប៉ុស្តិ៍ 5","channel_name_eng":"CH 5","channel_name_mm":"နံပါတ္ 5 အစီအစဥ","channel_name_th":"ช่อง 5"},"views":648,"isLiveChat":false},{"id":"NopZ5gjkGmE","thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/45345d10-e599-11ed-86b8-bb40638e3c49_webp_original.png","slug":"true-movie-hits","title":"True Movie Hits","content_type":"livetv","category":"livetv-ca|movies-series-ca|trueunlock-ca|tvsnow|movieseries","content_provider":"true_vision","channel_code":"057","content_rights":null,"channel_info":{"channel_name_cbd":"True Movie Hits","channel_name_chi":"True Movie Hits","channel_name_eng":"True Movie Hits","channel_name_mm":"True Movie Hits","channel_name_rus":"True Movie Hits","channel_name_th":"True Movie Hits","channel_name_vie":"True Movie Hits"},"views":640,"isLiveChat":false},{"id":"9xQq7Yk7Jzr","thumb":"https://cms.dmpcdn.com/livetv/2023/07/18/7d74eda0-2515-11ee-864f-a52221dad038_webp_original.webp","slug":"realitychannel-hd","title":"Reality","content_type":"livetv","category":"livetv-ca|education-ca|entertainment-ca|freetv-ca|kids-ca|truelittlemonk|tvsnow|entertainment","content_provider":"true_vision","channel_code":"107","content_rights":null,"channel_info":{"channel_name_cbd":"ប៉ុស្តិ៍ រែលអាលីធី","channel_name_eng":"Reality","channel_name_mm":"ထရူး ပူပန္ယာ","channel_name_th":"เรียลลิตี้"},"views":518,"isLiveChat":false},{"id":"GPVMYwpnzKv","thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/f63723d0-e595-11ed-abcb-c792e696f885_webp_original.png","slug":"truesport-7","title":"True Sports 7","content_type":"livetv","category":"livetv-ca|sports-ca|trueunlock-ca|tvsnow|sports","content_provider":"true_vision","channel_code":"105","content_rights":null,"channel_info":{"channel_name_cbd":"True Sports 7","channel_name_chi":"True Sports 7","channel_name_eng":"True Sports 7","channel_name_mm":"True Sports 7","channel_name_rus":"True Sports 7","channel_name_th":"ทรูสปอร์ต 7","channel_name_vie":"True Sports 7"},"views":499,"isLiveChat":true},{"id":"RN8ALdyRovrj","thumb":"https://cms.dmpcdn.com/livetv/2022/02/10/e00c0e00-8a3c-11ec-8f9e-831d2ccecc69_webp_original.png","slug":"t-sports-7-sd","title":"T Sports 7","content_type":"livetv","category":"livetv-ca|digitaltv-ca|freetv-ca|sports-ca","content_provider":"","channel_code":"t514","content_rights":null,"channel_info":{"channel_name_eng":"T Sports 7","channel_name_th":"สถานีโทรทัศน์เพื่อการท่องเที่ยวและกีฬา"},"views":484,"isLiveChat":false},{"id":"AlPo3NzNZa62","thumb":"https://cms.dmpcdn.com/livetv/2023/05/03/ba4c4510-e966-11ed-896e-69ce273284a6_webp_original.png","slug":"truepremierfootballhd2","title":"True Premier Football 2","content_type":"livetv","category":"livetv-ca|football-ca|sports-ca|tvsnow|sports","content_provider":"true_vision","channel_code":"ht112","content_rights":null,"channel_info":{"channel_name_eng":"True Premier Football 2","channel_name_th":"ทรู พรีเมียร์ ฟุตบอล 2"},"views":449,"isLiveChat":true},{"id":"PanRBOzKovQ","thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/43f28e40-e599-11ed-844f-795506bf0bf9_webp_original.png","slug":"true-film-hd","title":"True Film 1","content_type":"livetv","category":"hbtv-trueidtv-all|livetv-ca|movies-series-ca|trueidtv-movies-series|tvsnow|movieseries","content_provider":"true_vision","channel_code":"176","content_rights":null,"channel_info":{"channel_name_cbd":"True Film 1","channel_name_chi":"True Film 1","channel_name_eng":"True Film 1","channel_name_mm":"True Film 1","channel_name_rus":"True Film 1","channel_name_th":"True Film 1","channel_name_vie":"True Film 1"},"views":388,"isLiveChat":false},{"id":"GNd67OBJ6pv","thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/46f1c480-e599-11ed-96ec-4d05b9e2ca86_webp_original.png","slug":"thai-film","title":"True Thai Film","content_type":"livetv","category":"livetv-ca|movies-series-ca|trueunlock-ca|tvsnow|movieseries","content_provider":"true_vision","channel_code":"094","content_rights":null,"channel_info":{"channel_name_cbd":"True Thai Film","channel_name_chi":"True Thai Film","channel_name_eng":"True Thai Film","channel_name_mm":"True Thai Film","channel_name_rus":"True Thai Film","channel_name_th":"True Thai Film","channel_name_vie":"True Thai Film"},"views":359,"isLiveChat":false},{"id":"a0k7zw9OPrr0","thumb":"https://cms.dmpcdn.com/livetv/2020/07/14/72c22620-c5aa-11ea-a8d3-2b56c8ce453d_original.png","slug":"altv","title":"ALTV","content_type":"livetv","category":"livetv-ca|digitaltv-ca|education-ca|freetv-ca","content_provider":"","channel_code":"dum024","content_rights":null,"channel_info":{"channel_name_eng":"ALTV","channel_name_th":"เอแอลทีวี"},"views":255,"isLiveChat":false},{"id":"9WmoQMj0NOp","thumb":"https://cms.dmpcdn.com/livetv/2023/11/22/932dbce0-8919-11ee-820d-0ff332ca746f_webp_original.webp","slug":"trueplookpanya","title":"True Plook Panya","content_type":"livetv","category":"livetv-ca|documentary-ca|tvsnow|documentary","content_provider":"true_vision","channel_code":"139","content_rights":null,"channel_info":{"channel_name_cbd":"ទ្រូបណ្តុះគំណិត","channel_name_eng":"True Plookpanya","channel_name_mm":"True Plookpanya","channel_name_th":"ทรู ปลูกปัญญา"},"views":242,"isLiveChat":false},{"id":"KlW9OymBRqrD","thumb":"https://cms.dmpcdn.com/livetv/2023/09/25/c3898e20-5b4f-11ee-a599-1d1a4f7c1125_webp_original.webp","slug":"trueid-sports","title":"TrueID Sports","content_type":"livetv","category":"livetv-ca|sports-ca","content_provider":"","channel_code":"he003","content_rights":null,"channel_info":{"channel_name_eng":"TrueID Sports","channel_name_th":"ทรูไอดี สปอร์ต"},"views":240,"isLiveChat":true},{"id":"Vwz1j7XVRkdn","thumb":"https://cms.dmpcdn.com/livetv/2023/08/02/89262f60-30e1-11ee-b445-3703761d6f4d_webp_original.webp","slug":"true-ball-thai-1","title":"True Ball Thai 1","content_type":"livetv","category":"livetv-ca|football-ca|sports-ca|tvsnow|sports","content_provider":"true_vision","channel_code":"vc01","content_rights":null,"channel_info":{"channel_name_eng":"True Ball Thai 1","channel_name_th":"True Ball Thai 1"},"views":234,"isLiveChat":false},{"id":"YmaygkwgE6Lm","thumb":"https://cms.dmpcdn.com/livetv/2023/10/02/75023d90-60f1-11ee-935a-5d4eba985103_webp_original.webp","slug":"tnn16-hd","title":"TNN 16 HD","content_type":"livetv","category":"livetv-ca|news-ca|tnn|tvsnow|tvsnews","content_provider":"true_vision","channel_code":"t516","content_rights":null,"channel_info":{"channel_name_eng":"TNN​ 16 HD","channel_name_th":"TNN 16 HD"},"views":233,"isLiveChat":false},{"id":"GdgqaeMewGp4","thumb":"https://cms.dmpcdn.com/livetv/2023/05/03/baf9ea30-e966-11ed-a3d3-f3f98ac7a1a1_webp_original.png","slug":"truepremierfootballhd3","title":"True Premier Football 3","content_type":"livetv","category":"livetv-ca|football-ca|sports-ca|tvsnow|sports","content_provider":"true_vision","channel_code":"ht113","content_rights":null,"channel_info":{"channel_name_eng":"True Premier Football 3","channel_name_th":"ทรู พรีเมียร์ ฟุตบอล 3"},"views":199,"isLiveChat":true},{"id":"A36nrdXGn3V","thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/47a53600-e599-11ed-94a2-8feec94a4a3b_webp_original.png","slug":"true-asian-more","title":"True Asian More","content_type":"livetv","category":"livetv-ca|movies-series-ca|trueunlock-ca|tvsnow|movieseries","content_provider":"true_vision","channel_code":"081","content_rights":null,"channel_info":{"channel_name_cbd":"True Asian More","channel_name_chi":"True Asian More","channel_name_eng":"True Asian More","channel_name_mm":"True Asian More","channel_name_rus":"True Asian More","channel_name_th":"True Asian More","channel_name_vie":"True Asian More"},"views":190,"isLiveChat":false},{"id":"74ngXBo8ke0","thumb":"https://cms.dmpcdn.com/livetv/2019/01/21/a01a26bb-ed4a-45c5-88a9-ff30f6bbb039.png","slug":"cartoonclub","title":"Cartoon Club","content_type":"livetv","category":"cartoon|hbtv-trueidtv-all|hbtv-truetv-kids|trueidtv-all|trueidtv-kids|kids|livetv-ca|kids-ca","content_provider":"","channel_code":"143","content_rights":null,"channel_info":{"channel_name_cbd":"កាទូនខ្លឹប","channel_name_eng":"Cartoon Club","channel_name_mm":"ကာတြန္းကလပ္","channel_name_th":"การ์ตูน คลับ"},"views":189,"isLiveChat":false},{"id":"4QmJ09AyPm4","thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/43ffada0-e599-11ed-abcb-c792e696f885_webp_original.png","slug":"true-film-hd-2","title":"True Film 2","content_type":"livetv","category":"hbtv-truetv-movies-series|livetv-ca|movies-series-ca|tvsnow|movieseries","content_provider":"true_vision","channel_code":"221","content_rights":null,"channel_info":{"channel_name_cbd":"True Film 2","channel_name_chi":"True Film 2","channel_name_eng":"True Film 2","channel_name_mm":"True Film 2","channel_name_rus":"True Film 2","channel_name_th":"True Film 2","channel_name_vie":"True Film 2"},"views":185,"isLiveChat":false},{"id":"wQZrKd3mo65","thumb":"https://cms.dmpcdn.com/livetv/2023/04/24/81825540-e28a-11ed-9bb2-7fe2e28bfd8c_webp_original.png","slug":"truesport-hd","title":"True Sports 1","content_type":"livetv","category":"livetv-ca|sports-ca|tvsnow|sports","content_provider":"true_vision","channel_code":"097","content_rights":null,"channel_info":{"channel_name_cbd":"True Sports 1","channel_name_chi":"True Sports 1","channel_name_eng":"True Sports 1","channel_name_mm":"True Sports 1","channel_name_rus":"True Sports 1","channel_name_th":"ทรูสปอร์ต 1","channel_name_vie":"True Sports 1"},"views":179,"isLiveChat":true},{"id":"Lzz61DA3zYL","thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/486c7da0-e599-11ed-b481-1b121c78e74e_webp_original.png","slug":"true-explore-life","title":"True Explore Life","content_type":"livetv","category":"livetv-ca|documentary-ca|trueunlock-ca|tvsnow|documentary","content_provider":"true_vision","channel_code":"060","content_rights":null,"channel_info":{"channel_name_cbd":"True Explore Life","channel_name_chi":"True Explore Life","channel_name_eng":"True Explore Life","channel_name_mm":"True Explore Life","channel_name_rus":"True Explore Life","channel_name_th":"True Explore Life","channel_name_vie":"True Explore Life"},"views":124,"isLiveChat":false},{"id":"vNG2L371k5W","thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/433fe010-e599-11ed-96ec-4d05b9e2ca86_webp_original.png","slug":"true-explore-wild","title":"True Explore Wild","content_type":"livetv","category":"livetv-ca|documentary-ca|trueunlock-ca|true-unlock|true-unlock-atv|tvsnow|documentary","content_provider":"true_vision","channel_code":"058","content_rights":null,"channel_info":{"channel_name_cbd":"True Explore Wild","channel_name_eng":"True Explore Wild","channel_name_mm":"True Explore Wild","channel_name_th":"True Explore Wild"},"views":119,"isLiveChat":false},{"id":"jqepWV3ka8j","thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/46fb6170-e599-11ed-b606-c19576cb8b29_webp_original.png","slug":"true-x-zyte-hd","title":"True X-Zyte","content_type":"livetv","category":"livetv-ca|entertainment-ca|trueunlock-ca|tvsnow|entertainment","content_provider":"true_vision","channel_code":"034","content_rights":null,"channel_info":{"channel_name_cbd":"True X-Zyte","channel_name_chi":"True X-Zyte","channel_name_eng":"True X-Zyte","channel_name_mm":"True X-Zyte","channel_name_rus":"True X-Zyte","channel_name_th":"True X-Zyte","channel_name_vie":"True X-Zyte"},"views":107,"isLiveChat":false},{"id":"3wLvyKyryPAD","thumb":"https://cms.dmpcdn.com/livetv/2023/07/24/5fda18e0-29cc-11ee-846b-a1c4e5181c87_webp_original.webp","slug":"bein-sports-hd3","title":"beIN SPORTS 3","content_type":"livetv","category":"livetv-ca|football-ca|sports-ca|tvsnow|sports","content_provider":"true_vision","channel_code":"215","content_rights":null,"channel_info":{"channel_name_eng":"beIN SPORTS 3","channel_name_th":"บีอินสปอตส์ 3"},"views":96,"isLiveChat":false},{"id":"mVoXV1rk4B5","thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/488c61b0-e599-11ed-94a2-8feec94a4a3b_webp_original.png","slug":"true-explore-3","title":"True Explore Sci","content_type":"livetv","category":"livetv-ca|documentary-ca|trueunlock-ca|tvsnow|documentary","content_provider":"true_vision","channel_code":"061","content_rights":null,"channel_info":{"channel_name_cbd":"True Explore Sci","channel_name_chi":"True Explore Sci","channel_name_eng":"True Explore Sci","channel_name_mm":"True Explore Sci","channel_name_rus":"True Explore Sci","channel_name_th":"True Explore Sci","channel_name_vie":"True Explore Sci"},"views":91,"isLiveChat":false},{"id":"D1029rjaV6GQ","thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/0c14dd80-9407-11ee-b625-274874732f96_webp_original.webp","slug":"manchester-united","title":"Manchester United","content_type":"livetv","category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all","content_provider":"","channel_code":"mun01","content_rights":null,"channel_info":{"channel_name_eng":"Manchester United","channel_name_th":"แมนยู"},"views":91,"isLiveChat":false},{"id":"peWQgAb52vk","thumb":"https://cms.dmpcdn.com/livetv/2023/07/18/7cb4aae0-2515-11ee-9407-9367a664b338_webp_original.webp","slug":"golf-channel","title":"Golf Channel Thailand","content_type":"livetv","category":"livetv-ca|sports-ca|tvsnow|sports","content_provider":"true_vision","channel_code":"095","content_rights":null,"channel_info":{"channel_name_cbd":"Golf Channel Thailand HD","channel_name_chi":"Golf Channel Thailand HD","channel_name_eng":"Golf Channel Thailand HD","channel_name_mm":"Golf Channel Thailand HD","channel_name_rus":"Golf Channel Thailand HD","channel_name_th":"Golf Channel Thailand HD","channel_name_vie":"Golf Channel Thailand HD"},"views":88,"isLiveChat":false},{"id":"A8aVZWzlOmDE","thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/d7783cc0-9406-11ee-b445-0b5cfb8bf6f8_webp_original.webp","slug":"liverpool","title":"Liverpool","content_type":"livetv","category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all","content_provider":"","channel_code":"liv01","content_rights":null,"channel_info":{"channel_name_eng":"Liverpool","channel_name_th":"ลิเวอร์พูล"},"views":88,"isLiveChat":false},{"id":"Ay93Q8zlOeA","thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/456c5d00-e599-11ed-b550-9935ba8025b9_webp_original.png","slug":"true-series","title":"True Series","content_type":"livetv","category":"livetv-ca|movies-series-ca|tvsnow|movieseries","content_provider":"true_vision","channel_code":"st006","content_rights":null,"channel_info":{"channel_name_cbd":"True Series","channel_name_chi":"True Series","channel_name_eng":"True Series","channel_name_mm":"True Series","channel_name_rus":"True Series","channel_name_th":"True Series","channel_name_vie":"True Series"},"views":82,"isLiveChat":false},{"id":"g9ONWXWJV5pq","thumb":"https://cms.dmpcdn.com/livetv/2023/07/24/5f3c5240-29cc-11ee-b2f4-e9de482d866e_webp_original.webp","slug":"bein-sports-hd1","title":"beIN SPORTS 1","content_type":"livetv","category":"livetv-ca|football-ca|sports-ca|tvsnow|sports","content_provider":"true_vision","channel_code":"202","content_rights":null,"channel_info":{"channel_name_eng":"beIN SPORTS 1","channel_name_th":"บีอินสปอตส์ 1"},"views":80,"isLiveChat":false},{"id":"xR0n6ePG7wL","thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/2960b3f0-e593-11ed-b26c-6b89d082d464_webp_original.png","slug":"truesport-hd-2","title":"True Sports 2","content_type":"livetv","category":"livetv-ca|football-ca|sports-ca|trueunlock-ca|tvsnow|sports","content_provider":"true_vision","channel_code":"ht116","content_rights":null,"channel_info":{"channel_name_cbd":"True Sports 2","channel_name_chi":"True Sports 2","channel_name_eng":"True Sports 2","channel_name_mm":"True Sports 2","channel_name_rus":"True Sports 2","channel_name_th":"ทรูสปอร์ต 2","channel_name_vie":"True Sports 2"},"views":80,"isLiveChat":true},{"id":"JlrpNK19py0M","thumb":"https://cms.dmpcdn.com/livetv/2019/04/11/19b4bd2a-750c-4ee6-9d41-5080e1310bc3_original.png","slug":"Mangorn","title":"Mangorn","content_type":"livetv","category":"free-tv|livetv-ca|freetv-ca|movies-series-ca","content_provider":"","channel_code":"o020","content_rights":null,"channel_info":{"channel_name_eng":"Mangorn","channel_name_th":"มังกร"},"views":73,"isLiveChat":false},{"id":"lPXDJR6gN6l","thumb":"https://cms.dmpcdn.com/livetv/2019/02/28/a9490c72-7387-4409-b5a8-80db28585ca4.png","slug":"true-select","title":"True Select","content_type":"livetv","category":"livetv-ca|entertainment-ca|variety-ca|tvsnow|entertainment","content_provider":"true_vision","channel_code":"218","content_rights":null,"channel_info":{"channel_name_cbd":"True Select","channel_name_chi":"True Select","channel_name_eng":"True Select","channel_name_mm":"True Select","channel_name_rus":"True Select","channel_name_th":"True Select","channel_name_vie":"True Select"},"views":71,"isLiveChat":false},{"id":"NWY5K7ZELP2","thumb":"https://cms.dmpcdn.com/livetv/2018/12/17/0c30b192-953b-49b9-a9bf-a4c6e3e71de3.png","slug":"true-select-hd","title":"True Shopping","content_type":"livetv","category":"livetv-ca|entertainment-ca|tvsnow|entertainment","content_provider":"true_vision","channel_code":"127","content_rights":null,"channel_info":{"channel_name_cbd":"True Shopping","channel_name_chi":"True Shopping","channel_name_eng":"True Shopping","channel_name_mm":"True Shopping","channel_name_rus":"True Shopping","channel_name_th":"True Shopping","channel_name_vie":"True Shopping"},"views":70,"isLiveChat":true},{"id":"r71LNbqjaKe","thumb":"https://cms.dmpcdn.com/livetv/2019/01/31/a5aeb78c-c4db-474f-a5af-345cb9e2f5b5.png","slug":"rama-channel","title":"Rama Channel","content_type":"livetv","category":"livetv-ca|documentary-ca|news-ca|tvsnow|documentary","content_provider":"true_vision","channel_code":"128","content_rights":null,"channel_info":{"channel_name_cbd":"Rama Channel","channel_name_chi":"Rama Channel","channel_name_eng":"Rama Channel","channel_name_mm":"Rama Channel","channel_name_rus":"Rama Channel","channel_name_th":"Rama Channel","channel_name_vie":"Rama Channel"},"views":69,"isLiveChat":false},{"id":"YLN6d3oYyXEL","thumb":"https://cms.dmpcdn.com/livetv/2023/11/22/2a4de600-8919-11ee-8416-3dc6bea66698_webp_original.webp","slug":"tptv","title":"TPTV","content_type":"livetv","category":"livetv-ca|digitaltv-ca|education-ca|freetv-ca","content_provider":"","channel_code":"d31","content_rights":null,"channel_info":{"channel_name_eng":"TPTV - Thai Parliament TV","channel_name_th":"ทีพีทีวี"},"views":61,"isLiveChat":false},{"id":"eXlvvZ4EA5aY","thumb":"https://cms.dmpcdn.com/livetv/2022/12/22/d9313340-81d9-11ed-a7f9-412bbba270e9_webp_original.png","slug":"tv-nfl-nba","title":"NFL & NBA TV","content_type":"livetv","category":"livetv-ca|sports-ca","content_provider":"true_vision","channel_code":"t513","content_rights":null,"channel_info":{"channel_name_eng":"NFL & NBA TV","channel_name_th":"เอ็นเอฟแอล แอนด์ เอ็นบีเอ ทีวี"},"views":59,"isLiveChat":false},{"id":"zmvD0RO72nL","thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/f493fb20-e595-11ed-b26c-6b89d082d464_webp_original.png","slug":"truesport-5","title":"True Sports 5","content_type":"livetv","category":"livetv-ca|sports-ca|tvsnow|sports","content_provider":"true_vision","channel_code":"056","content_rights":null,"channel_info":{"channel_name_cbd":"True Sports 5","channel_name_chi":"True Sports 5","channel_name_eng":"True Sports 5","channel_name_mm":"True Sports 5","channel_name_rus":"True Sports 5","channel_name_th":"ทรูสปอร์ต 5","channel_name_vie":"True Sports 5"},"views":58,"isLiveChat":false},{"id":"mXQoNYKda2L9","thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/434696d0-e599-11ed-b26c-6b89d082d464_webp_original.png","slug":"film-asia-hd","title":"True Film Asia","content_type":"livetv","category":"livetv-ca|movies-series-ca|tvsnow|movieseries","content_provider":"true_vision","channel_code":"t500","content_rights":null,"channel_info":{"channel_name_cbd":"True Film Asia","channel_name_chi":"True Film Asia","channel_name_eng":"True Film Asia","channel_name_mm":"True Film Asia","channel_name_rus":"True Film Asia","channel_name_th":"True Film Asia","channel_name_vie":"True Film Asia"},"views":55,"isLiveChat":false},{"id":"P83vkq1M1Lp","thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/46065310-e599-11ed-96ec-4d05b9e2ca86_webp_original.png","slug":"true-spark","title":"True Spark Play","content_type":"livetv","category":"livetv-ca|kids-ca|trueunlock-ca|tvsnow|kids","content_provider":"true_vision","channel_code":"007","content_rights":null,"channel_info":{"channel_name_cbd":"True Spark Play","channel_name_chi":"True Spark Play","channel_name_eng":"True Spark Play","channel_name_mm":"True Spark Play","channel_name_rus":"True Spark Play","channel_name_th":"True Spark Play","channel_name_vie":"True Spark Play"},"views":54,"isLiveChat":false},{"id":"2L1ZZdJGxPej","thumb":"https://cms.dmpcdn.com/livetv/2023/07/24/61050450-29cc-11ee-b2f4-e9de482d866e_webp_original.webp","slug":"spotv2-hd","title":"SPOTV 2","content_type":"livetv","category":"livetv-ca|sports-ca|tvsnow|sports","content_provider":"true_vision","channel_code":"t511","content_rights":null,"channel_info":{"channel_name_eng":"SPOTV 2","channel_name_th":"SPOTV 2"},"views":46,"isLiveChat":false},{"id":"Mbx79DOD44J","thumb":"https://cms.dmpcdn.com/livetv/2021/06/01/e2f61c80-c234-11eb-92e3-4bf272c5d086_original.png","slug":"true-music-channel-hd","title":"True Music","content_type":"livetv","category":"livetv-ca|entertainment-ca|trueunlock-ca|tvsnow|entertainment","content_provider":"true_vision","channel_code":"159","content_rights":null,"channel_info":{"channel_name_cbd":"True Music","channel_name_chi":"True Music","channel_name_eng":"True Music","channel_name_mm":"True Music","channel_name_rus":"True Music","channel_name_th":"True Music","channel_name_vie":"True Music"},"views":40,"isLiveChat":false},{"id":"leVMNwY8LA1B","thumb":"https://cms.dmpcdn.com/livetv/2021/02/24/cc08cbe0-764d-11eb-b272-17d04980ce1e_original.png","slug":"ATTV","title":"@TV","content_type":"livetv","category":"free-tv|livetv-ca|freetv-ca","content_provider":"","channel_code":"i002","content_rights":null,"channel_info":{"channel_name_eng":"@TV","channel_name_th":"แอททีวี"},"views":39,"isLiveChat":false},{"id":"V14w2AL9grW6","thumb":"https://cms.dmpcdn.com/livetv/2023/11/13/bd6a6d20-8205-11ee-822c-6bbb3f82c35b_webp_original.webp","slug":"voicetv-2023","title":"VOICE TV","content_type":"livetv","category":"livetv-ca|freetv-ca|news-ca","content_provider":"","channel_code":"154","content_rights":null,"channel_info":{"channel_name_cbd":"វ៉យធីវី","channel_name_eng":"Voice TV","channel_name_mm":"Voice TV","channel_name_th":"วอยซ์ ทีวี"},"views":33,"isLiveChat":false},{"id":"NB2d2A9Zd94z","thumb":"https://cms.dmpcdn.com/livetv/2023/09/25/c389b530-5b4f-11ee-a6f1-ffa978a40b9f_webp_original.webp","slug":"trueid-live","title":"TrueID Live","content_type":"livetv","category":"livetv-ca|entertainment-ca|variety-ca","content_provider":"","channel_code":"ev04","content_rights":null,"channel_info":{"channel_name_th":"ทรูไอดี ไลฟ์"},"views":31,"isLiveChat":true},{"id":"GOPVJMln56Y","thumb":"https://cms.dmpcdn.com/livetv/2020/06/23/816989d0-b550-11ea-8fac-236a281cd6c5_original.png","slug":"dharmatv","title":"Dhamma TV","content_type":"livetv","category":"knowledge|livetv-ca|digitaltv-ca|documentary-ca|trueidtv-all|trueidtv-digital-tv|variety","content_provider":"","channel_code":"o016","content_rights":null,"channel_info":{"channel_name_cbd":"ព្រះធម៌ធីវី","channel_name_eng":"Dhamma TV","channel_name_mm":"Dhamma TV","channel_name_th":"ธรรมะทีวี"},"views":31,"isLiveChat":false},{"id":"09BRRXKbgge9","thumb":"https://cms.dmpcdn.com/livetv/2022/03/23/f7108720-aa94-11ec-9b91-03afdbb2e824_webp_original.png","slug":"truepremierfootballhd6","title":"True Premier Football 6","content_type":"livetv","category":"livetv-ca|football-ca|sports-ca","content_provider":"true_vision","channel_code":"t502","content_rights":null,"channel_info":{"channel_name_eng":"True Premier Football 6","channel_name_th":"ทรู พรีเมียร์ ฟุตบอล 6"},"views":31,"isLiveChat":false},{"id":"Q7vaEm8O9e4","thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/f65ea900-e595-11ed-86b8-bb40638e3c49_webp_original.png","slug":"true-tennis-hd","title":"True Tennis","content_type":"livetv","category":"livetv-ca|sports-ca|tvsnow|sports","content_provider":"true_vision","channel_code":"045","content_rights":null,"channel_info":{"channel_name_cbd":"True Tennis","channel_name_chi":"True Tennis","channel_name_eng":"True Tennis","channel_name_mm":"True Tennis","channel_name_rus":"True Tennis","channel_name_th":"True Tennis","channel_name_vie":"True Tennis"},"views":29,"isLiveChat":false},{"id":"N8E7v0JlM15e","thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/94a32ae0-9406-11ee-a0fd-836d91d2dd6e_webp_original.webp","slug":"chelsea","title":"Chelsea","content_type":"livetv","category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all","content_provider":"","channel_code":"che01","content_rights":null,"channel_info":{"channel_name_eng":"Chelsea","channel_name_th":"เชลซี"},"views":28,"isLiveChat":false},{"id":"PdOXKN4O1vDr","thumb":"https://cms.dmpcdn.com/livetv/2023/09/25/c3898e20-5b4f-11ee-a599-1d1a4f7c1125_webp_original.webp","slug":"trueid-sports02","title":"TrueID Sports 2","content_type":"livetv","category":"livetv-ca|sports-ca|trueidtv-sport","content_provider":"","channel_code":"he004","content_rights":null,"channel_info":{"channel_name_eng":"TrueID Sports 2","channel_name_th":"ทรูไอดี สปอร์ต 2"},"views":26,"isLiveChat":false},{"id":"k3B64mk9ELl3","thumb":"https://cms.dmpcdn.com/livetv/2023/07/24/6057ad50-29cc-11ee-846b-a1c4e5181c87_webp_original.webp","slug":"golfchannel-thhdplus","title":"Golf Channel Thailand HD+","content_type":"livetv","category":"livetv-ca|sports-ca|tvsnow|sports","content_provider":"true_vision","channel_code":"t501","content_rights":null,"channel_info":{"channel_name_cbd":"Golf Channel Thailand HD Plus","channel_name_chi":"Golf Channel Thailand HD Plus","channel_name_eng":"Golf Channel Thailand HD Plus","channel_name_mm":"Golf Channel Thailand HD Plus","channel_name_rus":"Golf Channel Thailand HD Plus","channel_name_th":"Golf Channel Thailand HD Plus","channel_name_vie":"Golf Channel Thailand HD Plus"},"views":26,"isLiveChat":false},{"id":"zWoZqZv6J6N5","thumb":"https://cms.dmpcdn.com/livetv/2022/10/11/f09e41a0-492e-11ed-bb17-0527d4e1664c_webp_original.png","slug":"crime-investigation","title":"Crime + Investigation","content_type":"livetv","category":"livetv-ca|documentary-ca|tvsnow|documentary","content_provider":"true_vision","channel_code":"t517","content_rights":null,"channel_info":{"channel_name_eng":"Crime Investigation","channel_name_th":"ไคร์ม แอนด์ อินเวสทิเกชั่น"},"views":25,"isLiveChat":false},{"id":"bDKPPGOdyAmn","thumb":"https://cms.dmpcdn.com/livetv/2023/07/24/60cba4d0-29cc-11ee-b2f4-e9de482d866e_webp_original.webp","slug":"spotv1-hd","title":"SPOTV 1","content_type":"livetv","category":"livetv-ca|sports-ca|tvsnow|sports","content_provider":"true_vision","channel_code":"t510","content_rights":null,"channel_info":{"channel_name_eng":"SPOTV 1","channel_name_th":"SPOTV 1"},"views":25,"isLiveChat":false},{"id":"zpwxwAgYOV7n","thumb":"https://cms.dmpcdn.com/livetv/2022/02/17/3943ca00-8fd6-11ec-b076-dffedf0eab22_webp_original.png","slug":"white-channel-hd","title":"White Channel","content_type":"livetv","category":"hbtv-trueidtv-all|livetv-ca|freetv-ca","content_provider":"","channel_code":"i006","content_rights":null,"channel_info":{"channel_name_eng":"White Channel","channel_name_th":"ไวท์แชนแนล"},"views":25,"isLiveChat":false},{"id":"vW6BOL0AzxdW","thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/21b70060-9406-11ee-906d-89adbc3169c1_webp_original.webp","slug":"arsenal","title":"Arsenal","content_type":"livetv","category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all","content_provider":"","channel_code":"ars01","content_rights":null,"channel_info":{"channel_name_eng":"Arsenal","channel_name_th":"อาร์เซน่อล"},"views":25,"isLiveChat":false},{"id":"5YQaWExRqD5","thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/87773550-98c4-11ea-b284-2bff0287c295_original.png","slug":"dltv-3","title":"DLTV 3","content_type":"livetv","category":"education|hbtv-trueidtv-all|livetv-ca|education-ca","content_provider":"","channel_code":"dum003","content_rights":null,"channel_info":{"channel_name_cbd":"DLTV 3","channel_name_chi":"DLTV 3","channel_name_eng":"DLTV 3","channel_name_mm":"DLTV 3","channel_name_rus":"DLTV 3","channel_name_th":"DLTV 3","channel_name_vie":"DLTV 3"},"views":23,"isLiveChat":false},{"id":"xPgxpqoyqQ62","thumb":"https://cms.dmpcdn.com/livetv/2021/01/06/68be8520-500f-11eb-8d28-4b8e3f30b51b_original.png","slug":"zing","title":"Zing","content_type":"livetv","category":"livetv-ca|entertainment-ca|movies-series-ca|trueidtv-all","content_provider":"","channel_code":"i001","content_rights":null,"channel_info":{"channel_name_cbd":"Zing","channel_name_chi":"Zing","channel_name_eng":"Zing","channel_name_mm":"Zing","channel_name_rus":"Zing","channel_name_th":"Zing","channel_name_vie":"Zing"},"views":22,"isLiveChat":false},{"id":"5XaDjQd1JJgw","thumb":"https://cms.dmpcdn.com/livetv/2023/07/24/5f346300-29cc-11ee-b2f4-e9de482d866e_webp_original.webp","slug":"bein-sports-hd2","title":"beIN SPORTS 2","content_type":"livetv","category":"livetv-ca|football-ca|sports-ca|trueidtv-all|tvsnow|sports","content_provider":"true_vision","channel_code":"t521","content_rights":null,"channel_info":{"channel_name_eng":"beIN SPORTS 2","channel_name_th":"บีอินสปอตส์ 2"},"views":22,"isLiveChat":false},{"id":"pmXrb1NjLeP0","thumb":"https://cms.dmpcdn.com/livetv/2023/05/03/bbea1690-e966-11ed-935b-df134f58d288_webp_original.png","slug":"truepremierfootballhd5","title":"True Premier Football 5","content_type":"livetv","category":"livetv-ca|football-ca|sports-ca|tvsnow|sports","content_provider":"true_vision","channel_code":"ht115","content_rights":null,"channel_info":{"channel_name_eng":"True Premier Football 5","channel_name_th":"ทรู พรีเมียร์ ฟุตบอล 5"},"views":21,"isLiveChat":false},{"id":"GDna51EdVk4","thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/f4888970-e595-11ed-8507-4fc0b025fedb_webp_original.png","slug":"truesport-hd-4","title":"True Sports 4","content_type":"livetv","category":"livetv-ca|sports-ca|tvsnow|sports","content_provider":"true_vision","channel_code":"062","content_rights":null,"channel_info":{"channel_name_cbd":"True Sports 4","channel_name_chi":"True Sports 4","channel_name_eng":"True Sports 4","channel_name_mm":"True Sports 4","channel_name_rus":"True Sports 4","channel_name_th":"ทรูสปอร์ต 4","channel_name_vie":"True Sports 4"},"views":21,"isLiveChat":false},{"id":"o9vKOR0dLVm7","thumb":"https://cms.dmpcdn.com/livetv/2023/09/25/c3898e20-5b4f-11ee-a599-1d1a4f7c1125_webp_original.webp","slug":"trueid-sports03","title":"TrueID Sports 3","content_type":"livetv","category":"livetv-ca|sports-ca|trueidtv-sport","content_provider":"","channel_code":"he005","content_rights":null,"channel_info":{"channel_name_eng":"TrueID Sports 3","channel_name_th":"ทรูไอดี สปอร์ต 3"},"views":20,"isLiveChat":false},{"id":"rO7WMREyepr","thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/c273ea90-98c4-11ea-bcb3-0320ce420b5e_original.png","slug":"dltv-15","title":"DLTV 15","content_type":"livetv","category":"education|hbtv-trueidtv-all|livetv-ca|education-ca","content_provider":"","channel_code":"dum015","content_rights":null,"channel_info":{"channel_name_cbd":"DLTV 15","channel_name_chi":"DLTV 15","channel_name_eng":"DLTV 15","channel_name_mm":"DLTV 15","channel_name_rus":"DLTV 15","channel_name_th":"DLTV 15","channel_name_vie":"DLTV 15"},"views":20,"isLiveChat":false},{"id":"67ollp0Raz2V","thumb":"https://cms.dmpcdn.com/livetv/2022/03/23/f7114a70-aa94-11ec-9b91-03afdbb2e824_webp_original.png","slug":"truepremierfootballhd7","title":"True Premier Football 7","content_type":"livetv","category":"livetv-ca|football-ca|sports-ca","content_provider":"true_vision","channel_code":"t503","content_rights":null,"channel_info":{"channel_name_eng":"True Premier Football 7","channel_name_th":"ทรู พรีเมียร์ ฟุตบอล 7"},"views":18,"isLiveChat":false},{"id":"2KyzkV6AyPZ","thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/65dc9bb0-98c4-11ea-bcb3-0320ce420b5e_original.png","slug":"dltv-1","title":"DLTV 1","content_type":"livetv","category":"education|hbtv-trueidtv-all|livetv-ca|education-ca","content_provider":"","channel_code":"dum001","content_rights":null,"channel_info":{"channel_name_cbd":"DLTV 1","channel_name_chi":"DLTV 1","channel_name_eng":"DLTV 1","channel_name_mm":"DLTV 1","channel_name_rus":"DLTV 1","channel_name_th":"DLTV 1","channel_name_vie":"DLTV 1"},"views":17,"isLiveChat":false},{"id":"gqVn9n7MeYXq","thumb":"https://cms.dmpcdn.com/livetv/2022/09/08/e55200a0-2f27-11ed-a458-efe831982670_webp_original.png","slug":"arirang-tv","title":"Arirang TV","content_type":"livetv","category":"livetv-ca|entertainment-ca|tvsnow|entertainment","content_provider":"true_vision","channel_code":"t519","content_rights":null,"channel_info":{"channel_name_eng":"Arirang TV","channel_name_th":"Arirang TV"},"views":16,"isLiveChat":false},{"id":"r4PaaOpzr0Ow","thumb":"https://cms.dmpcdn.com/livetv/2022/03/23/f868c420-aa94-11ec-9b91-03afdbb2e824_webp_original.png","slug":"truepremierfootballhd8","title":"True Premier Football 8","content_type":"livetv","category":"livetv-ca|football-ca|sports-ca","content_provider":"true_vision","channel_code":"t504","content_rights":null,"channel_info":{"channel_name_eng":"True Premier Football 8","channel_name_th":"ทรู พรีเมียร์ ฟุตบอล 8"},"views":16,"isLiveChat":false},{"id":"Kz5zjkGyDVA","thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/66ce9cd0-98c4-11ea-b284-2bff0287c295_original.png","slug":"dltv-6","title":"DLTV 6","content_type":"livetv","category":"education|hbtv-trueidtv-all|livetv-ca|education-ca","content_provider":"","channel_code":"dum006","content_rights":null,"channel_info":{"channel_name_cbd":"DLTV 6","channel_name_chi":"DLTV 6","channel_name_eng":"DLTV 6","channel_name_mm":"DLTV 6","channel_name_rus":"DLTV 6","channel_name_th":"DLTV 6","channel_name_vie":"DLTV 6"},"views":16,"isLiveChat":false},{"id":"Veb1NRpQ6LXk","thumb":"https://cms.dmpcdn.com/livetv/2022/02/10/16689820-8a46-11ec-8573-9fd52c482da3_webp_original.png","slug":"zee-anmol-sd","title":"Zee Anmol","content_type":"livetv","category":"livetv-ca|entertainment-ca|freetv-ca|movies-series-ca","content_provider":"","channel_code":"i005","content_rights":null,"channel_info":{"channel_name_eng":"Zee Anmol","channel_name_th":"Zee Anmol"},"views":15,"isLiveChat":false},{"id":"L3Jbvn0BnbA","thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/65debe90-98c4-11ea-b284-2bff0287c295_original.png","slug":"dltv-2","title":"DLTV 2","content_type":"livetv","category":"education|hbtv-trueidtv-all|livetv-ca|education-ca","content_provider":"","channel_code":"dum002","content_rights":null,"channel_info":{"channel_name_cbd":"DLTV 2","channel_name_chi":"DLTV 2","channel_name_eng":"DLTV 2","channel_name_mm":"DLTV 2","channel_name_rus":"DLTV 2","channel_name_th":"DLTV 2","channel_name_vie":"DLTV 2"},"views":15,"isLiveChat":false},{"id":"v2M0K4kgbrN","thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/66317270-98c4-11ea-b284-2bff0287c295_original.png","slug":"dltv-4","title":"DLTV 4","content_type":"livetv","category":"education|hbtv-trueidtv-all|livetv-ca|education-ca","content_provider":"","channel_code":"dum004","content_rights":null,"channel_info":{"channel_name_cbd":"DLTV 4","channel_name_chi":"DLTV 4","channel_name_eng":"DLTV 4","channel_name_mm":"DLTV 4","channel_name_rus":"DLTV 4","channel_name_th":"DLTV 4","channel_name_vie":"DLTV 4"},"views":15,"isLiveChat":false},{"id":"RGdlapJnLQNG","thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/f8af80b0-9406-11ee-8d65-879d2e0f23a3_webp_original.webp","slug":"manchester-city","title":"Manchester City","content_type":"livetv","category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all","content_provider":"","channel_code":"mci01","content_rights":null,"channel_info":{"channel_name_eng":"Manchester City","channel_name_th":"แมนซิตี้"},"views":15,"isLiveChat":false},{"id":"JkpG4LeljXJ0","thumb":"https://cms.dmpcdn.com/livetv/2023/08/02/8a0c49a0-30e1-11ee-b220-4544ede97b74_webp_original.webp","slug":"true-ball-thai-2","title":"True Ball Thai 2","content_type":"livetv","category":"livetv-ca|football-ca|sports-ca|tvsnow|sports","content_provider":"true_vision","channel_code":"vc02","content_rights":null,"channel_info":{"channel_name_eng":"True Ball Thai 2","channel_name_th":"True Ball Thai 2"},"views":14,"isLiveChat":false},{"id":"Yb4p39lbgvN","thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/6683b120-98c4-11ea-b284-2bff0287c295_original.png","slug":"dltv-5","title":"DLTV 5","content_type":"livetv","category":"education|hbtv-trueidtv-all|livetv-ca|education-ca","content_provider":"","channel_code":"dum005","content_rights":null,"channel_info":{"channel_name_cbd":"DLTV 5","channel_name_chi":"DLTV 5","channel_name_eng":"DLTV 5","channel_name_mm":"DLTV 5","channel_name_rus":"DLTV 5","channel_name_th":"DLTV 5","channel_name_vie":"DLTV 5"},"views":14,"isLiveChat":false},{"id":"D38Lb540KAE3","thumb":"https://cms.dmpcdn.com/livetv/2021/02/24/cc358130-764d-11eb-9057-2d10fb4d0cf4_original.png","slug":"MediaTV","title":"Media TV","content_type":"livetv","category":"free-tv|livetv-ca|freetv-ca","content_provider":"","channel_code":"i003","content_rights":null,"channel_info":{"channel_name_eng":"Media TV","channel_name_th":"มีเดีย ทีวี"},"views":13,"isLiveChat":false},{"id":"JGAQ7VZpX9Y","thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/044a2a10-98c5-11ea-bcb3-0320ce420b5e_original.png","slug":"dltv-12","title":"DLTV 12","content_type":"livetv","category":"education|hbtv-trueidtv-all|livetv-ca|education-ca","content_provider":"","channel_code":"dum012","content_rights":null,"channel_info":{"channel_name_cbd":"DLTV 12","channel_name_chi":"DLTV 12","channel_name_eng":"DLTV 12","channel_name_mm":"DLTV 12","channel_name_rus":"DLTV 12","channel_name_th":"DLTV 12","channel_name_vie":"DLTV 12"},"views":13,"isLiveChat":false},{"id":"zyab4aWZ0OWx","thumb":"https://cms.dmpcdn.com/livetv/2023/05/03/bb0beb90-e966-11ed-993c-b59183950f79_webp_original.png","slug":"truepremierfootballhd4","title":"True Premier Football 4","content_type":"livetv","category":"livetv-ca|football-ca|sports-ca|tvsnow|sports","content_provider":"true_vision","channel_code":"ht114","content_rights":null,"channel_info":{"channel_name_eng":"True Premier Football 4","channel_name_th":"ทรู พรีเมียร์ ฟุตบอล 4"},"views":12,"isLiveChat":false},{"id":"pQ6ok8M72AD","thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/bfd21690-98c4-11ea-bcb3-0320ce420b5e_original.png","slug":"dltv-10","title":"DLTV 10","content_type":"livetv","category":"education|hbtv-trueidtv-all|livetv-ca|education-ca","content_provider":null,"channel_code":"dum010","content_rights":null,"channel_info":{"channel_name_cbd":"DLTV 10","channel_name_chi":"DLTV 10","channel_name_eng":"DLTV 10","channel_name_mm":"DLTV 10","channel_name_rus":"DLTV 10","channel_name_th":"DLTV 10","channel_name_vie":"DLTV 10"},"views":12,"isLiveChat":false},{"id":"wkrQgY603zM","thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/c2364550-98c4-11ea-bcb3-0320ce420b5e_original.png","slug":"dltv-14","title":"DLTV 14","content_type":"livetv","category":"education|hbtv-trueidtv-all|livetv-ca|education-ca","content_provider":"","channel_code":"dum014","content_rights":null,"channel_info":{"channel_name_cbd":"DLTV 14","channel_name_chi":"DLTV 14","channel_name_eng":"DLTV 14","channel_name_mm":"DLTV 14","channel_name_rus":"DLTV 14","channel_name_th":"DLTV 14","channel_name_vie":"DLTV 14"},"views":12,"isLiveChat":false},{"id":"nYvz5QLWjyD","thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/67145860-98c4-11ea-b284-2bff0287c295_original.png","slug":"dltv-7","title":"DLTV 7","content_type":"livetv","category":"education|hbtv-trueidtv-all|livetv-ca|education-ca","content_provider":"","channel_code":"dum007","content_rights":null,"channel_info":{"channel_name_cbd":"DLTV 7","channel_name_chi":"DLTV 7","channel_name_eng":"DLTV 7","channel_name_mm":"DLTV 7","channel_name_rus":"DLTV 7","channel_name_th":"DLTV 7","channel_name_vie":"DLTV 7"},"views":11,"isLiveChat":false},{"id":"MndX5W8rWaMn","thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/7003e750-9407-11ee-b445-0b5cfb8bf6f8_webp_original.webp","slug":"tottenham-hotspur","title":"Tottenham Hotspur","content_type":"livetv","category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all","content_provider":"","channel_code":"tot01","content_rights":null,"channel_info":{"channel_name_eng":"Tottenham Hotspur","channel_name_th":"สเปอร์"},"views":11,"isLiveChat":false},{"id":"E9k68z9aKDp","thumb":"https://cms.dmpcdn.com/livetv/2017/10/18/f1b957db-b175-45fc-ab2b-60150f9c570a.png","slug":"tnn-2","title":"TNN 2","content_type":"livetv","category":"livetv-ca|freetv-ca|news-ca|tvsnow|tvsnews","content_provider":"true_vision","channel_code":"074","content_rights":null,"channel_info":{"channel_name_cbd":"TNN 2","channel_name_chi":"TNN 2","channel_name_eng":"TNN 2","channel_name_mm":"TNN 2","channel_name_rus":"TNN 2","channel_name_th":"TNN 2","channel_name_vie":"TNN 2"},"views":10,"isLiveChat":false},{"id":"JeQ5L9PpVBJ","thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/bf927580-98c4-11ea-bcb3-0320ce420b5e_original.png","slug":"dltv-9","title":"DLTV 9","content_type":"livetv","category":"education|hbtv-trueidtv-all|livetv-ca|education-ca","content_provider":"","channel_code":"dum009","content_rights":null,"channel_info":{"channel_name_cbd":"DLTV 9","channel_name_chi":"DLTV 9","channel_name_eng":"DLTV 9","channel_name_mm":"DLTV 9","channel_name_rus":"DLTV 9","channel_name_th":"DLTV 9","channel_name_vie":"DLTV 9"},"views":10,"isLiveChat":false},{"id":"M34YDGLk2wVj","thumb":"https://cms.dmpcdn.com/livetv/2023/08/02/8a3b21d0-30e1-11ee-a53e-b3f87dc8ba1e_webp_original.webp","slug":"true-ball-thai-3","title":"True Ball Thai 3","content_type":"livetv","category":"livetv-ca|football-ca|sports-ca|tvsnow|sports","content_provider":"true_vision","channel_code":"vc03","content_rights":null,"channel_info":{"channel_name_eng":"True Ball Thai 3","channel_name_th":"True Ball Thai 3"},"views":9,"isLiveChat":false},{"id":"R4WyxL6Mp8b","thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/bf9386f0-98c4-11ea-b284-2bff0287c295_original.png","slug":"dltv-8","title":"DLTV 8","content_type":"livetv","category":"education|hbtv-trueidtv-all|livetv-ca|education-ca","content_provider":"","channel_code":"dum008","content_rights":null,"channel_info":{"channel_name_cbd":"DLTV 8","channel_name_chi":"DLTV 8","channel_name_eng":"DLTV 8","channel_name_mm":"DLTV 8","channel_name_rus":"DLTV 8","channel_name_th":"DLTV 8","channel_name_vie":"DLTV 8"},"views":9,"isLiveChat":false},{"id":"6Qna2oVjq3P","thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/c2335f20-98c4-11ea-bcb3-0320ce420b5e_original.png","slug":"dltv-13","title":"DLTV 13","content_type":"livetv","category":"education|hbtv-trueidtv-all|livetv-ca|education-ca","content_provider":"","channel_code":"dum013","content_rights":null,"channel_info":{"channel_name_cbd":"DLTV 13","channel_name_chi":"DLTV 13","channel_name_eng":"DLTV 13","channel_name_mm":"DLTV 13","channel_name_rus":"DLTV 13","channel_name_th":"DLTV 13","channel_name_vie":"DLTV 13"},"views":9,"isLiveChat":false},{"id":"3JYow6Dx7zx0","thumb":"https://cms.dmpcdn.com/livetv/2023/08/02/2f7ad050-30f6-11ee-b57d-a9829f092f3e_webp_original.webp","slug":"bein-sports-6","title":"beIN SPORTS 6","content_type":"livetv","category":"livetv-ca|football-ca|sports-ca|tvsnow|sports","content_provider":"","channel_code":"216","content_rights":null,"channel_info":{"channel_name_eng":"beIN SPORTS 6","channel_name_th":"บีอินสปอตส์ 6"},"views":7,"isLiveChat":false},{"id":"EGvbeMNZOwq","thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/bfdaa210-98c4-11ea-bcb3-0320ce420b5e_original.png","slug":"dltv-11","title":"DLTV 11","content_type":"livetv","category":"education|hbtv-trueidtv-all|livetv-ca|education-ca","content_provider":"","channel_code":"dum011","content_rights":null,"channel_info":{"channel_name_cbd":"DLTV 11","channel_name_chi":"DLTV 11","channel_name_eng":"DLTV 11","channel_name_mm":"DLTV 11","channel_name_rus":"DLTV 11","channel_name_th":"DLTV 11","channel_name_vie":"DLTV 11"},"views":6,"isLiveChat":false},{"id":"JpawvVMe6aXO","thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/1bf1afd0-9407-11ee-8d65-879d2e0f23a3_webp_original.webp","slug":"newcastle-united","title":"Newcastle United","content_type":"livetv","category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all","content_provider":"","channel_code":"new01","content_rights":null,"channel_info":{"channel_name_eng":"Newcastle United","channel_name_th":"นิวคาสเซิล"},"views":6,"isLiveChat":false},{"id":"RVrAxAOGx21v","thumb":"https://cms.dmpcdn.com/livetv/2022/09/08/e5b667c0-2f27-11ed-9e57-d98920d4c462_webp_original.png","slug":"dw-english","title":"DW English","content_type":"livetv","category":"livetv-ca|entertainment-ca|trueunlock-ca|tvsnow|entertainment","content_provider":"true_vision","channel_code":"t518","content_rights":null,"channel_info":{"channel_name_eng":"DW English","channel_name_th":"ดี ดับเบิ้ลยู อิงลิช"},"views":5,"isLiveChat":false},{"id":"eyEPa8A2WaJN","thumb":"https://cms.dmpcdn.com/livetv/2023/08/02/2f7a5b20-30f6-11ee-8c65-b3a6cba5ed9d_webp_original.webp","slug":"beinsports-4","title":"beIN SPORTS 4","content_type":"livetv","category":"livetv-ca|football-ca|sports-ca|tvsnow|sports","content_provider":"","channel_code":"217","content_rights":null,"channel_info":{"channel_name_eng":"beIN SPORTS 4","channel_name_th":"บีอินสปอตส์ 4"},"views":5,"isLiveChat":false},{"id":"pQNm6nA20a6e","thumb":"https://cms.dmpcdn.com/livetv/2023/08/02/300adb50-30f6-11ee-b3e7-85edd640cc04_webp_original.webp","slug":"beinsports-5","title":"beIN SPORTS 5","content_type":"livetv","category":"livetv-ca|football-ca|sports-ca|tvsnow|sports","content_provider":"","channel_code":"219","content_rights":null,"channel_info":{"channel_name_eng":"beIN SPORTS 5","channel_name_th":"บีอินสปอตส์ 5"},"views":2,"isLiveChat":false},{"id":"YZzDXGM1Yd68","thumb":"https://cms.dmpcdn.com/livetv/2021/10/28/3965c0a0-3794-11ec-8e1f-6bce3683de8c_webp_original.png","slug":"Event2","title":"Event 2","content_type":"livetv","category":"hbtv-trueidtv-all|hbtv-truetv-sport|ott-trueidtv-sport|sport|trueidtv-all|trueidtv-sport","content_provider":"","channel_code":"ev03","content_rights":null,"channel_info":null,"views":1,"isLiveChat":false},{"id":"LVQzz7xplYpP","thumb":"https://cms.dmpcdn.com/livetv/2021/10/28/3965c0a0-3794-11ec-8e1f-6bce3683de8c_webp_original.png","slug":"Event5","title":"Event 5","content_type":"livetv","category":"hbtv-trueidtv-all|hbtv-truetv-sport|ott-trueidtv-sport|sport|trueidtv-all|trueidtv-sport","content_provider":null,"channel_code":"emer05","content_rights":null,"channel_info":null,"views":1,"isLiveChat":false},{"id":"xVW7oVd8Gen","thumb":"https://cms.dmpcdn.com/livetv/2020/06/23/99bc7f60-b550-11ea-8fac-236a281cd6c5_original.png","slug":"super-entertain","title":"Super Bunteung","content_type":"livetv","category":"hbtv-trueidtv-all|hbtv-truetv-entertainment|tvsnow|entertainment|livetv-ca|entertainment-ca","content_provider":"true_vision","channel_code":"108","content_rights":null,"channel_info":{"channel_name_cbd":"ស៊ុបព័រអិនធើធែនមិន","channel_name_eng":"Super Bunteung","channel_name_mm":"အထူးေဖ်ာ္ေျဖမႈမ်ား","channel_name_th":"ซุปเปอร์ บันเทิง"},"views":1,"isLiveChat":false},{"id":"rVWJGN1VLOB","thumb":"https://cms.dmpcdn.com/livetv/2017/10/17/31a68f7b-d24e-43e0-9403-22f5e48f081b.png","slug":"etv","title":"ETV","content_type":"livetv","category":"hbtv-trueidtv-all|tvsnow|entertainment|livetv-ca|entertainment-ca|education-ca","content_provider":"true_vision","channel_code":"da2","content_rights":null,"channel_info":{"channel_name_cbd":"ETV","channel_name_chi":"ETV","channel_name_eng":"ETV","channel_name_mm":"ETV","channel_name_rus":"ETV","channel_name_th":"ETV","channel_name_vie":"ETV"},"views":1,"isLiveChat":false},{"id":"vAG5EZznD1Kl","thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/7e72f8d0-9407-11ee-b32f-2d43ff6700d5_webp_original.webp","slug":"west-ham-united","title":"West Ham United","content_type":"livetv","category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all","content_provider":"","channel_code":"whu01","content_rights":null,"channel_info":{"channel_name_eng":"West Ham United","channel_name_th":"เวสต์แฮม"},"views":1,"isLiveChat":false},{"id":"nle3eNnyVpag","thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/a4879e50-9406-11ee-b543-51f040e58632_webp_original.webp","slug":"crystal-palace","title":"Crystal Palace","content_type":"livetv","category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all","content_provider":"","channel_code":"cry01","content_rights":null,"channel_info":{"channel_name_eng":"Crystal-Palace","channel_name_th":"คริสตัลพาเลซ"},"views":1,"isLiveChat":false},{"id":"GB0gZlzxgnJr","thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/0a03a630-9406-11ee-906d-89adbc3169c1_webp_original.webp","slug":"bournemouth","title":"A.F.C. Bournemouth","content_type":"livetv","category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all","content_provider":"","channel_code":"bou01","content_rights":null,"channel_info":{"channel_name_eng":"Bournemouth","channel_name_th":"บอร์นมัธ"},"views":1,"isLiveChat":false},{"id":"1ZrGkEk3qP7L","thumb":"https://cms.dmpcdn.com/livetv/2022/07/18/79461e70-0667-11ed-b687-85f145af88ed_webp_original.png","slug":"test3","title":"ช่องทดสอบออกอากาศที่ 3","content_type":"livetv","category":"livetv-ca|sports-ca","content_provider":"","channel_code":"tmp005","content_rights":null,"channel_info":{"channel_name_eng":"ช่องทดสอบออกอากาศที่ 3","channel_name_th":"ช่องทดสอบออกอากาศที่ 3"},"views":0,"isLiveChat":false},{"id":"6G190MBm2kkG","thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/7bb41c60-9406-11ee-a0fd-836d91d2dd6e_webp_original.webp","slug":"burnley","title":"Burnley","content_type":"livetv","category":"livetv-ca|football-ca|sports-ca|trueunlock-ca","content_provider":"","channel_code":"brn01","content_rights":null,"channel_info":{"channel_name_eng":"Burnley","channel_name_th":"เบิร์นลีย์"},"views":0,"isLiveChat":false},{"id":"4GePx966Dzao","thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/e8af9600-9406-11ee-b625-274874732f96_webp_original.webp","slug":"luton-town","title":"Luton Town","content_type":"livetv","category":"livetv-ca|football-ca|sports-ca|trueunlock-ca","content_provider":"","channel_code":"lut01","content_rights":null,"channel_info":{"channel_name_eng":"Luton Town","channel_name_th":"ลูตัน ทาวน์"},"views":0,"isLiveChat":false},{"id":"4NYqR5KyQArN","thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/61985840-9407-11ee-a0fd-836d91d2dd6e_webp_original.webp","slug":"sheffield-united","title":"Sheffield United","content_type":"livetv","category":"livetv-ca|football-ca|sports-ca|trueunlock-ca","content_provider":"","channel_code":"shu01","content_rights":null,"channel_info":{"channel_name_eng":"Sheffield United","channel_name_th":"เชฟฟิลด์ ยูไนเต็ด"},"views":0,"isLiveChat":false},{"id":"xAzllg2VXjRm","thumb":"https://cms.dmpcdn.com/livetv/2021/10/28/3965c0a0-3794-11ec-8e1f-6bce3683de8c_webp_original.png","slug":"Event3","title":"Event 3","content_type":"livetv","category":"hbtv-trueidtv-all|hbtv-truetv-sport|ott-trueidtv-sport|sport|trueidtv-all|trueidtv-sport","content_provider":null,"channel_code":"emer03","content_rights":null,"channel_info":null,"views":0,"isLiveChat":false},{"id":"WGVqq6zeAzaZ","thumb":"https://cms.dmpcdn.com/livetv/2021/10/28/3965c0a0-3794-11ec-8e1f-6bce3683de8c_webp_original.png","slug":"Event4","title":"Event 4","content_type":"livetv","category":"hbtv-trueidtv-all|hbtv-truetv-sport|ott-trueidtv-sport|sport|trueidtv-all|trueidtv-sport","content_provider":null,"channel_code":"emer04","content_rights":null,"channel_info":null,"views":0,"isLiveChat":false},{"id":"RjY3XkeL5Mwl","thumb":"https://cms.dmpcdn.com/livetv/2021/10/28/3965c0a0-3794-11ec-8e1f-6bce3683de8c_webp_original.png","slug":"Event1","title":"Event 1","content_type":"livetv","category":"hbtv-trueidtv-all|hbtv-truetv-sport|ott-trueidtv-sport|sport|trueidtv-all|trueidtv-sport","content_provider":null,"channel_code":"ev02","content_rights":null,"channel_info":null,"views":0,"isLiveChat":false},{"id":"glmE5eNRz47l","thumb":"https://cms.dmpcdn.com/livetv/2022/07/18/79461e70-0667-11ed-b687-85f145af88ed_webp_original.png","slug":"test","title":"ช่องทดสอบการออกอากาศ","content_type":"livetv","category":"livetv-ca","content_provider":"","channel_code":"tmp003","content_rights":null,"channel_info":{"channel_name_eng":"ทดสอบการออกอากาศ","channel_name_th":"ทดสอบการออกอากาศ"},"views":0,"isLiveChat":false},{"id":"RvJwkNg06Qre","thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/35f494c0-9406-11ee-b32f-2d43ff6700d5_webp_original.webp","slug":"aston-villa","title":"Aston Villa","content_type":"livetv","category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all","content_provider":"","channel_code":"avl01","content_rights":null,"channel_info":{"channel_name_eng":"Aston Villa","channel_name_th":"แอสตันวิลล่า"},"views":0,"isLiveChat":false},{"id":"Vjb43gpNAVnl","thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/b6bb0ad0-9406-11ee-b543-51f040e58632_webp_original.webp","slug":"everton","title":"Everton","content_type":"livetv","category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all","content_provider":"","channel_code":"eve01","content_rights":null,"channel_info":{"channel_name_eng":"Everton","channel_name_th":"เอเวอร์ตัน"},"views":0,"isLiveChat":false},{"id":"K24pNw8k5Kj2","thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/94ed36c0-9407-11ee-906d-89adbc3169c1_webp_original.webp","slug":"wolves","title":"Wolves","content_type":"livetv","category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all","content_provider":"","channel_code":"wol01","content_rights":null,"channel_info":{"channel_name_eng":"Wolves","channel_name_th":"วูลฟ์"},"views":0,"isLiveChat":false},{"id":"vQEl8Do0nK46","thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/c843df70-9406-11ee-8d65-879d2e0f23a3_webp_original.webp","slug":"fulham","title":"Fulham","content_type":"livetv","category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all","content_provider":"","channel_code":"ful01","content_rights":null,"channel_info":{"channel_name_eng":"Fulham","channel_name_th":"ฟูแล่ม"},"views":0,"isLiveChat":false},{"id":"oDqg2NPZdJz5","thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/600d67f0-9406-11ee-ab3e-a51daa175c33_webp_original.webp","slug":"brighton-and-hove-albion","title":"Brighton & Hove Albion","content_type":"livetv","category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all","content_provider":"","channel_code":"bha01","content_rights":null,"channel_info":{"channel_name_eng":"Brighton-and-Hove-Albion","channel_name_th":"ไบร์ทตัน"},"views":0,"isLiveChat":false},{"id":"ykaXNqEMoPZR","thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/30299ee0-9407-11ee-a469-0b60cd4a260f_webp_original.webp","slug":"nottingham-forest","title":"Nottingham Forest","content_type":"livetv","category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all","content_provider":"","channel_code":"nfo01","content_rights":null,"channel_info":{"channel_name_eng":"Nottingham Forest","channel_name_th":"ฟอร์เรสต์"},"views":0,"isLiveChat":false},{"id":"3gaL3mjZoxrE","thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/4738bf40-9406-11ee-a0fd-836d91d2dd6e_webp_original.webp","slug":"brentford","title":"Brentford","content_type":"livetv","category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all","content_provider":"","channel_code":"bre01","content_rights":null,"channel_info":{"channel_name_eng":"Brentford","channel_name_th":"เบรนท์ฟอร์ด"},"views":0,"isLiveChat":false},{"id":"2Ag1bgVdNwoL","thumb":"https://cms.dmpcdn.com/livetv/2022/07/18/79461e70-0667-11ed-b687-85f145af88ed_webp_original.png","slug":"test2","title":"ช่องทดสอบออกอากาศที่2","content_type":"livetv","category":"livetv-ca|sports-ca","content_provider":"","channel_code":"tmp004","content_rights":null,"channel_info":{"channel_name_eng":"ช่องทดสอบออกอากาศที่2","channel_name_th":"ช่องทดสอบออกอากาศที่2"},"views":0,"isLiveChat":false},{"id":"OzKE5d4pdNwy","thumb":"https://cms.dmpcdn.com/livetv/2022/11/16/88521af0-6598-11ed-8215-b386a7bd4f58_webp_original.png","slug":"ufc","title":"UFC","content_type":"livetv","category":"livetv-ca|sports-ca|tvsnow|sports","content_provider":"true_vision","channel_code":"vc04","content_rights":null,"channel_info":{"channel_name_eng":"UFC","channel_name_th":"ยูเอฟซี"},"views":0,"isLiveChat":false}],"channelSlug":"true-movie-hits","baseShelves":{"adsConfig":{"adsNetworkId":"","adsUnit":"21682623839/TrueID_Web/TV"},"id":"3MPnXKpGjKqQ","shelfItems":[{"id":"G3rooMXA2b4Z","title":{"th":"แนะนำ","en":"Featured"},"type":"by_banner_homepage","viewType":"horizontal","shelfItems":[{"id":"J1X89KQE7001","title":"ContentMkt_AVOD_Series_SpiceAndSpell","thumb":"https://cms.dmpcdn.com/hilight/2023/11/24/890e7c60-8aac-11ee-ad2b-e5fcaaed0aed_webp_original.webp","redirectUrl":"https://movie.trueid.net/series/kbNxLxw5Eg52/rV1rP8Dj27kB/AkMBP4z2VOKG/XrVjVjeWXz2r","article_category":null,"content_type":"hilight"},{"id":"xrGQRbjWx1EL","title":"ContentMkt_TVOD_Movie_MI7Part1","thumb":"https://cms.dmpcdn.com/hilight/2023/11/23/32f66c30-89cf-11ee-976f-abdcd950f267_webp_original.webp","redirectUrl":"https://movie.trueid.net/movie/xDnNeqGkreJD","article_category":null,"content_type":"hilight"},{"id":"J52JxN0jXkA5","title":"ContentMkt_SVOD_Asianseries_MyPrecious","thumb":"https://cms.dmpcdn.com/hilight/2023/11/06/00af5020-7c19-11ee-a9a1-41799b41aff4_webp_original.webp","redirectUrl":"","article_category":null,"content_type":"hilight"},{"id":"yb22AdYZnQGb","title":"ContentMkt_AVOD_Lakorn_BakeMePlease","thumb":"https://cms.dmpcdn.com/hilight/2023/11/24/48523fc0-8aa9-11ee-bf9d-a586c3ad0143_webp_original.webp","redirectUrl":"https://movie.trueid.net/series/kN0QYgOQ0EJ5/zWBxNa65n6Vv/Eadp1WLqDXra/yqJgdKrkyRLY","article_category":null,"content_type":"hilight"},{"id":"VmW2pEP9q3rw","title":"ContentMkt_AVOD_Anime_JujutsuKaisen","thumb":"https://cms.dmpcdn.com/hilight/2023/11/24/88238020-8aac-11ee-873d-3f4e5da5fd9c_webp_original.webp","redirectUrl":"https://movie.trueid.net/series/LgR5wpRnPQVA/qYQ00Mby07Rl/MvKZ6RnxKRaQ/jdAX0Wagxl4R","article_category":null,"content_type":"hilight"},{"id":"BXGpEAn51mwW","title":"ContentMkt_TVOD_Movie_Ambulance","thumb":"https://cms.dmpcdn.com/hilight/2023/11/23/32aa20f0-89cf-11ee-ae81-d157aa7f87b4_webp_original.webp","redirectUrl":"https://movie.trueid.net/movie/Eq63XJL4okzw","article_category":null,"content_type":"hilight"},{"id":"DoLWMe0YeQR8","title":"ContentMkt_AVOD_Anime_DarkGathering","thumb":"https://cms.dmpcdn.com/hilight/2023/11/23/316e93b0-89cf-11ee-a5ec-b3b59a2ebd97_webp_original.webp","redirectUrl":"https://movie.trueid.net/series/d6425Em1DPjQ/QMrQQgYaEDGZ/OAqkmxqX05DQ/1G8xwp6ja86G","article_category":null,"content_type":"hilight"},{"id":"1PddoL4xG12P","title":"ContentMkt_AVOD_Anime_OnePiece","thumb":"https://cms.dmpcdn.com/hilight/2023/11/24/88241c60-8aac-11ee-bf9d-a586c3ad0143_webp_original.webp","redirectUrl":"https://movie.trueid.net/th-th/series/kxqkPYqVBq0D/4AoxBYpn4L1W/p2N5lYZGlVlL/lZ78MWznOElG","article_category":null,"content_type":"hilight"},{"id":"nYa5A41VxmXY","title":"TruelD One Package ความบันเทิงระดับโลก แบบไร้ขีดจำกัด V21-23","thumb":"https://cms.dmpcdn.com/hilight/2023/11/13/84d3ef30-8215-11ee-84cd-3b76e2935cfd_webp_original.webp","redirectUrl":"https://home.trueid.net/external-browser?website=https://myaccount.trueid.net/checkout?promotionCode=SUPERBUNDLE_TID_IQIYI_WETV_PRIME&utm_campaign=Package_NA_NA_TrueIDOne&utm_medium=inside-platform&utm_source=Today_new_release","article_category":null,"content_type":"hilight"},{"id":"VKvARdba10aW","title":"What's Wrong with My Princess","thumb":"https://cms.dmpcdn.com/hilight/2023/11/27/77b75b30-8cd2-11ee-9352-bd2663fbb8ed_webp_original.webp","redirectUrl":"https://movie.trueid.net/series/mP3G7aOXQGXP/QeDwwRKOL2Mp/RvD2p1OpZMRQ/zxnrl6JJnZ2x","article_category":null,"content_type":"hilight"}]},{"id":"k42naQeVKbK4","title":{"th":"","en":""},"type":"by_ads","viewType":"horizontal","shelfItems":[{"ALL":{"targetingArguments":{"TrueID_page":[],"Device":[]},"sizeMapping":[{"viewport":[1280,0],"sizes":[[750,200],[970,90],[728,90],"fluid",[800,250],[970,250],[1,1],[1280,250]]},{"viewport":[375,0],"sizes":[[1,1],[320,250],[375,250],"fluid",[300,250],[320,100]]},{"viewport":[800,0],"sizes":["fluid",[640,250],[800,250],[1,1],[728,90]]},{"viewport":[0,0],"sizes":[[320,50],[320,100],[1,1]]}],"slotId":"div-gpt-ad-lb-1","adUnit":"21682623839/TrueID_Web/TV","sizes":[[970,90],[728,90]]}}]},{"id":"O8pKrLmQlj2a","title":{"th":"ช่องฟรีทีวีฮิต","en":"Free TV"},"type":"by_livetv_channel","viewType":"vertical","shelfItems":[{"id":"wKngqJ2Vqnl","title":"MONO 29","thumb":"https://cms.dmpcdn.com/livetv/2019/01/10/35a35017-8473-4953-8474-5c58d805b74a.png","redirectUrl":"mono29","channel_code":"d43","views":33721,"article_category":["livetv-ca","digitaltv-ca","freetv-ca","movies-series-ca"],"content_type":"livetv","content_rights":"","isLiveChat":false},{"id":"yYk6PvXwXDb","title":"WorkPoint TV","thumb":"https://cms.dmpcdn.com/livetv/2023/11/17/2a1de990-852d-11ee-bf98-41acc8fd04fc_webp_original.webp","redirectUrl":"workpointtv","channel_code":"d83","views":6075,"article_category":["livetv-ca","digitaltv-ca","entertainment-ca","freetv-ca"],"content_type":"livetv","content_rights":"","isLiveChat":false},{"id":"QRP2K658b7G","title":"Thai PBS","thumb":"https://cms.dmpcdn.com/livetv/2023/09/15/ab170410-5377-11ee-8e1b-194edbb69638_webp_original.webp","redirectUrl":"thaipbs","channel_code":"c12","views":2526,"article_category":["livetv-ca","digitaltv-ca","freetv-ca","news-ca"],"content_type":"livetv","content_rights":"","isLiveChat":false},{"id":"vqbr1WgEnGQ","title":"Channel 8","thumb":"https://cms.dmpcdn.com/livetv/2023/09/15/5408a390-5377-11ee-8e1b-194edbb69638_webp_original.webp","redirectUrl":"ch8","channel_code":"d62","views":8295,"article_category":["livetv-ca","digitaltv-ca","entertainment-ca","freetv-ca"],"content_type":"livetv","content_rights":"","isLiveChat":false},{"id":"9O54lyP5Rqx","title":"Channel 7HD","thumb":"https://cms.dmpcdn.com/livetv/2023/07/19/212d15e0-25e7-11ee-bfc1-85e95548413c_webp_original.webp","redirectUrl":"ch7-hd","channel_code":"c07","views":12092,"article_category":["livetv-ca","digitaltv-ca","entertainment-ca","freetv-ca"],"content_type":"livetv","content_rights":"","isLiveChat":false},{"id":"zMLBpX7AWmk","title":"Nation TV","thumb":"https://cms.dmpcdn.com/livetv/2023/09/09/9c6f59c0-4ebe-11ee-99a7-832609069236_webp_original.webp","redirectUrl":"nationtv","channel_code":"d78","views":4733,"article_category":["livetv-ca","digitaltv-ca","freetv-ca","news-ca"],"content_type":"livetv","content_rights":"","isLiveChat":false},{"id":"nQlqONGyoa4","title":"Channel 3","thumb":"https://cms.dmpcdn.com/livetv/2023/07/24/5ff3e270-29cc-11ee-b2f4-e9de482d866e_webp_original.webp","redirectUrl":"ch3-hd","channel_code":"c03","views":96184,"article_category":["livetv-ca","digitaltv-ca","entertainment-ca","freetv-ca"],"content_type":"livetv","content_rights":"","isLiveChat":false},{"id":"5PKobQk5gLOP","title":"Boomerang","thumb":"https://cms.dmpcdn.com/livetv/2023/07/05/b74a2460-1b05-11ee-8ce6-b102b53cb4a2_webp_original.webp","redirectUrl":"boomerang-hd","channel_code":"i007","views":707,"article_category":["livetv-ca","freetv-ca","kids-ca"],"content_type":"livetv","content_rights":"","isLiveChat":false},{"id":"OVKwZle4eop","title":"True4U","thumb":"https://cms.dmpcdn.com/livetv/2023/09/15/84504210-5377-11ee-aaa1-7d584d8ca7a4_webp_original.webp","redirectUrl":"true4u","channel_code":"207","views":6489,"article_category":["livetv-ca","digitaltv-ca","entertainment-ca","freetv-ca","movies-series-ca"],"content_type":"livetv","content_rights":"","isLiveChat":false},{"id":"0z4lvq6Xwoa","title":"One31","thumb":"https://cms.dmpcdn.com/livetv/2019/01/16/396384be-35dc-4d11-bf04-06c9546ec7bc.png","redirectUrl":"one-hd","channel_code":"d56","views":10182,"article_category":["livetv-ca","digitaltv-ca","entertainment-ca","freetv-ca"],"content_type":"livetv","content_rights":"","isLiveChat":false},{"id":"rBWOx89v9Rk","title":"9 MCOT","thumb":"https://cms.dmpcdn.com/livetv/2023/09/09/9cc40970-4ebe-11ee-9801-97f95b5eed9a_webp_original.webp","redirectUrl":"9mcot-hd","channel_code":"c09","views":1323,"article_category":["livetv-ca","digitaltv-ca","freetv-ca"],"content_type":"livetv","content_rights":"","isLiveChat":false}]},{"id":"agbxxnP7GZQ4","title":{"th":"โปรแกรมทีวียอดนิยม","en":"Trending TV Program"},"type":"by_trending_tv_program","viewType":"horizontal","shelfItems":[{"id":"nQlqONGyoa4","title":"แชนแนลทรี ซีรีส์ สายใยรัก เหนือบัลลังก์","thumb":"https://cms.dmpcdn.com/livetv/2023/07/24/5ff3e270-29cc-11ee-b2f4-e9de482d866e_webp_original.webp","thumbLarge":"https://epgthumb.dmpcdn.com/thumbnail_large/c03.jpg?time=1702312743612","redirectUrl":"https://tv.trueid.net/th-en/live/ch3-hd","views":96184,"program_id":"wDGy4q2m963K","program_name":"แชนแนลทรี ซีรีส์ สายใยรัก เหนือบัลลังก์","article_category":["livetv-ca","digitaltv-ca","entertainment-ca","freetv-ca"],"content_type":"livetv","channel_info":{"channel_name_cbd":"ប៉ុស្តិ៍ 3 HD","channel_name_eng":"CH3 HD","channel_name_mm":"CH3 HD","channel_name_th":"ช่อง 3 HD"}},{"id":"8v732AYomo9","title":"ไทยรัฐเจาะประเด็น","thumb":"https://cms.dmpcdn.com/livetv/2023/07/18/7dc7a180-2515-11ee-b8b2-77e2a8f4c31e_webp_original.webp","thumbLarge":"https://epgthumb.dmpcdn.com/thumbnail_large/d05.jpg?time=1702312743612","redirectUrl":"https://tv.trueid.net/th-en/live/thairathtv-hd","views":17228,"program_id":"xkKBjq0VA926","program_name":"ไทยรัฐเจาะประเด็น","article_category":["livetv-ca","digitaltv-ca","freetv-ca","news-ca"],"content_type":"livetv","channel_info":{"channel_name_cbd":"ថៃរ៉ាត់ ធីវី HD","channel_name_eng":"Thairath TV HD","channel_name_mm":"Thairath TV HD","channel_name_th":"ไทยรัฐ ทีวี HD"}},{"id":"0z4lvq6Xwoa","title":"ละคร เสน่หาข้ามเส้น (ตอนอวสาน)","thumb":"https://cms.dmpcdn.com/livetv/2019/01/16/396384be-35dc-4d11-bf04-06c9546ec7bc.png","thumbLarge":"https://epgthumb.dmpcdn.com/thumbnail_large/d56.jpg?time=1702312743612","redirectUrl":"https://tv.trueid.net/th-en/live/one-hd","views":10182,"program_id":"VZ8mMrWEKxQ3","program_name":"ละคร เสน่หาข้ามเส้น (ตอนอวสาน)","article_category":["livetv-ca","digitaltv-ca","entertainment-ca","freetv-ca"],"content_type":"livetv","channel_info":{"channel_name_cbd":"វ័ន HD","channel_name_eng":"One HD","channel_name_mm":"One HD","channel_name_th":"วัน HD"}},{"id":"OVKwZle4eop","title":"ภาพยนตร์ อันธพาล","thumb":"https://cms.dmpcdn.com/livetv/2023/09/15/84504210-5377-11ee-aaa1-7d584d8ca7a4_webp_original.webp","thumbLarge":"https://epgthumb.dmpcdn.com/thumbnail_large/207.jpg?time=1702312743612","redirectUrl":"https://tv.trueid.net/th-en/live/true4u","views":6489,"program_id":"4A8jX4rrX58J","program_name":"ภาพยนตร์ อันธพาล","article_category":["livetv-ca","digitaltv-ca","entertainment-ca","freetv-ca","movies-series-ca"],"content_type":"livetv","channel_info":{"channel_name_cbd":"ទ្រូ4យូ","channel_name_chi":"True4U","channel_name_eng":"True4U","channel_name_mm":"True4U","channel_name_rus":"True4U","channel_name_th":"ทรูโฟร์ยู","channel_name_vie":"True4U"}},{"id":"9O54lyP5Rqx","title":"One Lumpinee Heroes","thumb":"https://cms.dmpcdn.com/livetv/2023/07/19/212d15e0-25e7-11ee-bfc1-85e95548413c_webp_original.webp","thumbLarge":"https://epgthumb.dmpcdn.com/thumbnail_large/c07.jpg?time=1702312743612","redirectUrl":"https://tv.trueid.net/th-en/live/ch7-hd","views":12092,"program_id":"vbQqm8mnpyYb","program_name":"One Lumpinee Heroes","article_category":["livetv-ca","digitaltv-ca","entertainment-ca","freetv-ca"],"content_type":"livetv","channel_info":{"channel_name_cbd":"ប៉ុស្តិ៍​ 7","channel_name_eng":"CH 7HD","channel_name_mm":"Channel 7","channel_name_th":"ช่อง 7HD"}},{"id":"yYk6PvXwXDb","title":"เคลียร์ชัดชัด รีรัน","thumb":"https://cms.dmpcdn.com/livetv/2023/11/17/2a1de990-852d-11ee-bf98-41acc8fd04fc_webp_original.webp","thumbLarge":"https://epgthumb.dmpcdn.com/thumbnail_large/d83.jpg?time=1702312743612","redirectUrl":"https://tv.trueid.net/th-en/live/workpointtv","views":6075,"program_id":"KzKlKXKPDkDY","program_name":"เคลียร์ชัดชัด รีรัน","article_category":["livetv-ca","digitaltv-ca","entertainment-ca","freetv-ca"],"content_type":"livetv","channel_info":{"channel_name_cbd":"វើកភ័ញ គ្រីអ៊ែតធិវ ធីវី​","channel_name_eng":"Workpoint TV","channel_name_mm":"Workpoint TV","channel_name_th":"เวิร์คพอยท์ ทีวี"}},{"id":"OBb6NzoJX7O","title":"ทรูช้อปปิ้ง","thumb":"https://cms.dmpcdn.com/livetv/2023/10/02/d2ec4b30-60f1-11ee-92a4-8597bcef0049_webp_original.webp","thumbLarge":"https://epgthumb.dmpcdn.com/thumbnail_large/da0.jpg?time=1702312743612","redirectUrl":"https://tv.trueid.net/th-en/live/amarintv-hd","views":6407,"program_id":"PwP4AzA5KBXw","program_name":"ทรูช้อปปิ้ง","article_category":["livetv-ca","digitaltv-ca","freetv-ca"],"content_type":"livetv","channel_info":{"channel_name_cbd":"អាម៉ារិន","channel_name_eng":"Amarin TV","channel_name_mm":"Amarin TV","channel_name_th":"อมรินทร์"}},{"id":"vqbr1WgEnGQ","title":"เด็ดมวยเดือด","thumb":"https://cms.dmpcdn.com/livetv/2023/09/15/5408a390-5377-11ee-8e1b-194edbb69638_webp_original.webp","thumbLarge":"https://epgthumb.dmpcdn.com/thumbnail_large/d62.jpg?time=1702312743612","redirectUrl":"https://tv.trueid.net/th-en/live/ch8","views":8295,"program_id":"XJ4NgMgdr7K2","program_name":"เด็ดมวยเดือด","article_category":["livetv-ca","digitaltv-ca","entertainment-ca","freetv-ca"],"content_type":"livetv","channel_info":{"channel_name_cbd":"ប៉ុស្តិ៍ 8","channel_name_eng":"CH8","channel_name_th":"ช่อง 8"}},{"id":"zMLBpX7AWmk","title":"ยุคลชนข่าว","thumb":"https://cms.dmpcdn.com/livetv/2023/09/09/9c6f59c0-4ebe-11ee-99a7-832609069236_webp_original.webp","thumbLarge":"https://epgthumb.dmpcdn.com/thumbnail_large/d78.jpg?time=1702312743612","redirectUrl":"https://tv.trueid.net/th-en/live/nationtv","views":4733,"program_id":"m6ejRJ5pKvdL","program_name":"ยุคลชนข่าว","article_category":["livetv-ca","digitaltv-ca","freetv-ca","news-ca"],"content_type":"livetv","channel_info":{"channel_name_cbd":"Nation TV 22","channel_name_chi":"Nation TV 22","channel_name_eng":"Nation TV 22","channel_name_mm":"Nation TV 22","channel_name_rus":"Nation TV 22","channel_name_th":"เนชั่น ทีวี","channel_name_vie":"Nation TV 22"}},{"id":"QNBwOpdaxpQ","title":"Highlights Bundesliga","thumb":"https://cms.dmpcdn.com/livetv/2023/08/28/012eed00-458a-11ee-bd2b-6734a2d9e428_webp_original.webp","thumbLarge":"https://epgthumb.dmpcdn.com/thumbnail_large/da7.jpg?time=1702312743612","redirectUrl":"https://tv.trueid.net/th-en/live/pptv-hd","views":4723,"program_id":"DwYXj5Jbvex1","program_name":"Highlights Bundesliga","article_category":["livetv-ca","digitaltv-ca","freetv-ca"],"content_type":"livetv","channel_info":{"channel_name_cbd":"ភីភីធីវី","channel_name_eng":"PPTV","channel_name_mm":"PPTV","channel_name_th":"พีพีทีวี"}}]}]},"channelDetail":{"display_country":"th","display_lang":"en","id":"NopZ5gjkGmE","content_type":"livetv","original_id":"279","title":"True Movie Hits","article_category":["livetv-ca","movies-series-ca","trueunlock-ca","tvsnow","movieseries"],"thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/45345d10-e599-11ed-86b8-bb40638e3c49_webp_original.png","tags":null,"status":"publish","count_views":538976,"publish_date":"2020-07-26T17:00:00.000Z","create_date":"2017-10-17T22:01:00.000Z","update_date":"2023-11-26T11:08:14.333Z","searchable":"Y","create_by":"Live TV","create_by_ssoid":null,"update_by":"KANT","update_by_ssoid":"112710659","source_url":null,"count_likes":null,"count_ratings":null,"source_country":null,"channel_code":"057","drm":"WV_FPS","channel_info":{"channel_name_cbd":"True Movie Hits","channel_name_chi":"True Movie Hits","channel_name_eng":"True Movie Hits","channel_name_mm":"True Movie Hits","channel_name_rus":"True Movie Hits","channel_name_th":"True Movie Hits","channel_name_vie":"True Movie Hits"},"lang_dual":"yes","setting":null,"slug":"true-movie-hits","allow_app":["trueidapp","trueidweb","trueidott","hybrid"],"detail":"

      ช่องภาพยนต์ต่างประเทศ รับชมได้ทั้งครบครัวด้วยระบบเสียงภาษาไทย

      ","content_provider":"true_vision","playready":"","score":null},"liveChatConfig":{"channelId":"NopZ5gjkGmE","isLiveChat":false,"slug":"true-movie-hits","disabledChat":false,"supportBrowser":{"chrome_browser_version":{"min_version":83,"live_chat":false},"firefox_browser_version":{"min_version":92,"live_chat":false},"msedge_browser_version":{"min_version":80,"live_chat":false},"off_livechat":false},"disabledChatList":[]},"epgList":[{"id":"w2xyzKz9eyb3","original_id":"057:20231212_020500","content_type":"epg","title":"The Last Witch Hunter","detail":"","status":"publish","channel_code":"057","title_id":"715307","ep_id":"2397606","ep_no":"1","ep_name":"LAST WITCH HUNTER, THE (2015) [MHS] [R]","movie_type":"series","first_run":"Y","cast_type":"tape","start_date":"2023-12-11T19:05:00.000Z","end_date":"2023-12-11T20:55:00.000Z","publish_date":"2023-12-11T10:36:41.801Z","lang":"en","thumb":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_020500.jpg","thumb_list":{"thumb":"https://epgthumb.dmpcdn.com/thumbnail/057/20231212/20231212_020500.jpg","thumb_catchup":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_021000.jpg","thumb_large":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_020500.jpg"},"black_out":0,"catch_up":0,"flag":"N","info":{"channel_name":"TR MOVIE HITS","image":"https://bms.dmpcdn.com/uploads/pic/381f853da5f4a310bf248357fed21a57.jpg","synopsis_en":"A young man is all that stands between humanity and the most horrifying witches in history.","director":"Breck Eisner","title_id":"715307","video":"pBNufkr4KkU","channel_code":"057","type":"Movie","synopsis_th":"หนุ่มนักล่าแม่มดถูกสาปให้เป็นอมตะจนกระทั่งราชินีแม่มดได้ฟื้นคืนชีพขึ้นมาจึงมีเพียงเขาคนเดียวเท่านั้นที่จะสามารถกอบกู้มวลมนุษยชาติได้","imdb_image":"https://bms.dmpcdn.com/uploads/pic/44b0eb2f46eed6a3953014cb5abdbff3.jpg","cast":"Vin Diesel, Rose Leslie, Elijah Wood","genres":"action","program_title":"LAST WITCH HUNTER, THE","production_year":"2015"},"isShowTime":"02:05","isActive":false},{"id":"GPApa0aZzprE","original_id":"057:20231212_035500","content_type":"epg","title":"Point Break","detail":"","status":"publish","channel_code":"057","title_id":"718258","ep_id":"2413906","ep_no":"1","ep_name":"POINT BREAK (2015) [MHS] [R]","movie_type":"series","first_run":"Y","cast_type":"tape","start_date":"2023-12-11T20:55:00.000Z","end_date":"2023-12-11T22:55:00.000Z","publish_date":"2023-12-11T10:36:41.801Z","lang":"en","thumb":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_035500.jpg","thumb_list":{"thumb":"https://epgthumb.dmpcdn.com/thumbnail/057/20231212/20231212_035500.jpg","thumb_catchup":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_040000.jpg","thumb_large":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_035500.jpg"},"black_out":0,"catch_up":0,"flag":"N","info":{"channel_name":"TR MOVIE HITS","image":"https://bms.dmpcdn.com/uploads/pic/e143a6f05ce8e87bf3e7c0f8dfca9914.jpeg","synopsis_en":"An FBI agent infiltrates a gang of thrill-seeking athlete thieves who are suspects in a spate of daring robberies.","director":"Ericson Core","title_id":"718258","video":"jcDD2-s4vWA","channel_code":"057","type":"Movie","synopsis_th":"เรื่องราวของเจ้าหน้าที่เอฟบีไอกับปฏิบัติการสืบสวนเพื่อตามล่าตัวมิจฉาชีพระดับโลกด้วยการแฝงตัวเข้าไปในกลุ่มนักเล่นกระดานโต้คลื่น","imdb_image":"https://bms.dmpcdn.com/uploads/pic/5a0fc22d59c8aef7e9693119687b2172.jpg","cast":"Edgar Ramirez, Luke Bracey, Ray Winstone","genres":"action","program_title":"POINT BREAK","production_year":"2015"},"isShowTime":"03:55","isActive":false},{"id":"Va15bqbQn58a","original_id":"057:20231212_055500","content_type":"epg","title":"The Art of War","detail":"","status":"publish","channel_code":"057","title_id":"712097","ep_id":"2372786","ep_no":"1","ep_name":"ART OF WAR, THE [2000] [MHS]","movie_type":"series","first_run":"Y","cast_type":"tape","start_date":"2023-12-11T22:55:00.000Z","end_date":"2023-12-12T00:55:00.000Z","publish_date":"2023-12-11T10:36:41.801Z","lang":"en","thumb":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_055500.jpg","thumb_list":{"thumb":"https://epgthumb.dmpcdn.com/thumbnail/057/20231212/20231212_055500.jpg","thumb_catchup":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_060000.jpg","thumb_large":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_055500.jpg"},"black_out":0,"catch_up":0,"flag":"N","info":{"channel_name":"TR MOVIE HITS","image":"https://bms.dmpcdn.com/uploads/pic/4f12f9d90f2e8d29b9e427ef415bcb4e.jpg","synopsis_en":"After a US agent is framed for the assassination of the Chinese ambassador, he faces a race against time to catch the real killers.","director":"Christian Duguay","title_id":"712097","video":"rKFmSpB-uGQ","channel_code":"057","type":"Movie","synopsis_th":"เมื่อสายลับถูกใส่ร้ายว่าเป็นฆาตกรเขาจึงต้องหลบหนีการตามไล่ล่าและแข่งกับเวลาเพื่อสืบหาตามล่าฆาตกรตัวจริงให้ได้โดยเร็วที่สุด","imdb_image":"https://bms.dmpcdn.com/uploads/pic/28ab499dffdff191fba497f64131e744.jpg","cast":"Wesley Snipes, Anne Archer, Maury Chaykin","genres":"crime","program_title":"ART OF WAR, THE","production_year":"2000"},"isShowTime":"05:55","isActive":false},{"id":"ALxXy6y32XOL","original_id":"057:20231212_075500","content_type":"epg","title":"The Marksman","detail":"","status":"publish","channel_code":"057","title_id":"726358","ep_id":"2466252","ep_no":"1","ep_name":"MARKSMAN, THE (2021) [MHS]","movie_type":"series","first_run":"Y","cast_type":"tape","start_date":"2023-12-12T00:55:00.000Z","end_date":"2023-12-12T02:50:00.000Z","publish_date":"2023-12-11T10:36:41.801Z","lang":"en","thumb":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_075500.jpg","thumb_list":{"thumb":"https://epgthumb.dmpcdn.com/thumbnail/057/20231212/20231212_075500.jpg","thumb_catchup":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_080000.jpg","thumb_large":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_075500.jpg"},"black_out":0,"catch_up":0,"flag":"N","info":{"channel_name":"TR MOVIE HITS","image":"https://bms.dmpcdn.com/uploads/pic/3945dc2c6cfff5ebeb61f021b58104ab.jpg","synopsis_en":"An Arizona rancher becomes the unlikely defender of a Mexican boy desperately fleeing the cartel assassins who've pursued him into the US.","director":"Robert Lorenz","title_id":"726358","video":"lEBPNi4bEbc","channel_code":"057","type":"Movie","synopsis_th":"อดีตทหารเรือ ที่หนีความวุ่นวายมาใช้ชีวิตอย่างสงบสุขในฟาร์มนอกเมือง แต่กลับต้องไปพัวพันกับสองแม่ลูกที่หลบหนีเอาตัวรอดจากกลุ่มนักฆ่าค้ายา","imdb_image":"https://bms.dmpcdn.com/uploads/pic/573349fd5394caccce8c4f818fdb57b5.jpg","cast":"Liam Neeson, Katheryn Winnick, Juan Pablo Raba","genres":"action","program_title":"MARKSMAN, THE","production_year":"2021"},"isShowTime":"07:55","isActive":false},{"id":"XzDNDnDrXNJ9","original_id":"057:20231212_095000","content_type":"epg","title":"Gods of Egypt","detail":"","status":"publish","channel_code":"057","title_id":"718274","ep_id":"2414078","ep_no":"1","ep_name":"GODS OF EGYPT (2016) [MHS] [R]","movie_type":"series","first_run":"Y","cast_type":"tape","start_date":"2023-12-12T02:50:00.000Z","end_date":"2023-12-12T05:05:00.000Z","publish_date":"2023-12-11T10:36:41.801Z","lang":"en","thumb":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_095000.jpg","thumb_list":{"thumb":"https://epgthumb.dmpcdn.com/thumbnail/057/20231212/20231212_095000.jpg","thumb_catchup":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_095500.jpg","thumb_large":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_095000.jpg"},"black_out":0,"catch_up":0,"flag":"N","info":{"channel_name":"TR MOVIE HITS","image":"https://bms.dmpcdn.com/uploads/pic/e772028844f60526e6dc0fe5b666425a.jpg","synopsis_en":"A young hero teams with the god Horus to fight against the god of darkness, who has usurped Egypt's throne.","director":"Alex Proyas","title_id":"718274","video":"Oijdb-a9GKY","channel_code":"057","type":"Movie","synopsis_th":"เรื่องราวความขัดแย้งและการช่วงชิงที่อุบัติขึ้นท่ามกลางความร้อนระอุแห่งทะเลทรายในดินแดนลุ่มแม่น้ำไนล์อันเต็มไปด้วยมนตรา ทวยเทพ และ เหล่าอสูร","imdb_image":"https://bms.dmpcdn.com/uploads/pic/0a945dc3853317779946f5b9f38269a1.jpg","cast":"Gerard Butler, Brenton Thwaites, Nikolaj Coster-Waldau","genres":"action","program_title":"GODS OF EGYPT","production_year":"2016"},"isShowTime":"09:50","isActive":false},{"id":"4DnvNENgamwD","original_id":"057:20231212_120500","content_type":"epg","title":"The Last Witch Hunter","detail":"","status":"publish","channel_code":"057","title_id":"715307","ep_id":"2397606","ep_no":"1","ep_name":"LAST WITCH HUNTER, THE (2015) [MHS] [R]","movie_type":"series","first_run":"Y","cast_type":"tape","start_date":"2023-12-12T05:05:00.000Z","end_date":"2023-12-12T06:55:00.000Z","publish_date":"2023-12-11T10:36:41.801Z","lang":"en","thumb":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_120500.jpg","thumb_list":{"thumb":"https://epgthumb.dmpcdn.com/thumbnail/057/20231212/20231212_120500.jpg","thumb_catchup":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_121000.jpg","thumb_large":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_120500.jpg"},"black_out":0,"catch_up":0,"flag":"N","info":{"channel_name":"TR MOVIE HITS","image":"https://bms.dmpcdn.com/uploads/pic/381f853da5f4a310bf248357fed21a57.jpg","synopsis_en":"A young man is all that stands between humanity and the most horrifying witches in history.","director":"Breck Eisner","title_id":"715307","video":"pBNufkr4KkU","channel_code":"057","type":"Movie","synopsis_th":"หนุ่มนักล่าแม่มดถูกสาปให้เป็นอมตะจนกระทั่งราชินีแม่มดได้ฟื้นคืนชีพขึ้นมาจึงมีเพียงเขาคนเดียวเท่านั้นที่จะสามารถกอบกู้มวลมนุษยชาติได้","imdb_image":"https://bms.dmpcdn.com/uploads/pic/44b0eb2f46eed6a3953014cb5abdbff3.jpg","cast":"Vin Diesel, Rose Leslie, Elijah Wood","genres":"action","program_title":"LAST WITCH HUNTER, THE","production_year":"2015"},"isShowTime":"12:05","isActive":false},{"id":"AEMb1g1LnzpQ","original_id":"057:20231212_135500","content_type":"epg","title":"Leon","detail":"","status":"publish","channel_code":"057","title_id":"729656","ep_id":"2488591","ep_no":"1","ep_name":"LEON [1994] [MHS]","movie_type":"series","first_run":"Y","cast_type":"tape","start_date":"2023-12-12T06:55:00.000Z","end_date":"2023-12-12T09:05:00.000Z","publish_date":"2023-12-11T10:36:41.801Z","lang":"en","thumb":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_135500.jpg","thumb_list":{"thumb":"https://epgthumb.dmpcdn.com/thumbnail/057/20231212/20231212_135500.jpg","thumb_catchup":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_140000.jpg","thumb_large":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_135500.jpg"},"black_out":0,"catch_up":0,"flag":"N","info":{"channel_name":"TR MOVIE HITS","image":"https://bms.dmpcdn.com/uploads/pic/79eabd21fdb1da338cca6b598de46cde.jpg","synopsis_en":"A hitman forms an unlikely bond with a young girl, teaching her his deadly skills while protecting her from ruthless criminals.","director":"Luc Besson","title_id":"729656","video":"aNQqoExfQsg","channel_code":"057","type":"Movie","synopsis_th":"เรื่องราวของนักฆ่าที่ได้สร้างความผูกพันธ์ที่ไม่น่าจะเป็นไปได้กับเด็กหญิง โดยสอนทักษะอันอันตรายแก่เธอพร้อมทั้งปกป้องเธอจากอาชญากรผู้โหดเหี้ยม","imdb_image":"https://bms.dmpcdn.com/uploads/pic/20be5d12ff2b8f86fb40f9db619d4cb8.jpg","cast":"Jean Reno, Gary Oldman, Natalie Portman","genres":"crime","program_title":"LEON","production_year":"1994"},"isShowTime":"13:55","isActive":false},{"id":"2xe7R1Rgamq4","original_id":"057:20231212_160500","content_type":"epg","title":"Gunpowder Milkshake","detail":"","status":"publish","channel_code":"057","title_id":"710376","ep_id":"2363208","ep_no":"1","ep_name":"GUNPOWDER MILKSHAKE (2021) [MHS]","movie_type":"series","first_run":"Y","cast_type":"tape","start_date":"2023-12-12T09:05:00.000Z","end_date":"2023-12-12T11:00:00.000Z","publish_date":"2023-12-11T10:36:41.801Z","lang":"en","thumb":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_160500.jpg","thumb_list":{"thumb":"https://epgthumb.dmpcdn.com/thumbnail/057/20231212/20231212_160500.jpg","thumb_catchup":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_161000.jpg","thumb_large":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_160500.jpg"},"black_out":0,"catch_up":0,"flag":"N","info":{"channel_name":"TR MOVIE HITS","image":"https://bms.dmpcdn.com/uploads/pic/0039112f2fef876ebf32f9bfb3a9fcf9.jpg","synopsis_en":"Three generations of women fight back against those who aim to take everything from them.","director":"Navot Papushado","title_id":"710376","video":"yxuAroBqt2c","channel_code":"057","type":"Movie","synopsis_th":"เรื่องราวของสามหญิงสามวัยที่ต้องต่อสู้กับผู้ซึ่งแย่งชิงทุกสิ่งทุกอย่างไปจากพวกเธอ","imdb_image":"https://bms.dmpcdn.com/uploads/pic/756c225bc8f5f2ed1268945c979b01a1.jpg","cast":"Karen Gillan, Lena Headey, Carla Gugino","genres":"action","program_title":"GUNPOWDER MILKSHAKE","production_year":"2021"},"isShowTime":"16:05","isActive":false},{"id":"QoeyO1O0Q3no","original_id":"057:20231212_180000","content_type":"epg","title":"Point Break","detail":"","status":"publish","channel_code":"057","title_id":"718258","ep_id":"2413906","ep_no":"1","ep_name":"POINT BREAK (2015) [MHS] [R]","movie_type":"series","first_run":"Y","cast_type":"tape","start_date":"2023-12-12T11:00:00.000Z","end_date":"2023-12-12T13:00:00.000Z","publish_date":"2023-12-11T10:36:41.801Z","lang":"en","thumb":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_180000.jpg","thumb_list":{"thumb":"https://epgthumb.dmpcdn.com/thumbnail/057/20231212/20231212_180000.jpg","thumb_catchup":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_180500.jpg","thumb_large":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_180000.jpg"},"black_out":0,"catch_up":0,"flag":"N","info":{"channel_name":"TR MOVIE HITS","image":"https://bms.dmpcdn.com/uploads/pic/e143a6f05ce8e87bf3e7c0f8dfca9914.jpeg","synopsis_en":"An FBI agent infiltrates a gang of thrill-seeking athlete thieves who are suspects in a spate of daring robberies.","director":"Ericson Core","title_id":"718258","video":"jcDD2-s4vWA","channel_code":"057","type":"Movie","synopsis_th":"เรื่องราวของเจ้าหน้าที่เอฟบีไอกับปฏิบัติการสืบสวนเพื่อตามล่าตัวมิจฉาชีพระดับโลกด้วยการแฝงตัวเข้าไปในกลุ่มนักเล่นกระดานโต้คลื่น","imdb_image":"https://bms.dmpcdn.com/uploads/pic/5a0fc22d59c8aef7e9693119687b2172.jpg","cast":"Edgar Ramirez, Luke Bracey, Ray Winstone","genres":"action","program_title":"POINT BREAK","production_year":"2015"},"isShowTime":"18:00","isActive":false},{"id":"voAargrBvRQo","original_id":"057:20231212_200000","content_type":"epg","title":"Max Steel","detail":"","status":"publish","channel_code":"057","title_id":"718257","ep_id":"2413902","ep_no":"1","ep_name":"MAX STEEL (2016) [MHS] [R]","movie_type":"series","first_run":"Y","cast_type":"tape","start_date":"2023-12-12T13:00:00.000Z","end_date":"2023-12-12T14:35:00.000Z","publish_date":"2023-12-11T10:36:41.801Z","lang":"en","thumb":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_200000.jpg","thumb_list":{"thumb":"https://epgthumb.dmpcdn.com/thumbnail/057/20231212/20231212_200000.jpg","thumb_catchup":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_200500.jpg","thumb_large":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_200000.jpg"},"black_out":0,"catch_up":0,"flag":"N","info":{"channel_name":"TR MOVIE HITS","image":"https://bms.dmpcdn.com/uploads/pic/a10785bc40cd82e82ae702d8a7827393.jpg","synopsis_en":"A young teen and an alien companion harness and combine their tremendous new powers to evolve into the turbo-charged superhero Max Steel.","director":"Stewart Hendler","title_id":"718257","video":"Tf4sa0BVJVw","channel_code":"057","type":"Movie","synopsis_th":"ชายหนุ่มที่ชีวิตต้องแปรเปลี่ยนไปตลอดกาลจากอุบัติเหตุภายในห้องทดลองซึ่งทำให้เขากลายเป็นยอดมนุษย์แกร่ง","imdb_image":"https://bms.dmpcdn.com/uploads/pic/c8e9e6d49546fbde72d0f0b552db97a6.jpg","cast":"Ben Winchell, Josh Brener, Maria Bello","genres":"action","program_title":"MAX STEEL","production_year":"2016"},"isShowTime":"20:00","isActive":false},{"id":"Ena7xBxkNK3z","original_id":"057:20231212_213500","content_type":"epg","title":"Pompeii","detail":"","status":"publish","channel_code":"057","title_id":"715311","ep_id":"2397620","ep_no":"1","ep_name":"POMPEII (2014) [MHS] [R]","movie_type":"series","first_run":"Y","cast_type":"tape","start_date":"2023-12-12T14:35:00.000Z","end_date":"2023-12-12T16:20:00.000Z","publish_date":"2023-12-11T10:36:41.801Z","lang":"en","thumb":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_213500.jpg","thumb_list":{"thumb":"https://epgthumb.dmpcdn.com/thumbnail/057/20231212/20231212_213500.jpg","thumb_catchup":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_214000.jpg","thumb_large":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_213500.jpg"},"black_out":0,"catch_up":0,"flag":"N","info":{"channel_name":"TR MOVIE HITS","image":"https://bms.dmpcdn.com/uploads/pic/28b7c486d1d8c52060413fb58c869c76.jpg","synopsis_en":"Just before the fateful eruption of Mt Vesuvius, a gladiator must save the love of his life from a corrupt Roman.","director":"Paul Anderson","title_id":"715311","video":"t6TRwfxDICM","channel_code":"057","type":"Movie","synopsis_th":"เรื่องราวของหนุ่มนักรบซึ่งเสี่ยงชีพช่วยเหลือหญิงสาวผู้เป็นที่รักจากมหาวิบัติกัมปนาทครั้งใหญ่แห่งประวัติศาสตร์เมื่อภูเขาไฟวิซูเวียสเกิดปะทุขึ้น","imdb_image":"https://bms.dmpcdn.com/uploads/pic/8675b7f9a08f3f0587bed52c7a8015e1.jpg","cast":"Kit Harington, Emily Browning, Kiefer Sutherland","genres":"action","program_title":"POMPEII","production_year":"2014"},"isShowTime":"21:35","isActive":false},{"id":"WNxrPpPwwkQl","original_id":"057:20231212_232000","content_type":"epg","title":"Leon","detail":"","status":"publish","channel_code":"057","title_id":"729656","ep_id":"2488591","ep_no":"1","ep_name":"LEON [1994] [MHS]","movie_type":"series","first_run":"Y","cast_type":"tape","start_date":"2023-12-12T16:20:00.000Z","end_date":"2023-12-12T18:30:00.000Z","publish_date":"2023-12-11T10:36:41.801Z","lang":"en","thumb":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_232000.jpg","thumb_list":{"thumb":"https://epgthumb.dmpcdn.com/thumbnail/057/20231212/20231212_232000.jpg","thumb_catchup":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_232500.jpg","thumb_large":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_232000.jpg"},"black_out":0,"catch_up":0,"flag":"N","info":{"channel_name":"TR MOVIE HITS","image":"https://bms.dmpcdn.com/uploads/pic/79eabd21fdb1da338cca6b598de46cde.jpg","synopsis_en":"A hitman forms an unlikely bond with a young girl, teaching her his deadly skills while protecting her from ruthless criminals.","director":"Luc Besson","title_id":"729656","video":"aNQqoExfQsg","channel_code":"057","type":"Movie","synopsis_th":"เรื่องราวของนักฆ่าที่ได้สร้างความผูกพันธ์ที่ไม่น่าจะเป็นไปได้กับเด็กหญิง โดยสอนทักษะอันอันตรายแก่เธอพร้อมทั้งปกป้องเธอจากอาชญากรผู้โหดเหี้ยม","imdb_image":"https://bms.dmpcdn.com/uploads/pic/20be5d12ff2b8f86fb40f9db619d4cb8.jpg","cast":"Jean Reno, Gary Oldman, Natalie Portman","genres":"crime","program_title":"LEON","production_year":"1994"},"isShowTime":"23:20","isActive":false}],"audioData":{"lang_locale":"","voice_commentary":""},"playerLanguage":{"data":{"aa":"Afar","ab":"Abkhazian","af":"Afrikaans","ak":"Akan","am":"Amharic","ar":"Arabic","an":"Aragonese","as":"Assamese","av":"Avaric","ae":"Avestan","ay":"Aymara","az":"Azerbaijani","ba":"Bashkir","bm":"Bambara","be":"Belarusian","bn":"Bengali","bh":"Biharilanguages","bi":"Bislama","bs":"Bosnian","br":"Breton","bg":"Bulgarian","ca":"CatalanValencian","ch":"Chamorro","ce":"Chechen","cu":"ChurchSlavicOldSlavonicChurchSlavonicOldBulgarianOldChurchSlavonic","cv":"Chuvash","kw":"Cornish","co":"Corsican","cr":"Cree","cs":"Czech","da":"Danish","dv":"DivehiDhivehiMaldivian","dz":"Dzongkha","en":"English","eo":"Esperanto","et":"Estonian","eu":"Basque","ee":"Ewe","fo":"Faroese","fj":"Fijian","fi":"Finnish","fr":"French","fy":"WesternFrisian","ff":"Fulah","de":"German","gd":"GaelicScottishGaelic","ga":"Irish","gl":"Galician","gv":"Manx","el":"Greek,Modern(1453-)","gn":"Guarani","gu":"Gujarati","ht":"HaitianHaitianCreole","ha":"Hausa","he":"Hebrew","hz":"Herero","hi":"Hindi","ho":"HiriMotu","hr":"Croatian","hu":"Hungarian","hy":"Armenian","ig":"Igbo","io":"Ido","ii":"SichuanYiNuosu","iu":"Inuktitut","ie":"InterlingueOccidental","ia":"Interlingua(InternationalAuxiliaryLanguageAssociation)","id":"Indonesian","ik":"Inupiaq","is":"Icelandic","it":"Italian","jv":"Javanese","ja":"Japanese","kl":"KalaallisutGreenlandic","kn":"Kannada","ks":"Kashmiri","ka":"Georgian","kr":"Kanuri","kk":"Kazakh","km":"CentralKhmer","ki":"KikuyuGikuyu","rw":"Kinyarwanda","ky":"KirghizKyrgyz","kv":"Komi","kg":"Kongo","ko":"Korean","kj":"KuanyamaKwanyama","ku":"Kurdish","lo":"Lao","la":"Latin","lv":"Latvian","li":"LimburganLimburgerLimburgish","ln":"Lingala","lt":"Lithuanian","lb":"LuxembourgishLetzeburgesch","lu":"Luba-Katanga","lg":"Ganda","mh":"Marshallese","ml":"Malayalam","mr":"Marathi","mk":"Macedonian","mg":"Malagasy","mt":"Maltese","mn":"Mongolian","mi":"Maori","ms":"Malay","my":"Burmese","na":"Nauru","nv":"NavajoNavaho","nr":"Ndebele,SouthSouthNdebele","nd":"Ndebele,NorthNorthNdebele","ng":"Ndonga","ne":"Nepali","nl":"DutchFlemish","nn":"NorwegianNynorskNynorsk,Norwegian","nb":"Bokmål,NorwegianNorwegianBokmål","no":"Norwegian","ny":"ChichewaChewaNyanja","oc":"Occitan(post1500)","oj":"Ojibwa","or":"Oriya","om":"Oromo","os":"OssetianOssetic","pa":"PanjabiPunjabi","fa":"Persian","pi":"Pali","pl":"Polish","pt":"Portuguese","ps":"PushtoPashto","qu":"Quechua","rm":"Romansh","ro":"RomanianMoldavianMoldovan","rn":"Rundi","ru":"Russian","sg":"Sango","sa":"Sanskrit","si":"SinhalaSinhalese","sk":"Slovak","sl":"Slovenian","se":"NorthernSami","sm":"Samoan","sn":"Shona","sd":"Sindhi","so":"Somali","st":"Sotho,Southern","es":"SpanishCastilian","sq":"Albanian","sc":"Sardinian","sr":"Serbian","ss":"Swati","su":"Sundanese","sw":"Swahili","sv":"Swedish","ty":"Tahitian","ta":"Tamil","tt":"Tatar","te":"Telugu","tg":"Tajik","tl":"Tagalog","th":"Thai","bo":"Tibetan","ti":"Tigrinya","to":"Tonga(TongaIslands)","tn":"Tswana","ts":"Tsonga","tk":"Turkmen","tr":"Turkish","tw":"Twi","ug":"UighurUyghur","uk":"Ukrainian","ur":"Urdu","uz":"Uzbek","ve":"Venda","vi":"Vietnamese","vo":"Volapük","cy":"Welsh","wa":"Walloon","wo":"Wolof","xh":"Xhosa","yi":"Yiddish","yo":"Yoruba","za":"ZhuangChuang","zh":"Chinese","zu":"Zulu","aar":"Afar","abk":"Abkhazian","ace":"Achinese","ach":"Acoli","ada":"Adangme","ady":"AdygheAdygei","afa":"Afro-Asiaticlanguages","afh":"Afrihili","afr":"Afrikaans","ain":"Ainu","aka":"Akan","akk":"Akkadian","ale":"Aleut","alg":"Algonquianlanguages","alt":"SouthernAltai","amh":"Amharic","ang":"English,Old(ca.450-1100)","anp":"Angika","apa":"Apachelanguages","ara":"Arabic","arc":"OfficialAramaic(700-300BCE)ImperialAramaic(700-300BCE)","arg":"Aragonese","arn":"MapudungunMapuche","arp":"Arapaho","art":"Artificiallanguages","arw":"Arawak","asm":"Assamese","ast":"AsturianBableLeoneseAsturleonese","ath":"Athapascanlanguages","aus":"Australianlanguages","ava":"Avaric","ave":"Avestan","awa":"Awadhi","aym":"Aymara","aze":"Azerbaijani","bad":"Bandalanguages","bai":"Bamilekelanguages","bak":"Bashkir","bal":"Baluchi","bam":"Bambara","ban":"Balinese","bas":"Basa","bat":"Balticlanguages","bej":"BejaBedawiyet","bel":"Belarusian","bem":"Bemba","ben":"Bengali","ber":"Berberlanguages","bho":"Bhojpuri","bih":"Biharilanguages","bik":"Bikol","bin":"BiniEdo","bis":"Bislama","bla":"Siksika","bnt":"Bantulanguages","bos":"Bosnian","bra":"Braj","bre":"Breton","btk":"Bataklanguages","bua":"Buriat","bug":"Buginese","bul":"Bulgarian","bur(B)mya(T)":"Burmese","byn":"BlinBilin","cad":"Caddo","cai":"CentralAmericanIndianlanguages","car":"GalibiCarib","cat":"CatalanValencian","cau":"Caucasianlanguages","ceb":"Cebuano","cel":"Celticlanguages","cha":"Chamorro","chb":"Chibcha","che":"Chechen","chg":"Chagatai","chk":"Chuukese","chm":"Mari","chn":"Chinookjargon","cho":"Choctaw","chp":"ChipewyanDeneSuline","chr":"Cherokee","chu":"ChurchSlavicOldSlavonicChurchSlavonicOldBulgarianOldChurchSlavonic","chv":"Chuvash","chy":"Cheyenne","cmc":"Chamiclanguages","cnr":"Montenegrin","cop":"Coptic","cor":"Cornish","cos":"Corsican","cpe":"Creolesandpidgins,Englishbased","cpf":"Creolesandpidgins,French-based","cpp":"Creolesandpidgins,Portuguese-based","cre":"Cree","crh":"CrimeanTatarCrimeanTurkish","crp":"Creolesandpidgins","csb":"Kashubian","cus":"Cushiticlanguages","cze(B)ces(T)":"Czech","dak":"Dakota","dan":"Danish","dar":"Dargwa","day":"LandDayaklanguages","del":"Delaware","den":"Slave(Athapascan)","dgr":"Dogrib","din":"Dinka","div":"DivehiDhivehiMaldivian","doi":"Dogri","dra":"Dravidianlanguages","dsb":"LowerSorbian","dua":"Duala","dum":"Dutch,Middle(ca.1050-1350)","dyu":"Dyula","dzo":"Dzongkha","efi":"Efik","egy":"Egyptian(Ancient)","eka":"Ekajuk","elx":"Elamite","eng":"English","enm":"English,Middle(1100-1500)","epo":"Esperanto","est":"Estonian","baq(B)eus(T)":"Basque","ewo":"Ewondo","fan":"Fang","fao":"Faroese","fat":"Fanti","fij":"Fijian","fil":"FilipinoPilipino","fin":"Finnish","fiu":"Finno-Ugrianlanguages","fre(B)fra(T)":"French","frm":"French,Middle(ca.1400-1600)","fro":"French,Old(842-ca.1400)","frr":"NorthernFrisian","frs":"EasternFrisian","fry":"WesternFrisian","ful":"Fulah","fur":"Friulian","gaa":"Ga","gay":"Gayo","gba":"Gbaya","gem":"Germaniclanguages","ger(B)deu(T)":"German","gez":"Geez","gil":"Gilbertese","gla":"GaelicScottishGaelic","gle":"Irish","glg":"Galician","glv":"Manx","gmh":"German,MiddleHigh(ca.1050-1500)","goh":"German,OldHigh(ca.750-1050)","gon":"Gondi","gor":"Gorontalo","got":"Gothic","grb":"Grebo","grc":"Greek,Ancient(to1453)","gre(B)ell(T)":"Greek,Modern(1453-)","grn":"Guarani","gsw":"SwissGermanAlemannicAlsatian","guj":"Gujarati","gwi":"Gwich'in","hai":"Haida","hat":"HaitianHaitianCreole","hau":"Hausa","haw":"Hawaiian","heb":"Hebrew","her":"Herero","hil":"Hiligaynon","him":"HimachalilanguagesWesternPaharilanguages","hin":"Hindi","hit":"Hittite","hmn":"HmongMong","hmo":"HiriMotu","hrv":"Croatian","hsb":"UpperSorbian","hun":"Hungarian","hup":"Hupa","arm(B)hye(T)":"Armenian","iba":"Iban","ibo":"Igbo","iii":"SichuanYiNuosu","ijo":"Ijolanguages","iku":"Inuktitut","ile":"InterlingueOccidental","ilo":"Iloko","ina":"Interlingua(InternationalAuxiliaryLanguageAssociation)","inc":"Indiclanguages","ind":"Indonesian","ine":"Indo-Europeanlanguages","inh":"Ingush","ipk":"Inupiaq","ira":"Iranianlanguages","iro":"Iroquoianlanguages","ice(B)isl(T)":"Icelandic","ita":"Italian","jav":"Javanese","jbo":"Lojban","jpn":"Japanese","jpr":"Judeo-Persian","jrb":"Judeo-Arabic","kaa":"Kara-Kalpak","kab":"Kabyle","kac":"KachinJingpho","kal":"KalaallisutGreenlandic","kam":"Kamba","kan":"Kannada","kar":"Karenlanguages","kas":"Kashmiri","geo(B)kat(T)":"Georgian","kau":"Kanuri","kaw":"Kawi","kaz":"Kazakh","kbd":"Kabardian","kha":"Khasi","khi":"Khoisanlanguages","khm":"CentralKhmer","kho":"KhotaneseSakan","kik":"KikuyuGikuyu","kin":"Kinyarwanda","kir":"KirghizKyrgyz","kmb":"Kimbundu","kok":"Konkani","kom":"Komi","kon":"Kongo","kor":"Korean","kos":"Kosraean","kpe":"Kpelle","krc":"Karachay-Balkar","krl":"Karelian","kro":"Krulanguages","kru":"Kurukh","kua":"KuanyamaKwanyama","kum":"Kumyk","kur":"Kurdish","kut":"Kutenai","lad":"Ladino","lah":"Lahnda","lam":"Lamba","lat":"Latin","lav":"Latvian","lez":"Lezghian","lim":"LimburganLimburgerLimburgish","lin":"Lingala","lit":"Lithuanian","lol":"Mongo","loz":"Lozi","ltz":"LuxembourgishLetzeburgesch","lua":"Luba-Lulua","lub":"Luba-Katanga","lug":"Ganda","lui":"Luiseno","lun":"Lunda","luo":"Luo(KenyaandTanzania)","lus":"Lushai","mac(B)mkd(T)":"Macedonian","mad":"Madurese","mag":"Magahi","mah":"Marshallese","mai":"Maithili","mak":"Makasar","mal":"Malayalam","man":"Mandingo","mao(B)mri(T)":"Maori","map":"Austronesianlanguages","mar":"Marathi","mas":"Masai","may(B)msa(T)":"Malay","mdf":"Moksha","mdr":"Mandar","men":"Mende","mga":"Irish,Middle(900-1200)","mic":"Mi'kmaqMicmac","min":"Minangkabau","mis":"Uncodedlanguages","mkh":"Mon-Khmerlanguages","mlg":"Malagasy","mlt":"Maltese","mnc":"Manchu","mni":"Manipuri","mno":"Manobolanguages","moh":"Mohawk","mon":"Mongolian","mos":"Mossi","mul":"Multiplelanguages","mun":"Mundalanguages","mus":"Creek","mwl":"Mirandese","mwr":"Marwari","myn":"Mayanlanguages","myv":"Erzya","nah":"Nahuatllanguages","nai":"NorthAmericanIndianlanguages","nap":"Neapolitan","nau":"Nauru","nav":"NavajoNavaho","nbl":"Ndebele,SouthSouthNdebele","nde":"Ndebele,NorthNorthNdebele","ndo":"Ndonga","nds":"LowGermanLowSaxonGerman,LowSaxon,Low","nep":"Nepali","new":"NepalBhasaNewari","nia":"Nias","nic":"Niger-Kordofanianlanguages","niu":"Niuean","dut(B)nld(T)":"DutchFlemish","nno":"NorwegianNynorskNynorsk,Norwegian","nob":"Bokmål,NorwegianNorwegianBokmål","nog":"Nogai","non":"Norse,Old","nor":"Norwegian","nqo":"N'Ko","nso":"PediSepediNorthernSotho","nub":"Nubianlanguages","nwc":"ClassicalNewariOldNewariClassicalNepalBhasa","nya":"ChichewaChewaNyanja","nym":"Nyamwezi","nyn":"Nyankole","nyo":"Nyoro","nzi":"Nzima","oci":"Occitan(post1500)","oji":"Ojibwa","ori":"Oriya","orm":"Oromo","osa":"Osage","oss":"OssetianOssetic","ota":"Turkish,Ottoman(1500-1928)","oto":"Otomianlanguages","paa":"Papuanlanguages","pag":"Pangasinan","pal":"Pahlavi","pam":"PampangaKapampangan","pan":"PanjabiPunjabi","pap":"Papiamento","pau":"Palauan","peo":"Persian,Old(ca.600-400B.C.)","per(B)fas(T)":"Persian","phi":"Philippinelanguages","phn":"Phoenician","pli":"Pali","pol":"Polish","pon":"Pohnpeian","por":"Portuguese","pra":"Prakritlanguages","pro":"Provençal,Old(to1500)Occitan,Old(to1500)","pus":"PushtoPashto","qaa-qtz":"Reservedforlocaluse","que":"Quechua","raj":"Rajasthani","rap":"Rapanui","rar":"RarotonganCookIslandsMaori","roa":"Romancelanguages","roh":"Romansh","rom":"Romany","rum(B)ron(T)":"RomanianMoldavianMoldovan","run":"Rundi","rup":"AromanianArumanianMacedo-Romanian","rus":"Russian","sad":"Sandawe","sag":"Sango","sah":"Yakut","sai":"SouthAmericanIndianlanguages","sal":"Salishanlanguages","sam":"SamaritanAramaic","san":"Sanskrit","sas":"Sasak","sat":"Santali","scn":"Sicilian","sco":"Scots","sel":"Selkup","sem":"Semiticlanguages","sga":"Irish,Old(to900)","sgn":"SignLanguages","shn":"Shan","sid":"Sidamo","sin":"SinhalaSinhalese","sio":"Siouanlanguages","sit":"Sino-Tibetanlanguages","sla":"Slaviclanguages","slo(B)slk(T)":"Slovak","slv":"Slovenian","sma":"SouthernSami","sme":"NorthernSami","smi":"Samilanguages","smj":"LuleSami","smn":"InariSami","smo":"Samoan","sms":"SkoltSami","sna":"Shona","snd":"Sindhi","snk":"Soninke","sog":"Sogdian","som":"Somali","son":"Songhailanguages","sot":"Sotho,Southern","spa":"SpanishCastilian","alb(B)sqi(T)":"Albanian","srd":"Sardinian","srn":"SrananTongo","srp":"Serbian","srr":"Serer","ssa":"Nilo-Saharanlanguages","ssw":"Swati","suk":"Sukuma","sun":"Sundanese","sus":"Susu","sux":"Sumerian","swa":"Swahili","swe":"Swedish","syc":"ClassicalSyriac","syr":"Syriac","tah":"Tahitian","tai":"Tailanguages","tam":"Tamil","tat":"Tatar","tel":"Telugu","tem":"Timne","ter":"Tereno","tet":"Tetum","tgk":"Tajik","tgl":"Tagalog","tha":"Thai","tib(B)bod(T)":"Tibetan","tig":"Tigre","tir":"Tigrinya","tiv":"Tiv","tkl":"Tokelau","tlh":"KlingontlhIngan-Hol","tli":"Tlingit","tmh":"Tamashek","tog":"Tonga(Nyasa)","ton":"Tonga(TongaIslands)","tpi":"TokPisin","tsi":"Tsimshian","tsn":"Tswana","tso":"Tsonga","tuk":"Turkmen","tum":"Tumbuka","tup":"Tupilanguages","tur":"Turkish","tut":"Altaiclanguages","tvl":"Tuvalu","tyv":"Tuvinian","udm":"Udmurt","uga":"Ugaritic","uig":"UighurUyghur","ukr":"Ukrainian","umb":"Umbundu","und":"Undetermined","urd":"Urdu","uzb":"Uzbek","ven":"Venda","vie":"Vietnamese","vol":"Volapük","vot":"Votic","wak":"Wakashanlanguages","wal":"WolaittaWolaytta","war":"Waray","was":"Washo","wel(B)cym(T)":"Welsh","wen":"Sorbianlanguages","wln":"Walloon","wol":"Wolof","xal":"KalmykOirat","xho":"Xhosa","yap":"Yapese","yid":"Yiddish","yor":"Yoruba","ypk":"Yupiklanguages","zap":"Zapotec","zbl":"BlissymbolsBlissymbolicsBliss","zen":"Zenaga","zgh":"StandardMoroccanTamazight","zha":"ZhuangChuang","chi(B)zho(T)":"Chinese","chi":"Chinese","znd":"Zandelanguages","zul":"Zulu","zun":"Zuni","zxx":"NolinguisticcontentNotapplicable","zza":"ZazaDimiliDimliKirdkiKirmanjkiZazaki","afar":"Afar","abkhazian":"Abkhazian","achinese":"Achinese","acoli":"Acoli","adangme":"Adangme","adygheadygei":"AdygheAdygei","afro-asiaticlanguages":"Afro-Asiaticlanguages","afrihili":"Afrihili","afrikaans":"Afrikaans","ainu":"Ainu","akan":"Akan","akkadian":"Akkadian","aleut":"Aleut","algonquianlanguages":"Algonquianlanguages","southernaltai":"SouthernAltai","amharic":"Amharic","english,old(ca.450-1100)":"English,Old(ca.450-1100)","angika":"Angika","apachelanguages":"Apachelanguages","arabic":"Arabic","officialaramaic(700-300bce)imperialaramaic(700-300bce)":"OfficialAramaic(700-300BCE)ImperialAramaic(700-300BCE)","aragonese":"Aragonese","mapudungunmapuche":"MapudungunMapuche","arapaho":"Arapaho","artificiallanguages":"Artificiallanguages","arawak":"Arawak","assamese":"Assamese","asturianbableleoneseasturleonese":"AsturianBableLeoneseAsturleonese","athapascanlanguages":"Athapascanlanguages","australianlanguages":"Australianlanguages","avaric":"Avaric","avestan":"Avestan","awadhi":"Awadhi","aymara":"Aymara","azerbaijani":"Azerbaijani","bandalanguages":"Bandalanguages","bamilekelanguages":"Bamilekelanguages","bashkir":"Bashkir","baluchi":"Baluchi","bambara":"Bambara","balinese":"Balinese","basa":"Basa","balticlanguages":"Balticlanguages","bejabedawiyet":"BejaBedawiyet","belarusian":"Belarusian","bemba":"Bemba","bengali":"Bengali","berberlanguages":"Berberlanguages","bhojpuri":"Bhojpuri","biharilanguages":"Biharilanguages","bikol":"Bikol","biniedo":"BiniEdo","bislama":"Bislama","siksika":"Siksika","bantulanguages":"Bantulanguages","bosnian":"Bosnian","braj":"Braj","breton":"Breton","bataklanguages":"Bataklanguages","buriat":"Buriat","buginese":"Buginese","bulgarian":"Bulgarian","blinbilin":"BlinBilin","caddo":"Caddo","centralamericanindianlanguages":"CentralAmericanIndianlanguages","galibicarib":"GalibiCarib","catalanvalencian":"CatalanValencian","caucasianlanguages":"Caucasianlanguages","cebuano":"Cebuano","celticlanguages":"Celticlanguages","chamorro":"Chamorro","chibcha":"Chibcha","chechen":"Chechen","chagatai":"Chagatai","chuukese":"Chuukese","mari":"Mari","chinookjargon":"Chinookjargon","choctaw":"Choctaw","chipewyandenesuline":"ChipewyanDeneSuline","cherokee":"Cherokee","churchslavicoldslavonicchurchslavonicoldbulgarianoldchurchslavonic":"ChurchSlavicOldSlavonicChurchSlavonicOldBulgarianOldChurchSlavonic","chuvash":"Chuvash","cheyenne":"Cheyenne","chamiclanguages":"Chamiclanguages","montenegrin":"Montenegrin","coptic":"Coptic","cornish":"Cornish","corsican":"Corsican","creolesandpidgins,englishbased":"Creolesandpidgins,Englishbased","creolesandpidgins,french-based":"Creolesandpidgins,French-based","creolesandpidgins,portuguese-based":"Creolesandpidgins,Portuguese-based","cree":"Cree","crimeantatarcrimeanturkish":"CrimeanTatarCrimeanTurkish","creolesandpidgins":"Creolesandpidgins","kashubian":"Kashubian","cushiticlanguages":"Cushiticlanguages","czech":"Czech","dakota":"Dakota","danish":"Danish","dargwa":"Dargwa","landdayaklanguages":"LandDayaklanguages","delaware":"Delaware","slave(athapascan)":"Slave(Athapascan)","dogrib":"Dogrib","dinka":"Dinka","divehidhivehimaldivian":"DivehiDhivehiMaldivian","dogri":"Dogri","dravidianlanguages":"Dravidianlanguages","lowersorbian":"LowerSorbian","duala":"Duala","dutch,middle(ca.1050-1350)":"Dutch,Middle(ca.1050-1350)","dyula":"Dyula","dzongkha":"Dzongkha","efik":"Efik","egyptian(ancient)":"Egyptian(Ancient)","ekajuk":"Ekajuk","elamite":"Elamite","english":"English","english,middle(1100-1500)":"English,Middle(1100-1500)","esperanto":"Esperanto","estonian":"Estonian","basque":"Basque","ewe":"Ewe","ewondo":"Ewondo","fang":"Fang","faroese":"Faroese","fanti":"Fanti","fijian":"Fijian","filipinopilipino":"FilipinoPilipino","finnish":"Finnish","finno-ugrianlanguages":"Finno-Ugrianlanguages","fon":"Fon","french":"French","french,middle(ca.1400-1600)":"French,Middle(ca.1400-1600)","french,old(842-ca.1400)":"French,Old(842-ca.1400)","northernfrisian":"NorthernFrisian","easternfrisian":"EasternFrisian","westernfrisian":"WesternFrisian","fulah":"Fulah","friulian":"Friulian","gayo":"Gayo","gbaya":"Gbaya","germaniclanguages":"Germaniclanguages","german":"German","geez":"Geez","gilbertese":"Gilbertese","gaelicscottishgaelic":"GaelicScottishGaelic","irish":"Irish","galician":"Galician","manx":"Manx","german,middlehigh(ca.1050-1500)":"German,MiddleHigh(ca.1050-1500)","german,oldhigh(ca.750-1050)":"German,OldHigh(ca.750-1050)","gondi":"Gondi","gorontalo":"Gorontalo","gothic":"Gothic","grebo":"Grebo","greek,ancient(to1453)":"Greek,Ancient(to1453)","greek,modern(1453-)":"Greek,Modern(1453-)","guarani":"Guarani","swissgermanalemannicalsatian":"SwissGermanAlemannicAlsatian","gujarati":"Gujarati","gwich'in":"Gwich'in","haida":"Haida","haitianhaitiancreole":"HaitianHaitianCreole","hausa":"Hausa","hawaiian":"Hawaiian","hebrew":"Hebrew","herero":"Herero","hiligaynon":"Hiligaynon","himachalilanguageswesternpaharilanguages":"HimachalilanguagesWesternPaharilanguages","hindi":"Hindi","hittite":"Hittite","hmongmong":"HmongMong","hirimotu":"HiriMotu","croatian":"Croatian","uppersorbian":"UpperSorbian","hungarian":"Hungarian","hupa":"Hupa","armenian":"Armenian","iban":"Iban","igbo":"Igbo","ido":"Ido","sichuanyinuosu":"SichuanYiNuosu","ijolanguages":"Ijolanguages","inuktitut":"Inuktitut","interlingueoccidental":"InterlingueOccidental","iloko":"Iloko","interlingua(internationalauxiliarylanguageassociation)":"Interlingua(InternationalAuxiliaryLanguageAssociation)","indiclanguages":"Indiclanguages","indonesian":"Indonesian","indo-europeanlanguages":"Indo-Europeanlanguages","ingush":"Ingush","inupiaq":"Inupiaq","iranianlanguages":"Iranianlanguages","iroquoianlanguages":"Iroquoianlanguages","icelandic":"Icelandic","italian":"Italian","javanese":"Javanese","lojban":"Lojban","japanese":"Japanese","judeo-persian":"Judeo-Persian","judeo-arabic":"Judeo-Arabic","kara-kalpak":"Kara-Kalpak","kabyle":"Kabyle","kachinjingpho":"KachinJingpho","kalaallisutgreenlandic":"KalaallisutGreenlandic","kamba":"Kamba","kannada":"Kannada","karenlanguages":"Karenlanguages","kashmiri":"Kashmiri","georgian":"Georgian","kanuri":"Kanuri","kawi":"Kawi","kazakh":"Kazakh","kabardian":"Kabardian","khasi":"Khasi","khoisanlanguages":"Khoisanlanguages","centralkhmer":"CentralKhmer","khotanesesakan":"KhotaneseSakan","kikuyugikuyu":"KikuyuGikuyu","kinyarwanda":"Kinyarwanda","kirghizkyrgyz":"KirghizKyrgyz","kimbundu":"Kimbundu","konkani":"Konkani","komi":"Komi","kongo":"Kongo","korean":"Korean","kosraean":"Kosraean","kpelle":"Kpelle","karachay-balkar":"Karachay-Balkar","karelian":"Karelian","krulanguages":"Krulanguages","kurukh":"Kurukh","kuanyamakwanyama":"KuanyamaKwanyama","kumyk":"Kumyk","kurdish":"Kurdish","kutenai":"Kutenai","ladino":"Ladino","lahnda":"Lahnda","lamba":"Lamba","lao":"Lao","latin":"Latin","latvian":"Latvian","lezghian":"Lezghian","limburganlimburgerlimburgish":"LimburganLimburgerLimburgish","lingala":"Lingala","lithuanian":"Lithuanian","mongo":"Mongo","lozi":"Lozi","luxembourgishletzeburgesch":"LuxembourgishLetzeburgesch","luba-lulua":"Luba-Lulua","luba-katanga":"Luba-Katanga","ganda":"Ganda","luiseno":"Luiseno","lunda":"Lunda","luo(kenyaandtanzania)":"Luo(KenyaandTanzania)","lushai":"Lushai","madurese":"Madurese","magahi":"Magahi","marshallese":"Marshallese","maithili":"Maithili","makasar":"Makasar","malayalam":"Malayalam","mandingo":"Mandingo","austronesianlanguages":"Austronesianlanguages","marathi":"Marathi","masai":"Masai","moksha":"Moksha","mandar":"Mandar","mende":"Mende","irish,middle(900-1200)":"Irish,Middle(900-1200)","mi'kmaqmicmac":"Mi'kmaqMicmac","minangkabau":"Minangkabau","uncodedlanguages":"Uncodedlanguages","macedonian":"Macedonian","mon-khmerlanguages":"Mon-Khmerlanguages","malagasy":"Malagasy","maltese":"Maltese","manchu":"Manchu","manipuri":"Manipuri","manobolanguages":"Manobolanguages","mohawk":"Mohawk","mongolian":"Mongolian","mossi":"Mossi","maori":"Maori","malay":"Malay","multiplelanguages":"Multiplelanguages","mundalanguages":"Mundalanguages","creek":"Creek","mirandese":"Mirandese","marwari":"Marwari","burmese":"Burmese","mayanlanguages":"Mayanlanguages","erzya":"Erzya","nahuatllanguages":"Nahuatllanguages","northamericanindianlanguages":"NorthAmericanIndianlanguages","neapolitan":"Neapolitan","nauru":"Nauru","navajonavaho":"NavajoNavaho","ndebele,southsouthndebele":"Ndebele,SouthSouthNdebele","ndebele,northnorthndebele":"Ndebele,NorthNorthNdebele","ndonga":"Ndonga","lowgermanlowsaxongerman,lowsaxon,low":"LowGermanLowSaxonGerman,LowSaxon,Low","nepali":"Nepali","nepalbhasanewari":"NepalBhasaNewari","nias":"Nias","niger-kordofanianlanguages":"Niger-Kordofanianlanguages","niuean":"Niuean","dutchflemish":"DutchFlemish","norwegiannynorsknynorsk,norwegian":"NorwegianNynorskNynorsk,Norwegian","bokmål,norwegiannorwegianbokmål":"Bokmål,NorwegianNorwegianBokmål","nogai":"Nogai","norse,old":"Norse,Old","norwegian":"Norwegian","n'ko":"N'Ko","pedisepedinorthernsotho":"PediSepediNorthernSotho","nubianlanguages":"Nubianlanguages","classicalnewarioldnewariclassicalnepalbhasa":"ClassicalNewariOldNewariClassicalNepalBhasa","chichewachewanyanja":"ChichewaChewaNyanja","nyamwezi":"Nyamwezi","nyankole":"Nyankole","nyoro":"Nyoro","nzima":"Nzima","occitan(post1500)":"Occitan(post1500)","ojibwa":"Ojibwa","oriya":"Oriya","oromo":"Oromo","osage":"Osage","ossetianossetic":"OssetianOssetic","turkish,ottoman(1500-1928)":"Turkish,Ottoman(1500-1928)","otomianlanguages":"Otomianlanguages","papuanlanguages":"Papuanlanguages","pangasinan":"Pangasinan","pahlavi":"Pahlavi","pampangakapampangan":"PampangaKapampangan","panjabipunjabi":"PanjabiPunjabi","papiamento":"Papiamento","palauan":"Palauan","persian,old(ca.600-400b.c.)":"Persian,Old(ca.600-400B.C.)","persian":"Persian","philippinelanguages":"Philippinelanguages","phoenician":"Phoenician","pali":"Pali","polish":"Polish","pohnpeian":"Pohnpeian","portuguese":"Portuguese","prakritlanguages":"Prakritlanguages","provençal,old(to1500)occitan,old(to1500)":"Provençal,Old(to1500)Occitan,Old(to1500)","pushtopashto":"PushtoPashto","reservedforlocaluse":"Reservedforlocaluse","quechua":"Quechua","rajasthani":"Rajasthani","rapanui":"Rapanui","rarotongancookislandsmaori":"RarotonganCookIslandsMaori","romancelanguages":"Romancelanguages","romansh":"Romansh","romany":"Romany","romanianmoldavianmoldovan":"RomanianMoldavianMoldovan","rundi":"Rundi","aromanianarumanianmacedo-romanian":"AromanianArumanianMacedo-Romanian","russian":"Russian","sandawe":"Sandawe","sango":"Sango","yakut":"Yakut","southamericanindianlanguages":"SouthAmericanIndianlanguages","salishanlanguages":"Salishanlanguages","samaritanaramaic":"SamaritanAramaic","sanskrit":"Sanskrit","sasak":"Sasak","santali":"Santali","sicilian":"Sicilian","scots":"Scots","selkup":"Selkup","semiticlanguages":"Semiticlanguages","irish,old(to900)":"Irish,Old(to900)","signlanguages":"SignLanguages","shan":"Shan","sidamo":"Sidamo","sinhalasinhalese":"SinhalaSinhalese","siouanlanguages":"Siouanlanguages","sino-tibetanlanguages":"Sino-Tibetanlanguages","slaviclanguages":"Slaviclanguages","slovak":"Slovak","slovenian":"Slovenian","southernsami":"SouthernSami","northernsami":"NorthernSami","samilanguages":"Samilanguages","lulesami":"LuleSami","inarisami":"InariSami","samoan":"Samoan","skoltsami":"SkoltSami","shona":"Shona","sindhi":"Sindhi","soninke":"Soninke","sogdian":"Sogdian","somali":"Somali","songhailanguages":"Songhailanguages","sotho,southern":"Sotho,Southern","spanishcastilian":"SpanishCastilian","albanian":"Albanian","sardinian":"Sardinian","sranantongo":"SrananTongo","serbian":"Serbian","serer":"Serer","nilo-saharanlanguages":"Nilo-Saharanlanguages","swati":"Swati","sukuma":"Sukuma","sundanese":"Sundanese","susu":"Susu","sumerian":"Sumerian","swahili":"Swahili","swedish":"Swedish","classicalsyriac":"ClassicalSyriac","syriac":"Syriac","tahitian":"Tahitian","tailanguages":"Tailanguages","tamil":"Tamil","tatar":"Tatar","telugu":"Telugu","timne":"Timne","tereno":"Tereno","tetum":"Tetum","tajik":"Tajik","tagalog":"Tagalog","thai":"Thai","tibetan":"Tibetan","tigre":"Tigre","tigrinya":"Tigrinya","tokelau":"Tokelau","klingontlhingan-hol":"KlingontlhIngan-Hol","tlingit":"Tlingit","tamashek":"Tamashek","tonga(nyasa)":"Tonga(Nyasa)","tonga(tongaislands)":"Tonga(TongaIslands)","tokpisin":"TokPisin","tsimshian":"Tsimshian","tswana":"Tswana","tsonga":"Tsonga","turkmen":"Turkmen","tumbuka":"Tumbuka","tupilanguages":"Tupilanguages","turkish":"Turkish","altaiclanguages":"Altaiclanguages","tuvalu":"Tuvalu","twi":"Twi","tuvinian":"Tuvinian","udmurt":"Udmurt","ugaritic":"Ugaritic","uighuruyghur":"UighurUyghur","ukrainian":"Ukrainian","umbundu":"Umbundu","undetermined":"Undetermined","urdu":"Urdu","uzbek":"Uzbek","vai":"Vai","venda":"Venda","vietnamese":"Vietnamese","volapük":"Volapük","votic":"Votic","wakashanlanguages":"Wakashanlanguages","wolaittawolaytta":"WolaittaWolaytta","waray":"Waray","washo":"Washo","welsh":"Welsh","sorbianlanguages":"Sorbianlanguages","walloon":"Walloon","wolof":"Wolof","kalmykoirat":"KalmykOirat","xhosa":"Xhosa","yao":"Yao","yapese":"Yapese","yiddish":"Yiddish","yoruba":"Yoruba","yupiklanguages":"Yupiklanguages","zapotec":"Zapotec","blissymbolsblissymbolicsbliss":"BlissymbolsBlissymbolicsBliss","zenaga":"Zenaga","standardmoroccantamazight":"StandardMoroccanTamazight","zhuangchuang":"ZhuangChuang","chinese":"Chinese","zandelanguages":"Zandelanguages","zulu":"Zulu","zuni":"Zuni","nolinguisticcontentnotapplicable":"NolinguisticcontentNotapplicable","zazadimilidimlikirdkikirmanjkizazaki":"ZazaDimiliDimliKirdkiKirmanjkiZazaki","influencer1":"Influencer1","influencer2":"Influencer2","original":"Original"}},"ccu":458,"deviceId":"yxN2Cd3kIhYKTM7aYAxGl5NnYCAdyIlQ","isAllowedWeb":true,"epgActive":{},"configBlackout":{"duration":300000,"enable":false,"btn_channel_list":{"title_en":"","title_th":"","url":"","url_th":"","url_en":""}},"activeCategory":"","adsSideBar":{"adsData":{"ALL":{"targetingArguments":{"TrueID_page":[],"Device":[]},"sizeMapping":[{"viewport":[0,0],"sizes":[[320,250],[300,250],[1,1],"fluid"]}],"slotId":"div-gpt-ad-rt-1","adUnit":"21682623839/TrueID_Web/TV","sizes":[[320,250]]}},"adsConfig":{"adsNetworkId":"","adsUnit":"21682623839/TrueID_Web/TV"}},"currentURL":"https://tv.trueid.net/th-en/live/true-movie-hits"},"__N_SSP":true} \ No newline at end of file +{ + "pageProps":{ + "currentLang":{ + "country":"th", + "lang":"en" + }, + "isBotPerformance":false, + "titleH1":"Watch Live TV Online 24 hours", + "metaData":{ + "title":"ดูทีวีออนไลน์ True Movie Hits - TrueID TV", + "description":"Watch Live TV Online 24 hours, Thai Drama, Full HD", + "imageURL":"https://cms.dmpcdn.com/livetv/2023/04/28/45345d10-e599-11ed-86b8-bb40638e3c49_webp_original.png", + "currentUrl":"https://tv.trueid.net/th-en/live/true-movie-hits", + "metaTitle":"ดูทีวีออนไลน์ True Movie Hits - TrueID TV" + }, + "channelList":[ + { + "id":"nQlqONGyoa4", + "thumb":"https://cms.dmpcdn.com/livetv/2023/07/24/5ff3e270-29cc-11ee-b2f4-e9de482d866e_webp_original.webp", + "slug":"ch3-hd", + "title":"Channel 3", + "content_type":"livetv", + "category":"livetv-ca|digitaltv-ca|entertainment-ca|freetv-ca", + "content_provider":"", + "channel_code":"c03", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"ប៉ុស្តិ៍ 3 HD", + "channel_name_eng":"CH3 HD", + "channel_name_mm":"CH3 HD", + "channel_name_th":"ช่อง 3 HD" + }, + "views":96184, + "isLiveChat":false + }, + { + "id":"wKngqJ2Vqnl", + "thumb":"https://cms.dmpcdn.com/livetv/2019/01/10/35a35017-8473-4953-8474-5c58d805b74a.png", + "slug":"mono29", + "title":"MONO 29", + "content_type":"livetv", + "category":"livetv-ca|digitaltv-ca|freetv-ca|movies-series-ca", + "content_provider":"", + "channel_code":"d43", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"មូណូ ធីវី", + "channel_name_eng":"Mono 29", + "channel_name_mm":"Mono 29", + "channel_name_th":"โมโน 29" + }, + "views":33721, + "isLiveChat":false + }, + { + "id":"8v732AYomo9", + "thumb":"https://cms.dmpcdn.com/livetv/2023/07/18/7dc7a180-2515-11ee-b8b2-77e2a8f4c31e_webp_original.webp", + "slug":"thairathtv-hd", + "title":"Thairath TV", + "content_type":"livetv", + "category":"livetv-ca|digitaltv-ca|freetv-ca|news-ca", + "content_provider":"", + "channel_code":"d05", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"ថៃរ៉ាត់ ធីវី HD", + "channel_name_eng":"Thairath TV HD", + "channel_name_mm":"Thairath TV HD", + "channel_name_th":"ไทยรัฐ ทีวี HD" + }, + "views":17228, + "isLiveChat":false + }, + { + "id":"9O54lyP5Rqx", + "thumb":"https://cms.dmpcdn.com/livetv/2023/07/19/212d15e0-25e7-11ee-bfc1-85e95548413c_webp_original.webp", + "slug":"ch7-hd", + "title":"Channel 7HD", + "content_type":"livetv", + "category":"livetv-ca|digitaltv-ca|entertainment-ca|freetv-ca", + "content_provider":"", + "channel_code":"c07", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"ប៉ុស្តិ៍​ 7", + "channel_name_eng":"CH 7HD", + "channel_name_mm":"Channel 7", + "channel_name_th":"ช่อง 7HD" + }, + "views":12092, + "isLiveChat":false + }, + { + "id":"0z4lvq6Xwoa", + "thumb":"https://cms.dmpcdn.com/livetv/2019/01/16/396384be-35dc-4d11-bf04-06c9546ec7bc.png", + "slug":"one-hd", + "title":"One31", + "content_type":"livetv", + "category":"livetv-ca|digitaltv-ca|entertainment-ca|freetv-ca", + "content_provider":"", + "channel_code":"d56", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"វ័ន HD", + "channel_name_eng":"One HD", + "channel_name_mm":"One HD", + "channel_name_th":"วัน HD" + }, + "views":10182, + "isLiveChat":false + }, + { + "id":"vqbr1WgEnGQ", + "thumb":"https://cms.dmpcdn.com/livetv/2023/09/15/5408a390-5377-11ee-8e1b-194edbb69638_webp_original.webp", + "slug":"ch8", + "title":"Channel 8", + "content_type":"livetv", + "category":"livetv-ca|digitaltv-ca|entertainment-ca|freetv-ca", + "content_provider":"", + "channel_code":"d62", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"ប៉ុស្តិ៍ 8", + "channel_name_eng":"CH8", + "channel_name_th":"ช่อง 8" + }, + "views":8295, + "isLiveChat":false + }, + { + "id":"OVKwZle4eop", + "thumb":"https://cms.dmpcdn.com/livetv/2023/09/15/84504210-5377-11ee-aaa1-7d584d8ca7a4_webp_original.webp", + "slug":"true4u", + "title":"True4U", + "content_type":"livetv", + "category":"livetv-ca|digitaltv-ca|entertainment-ca|freetv-ca|movies-series-ca", + "content_provider":"", + "channel_code":"207", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"ទ្រូ4យូ", + "channel_name_chi":"True4U", + "channel_name_eng":"True4U", + "channel_name_mm":"True4U", + "channel_name_rus":"True4U", + "channel_name_th":"ทรูโฟร์ยู", + "channel_name_vie":"True4U" + }, + "views":6489, + "isLiveChat":false + }, + { + "id":"OBb6NzoJX7O", + "thumb":"https://cms.dmpcdn.com/livetv/2023/10/02/d2ec4b30-60f1-11ee-92a4-8597bcef0049_webp_original.webp", + "slug":"amarintv-hd", + "title":"Amarin TV", + "content_type":"livetv", + "category":"livetv-ca|digitaltv-ca|freetv-ca", + "content_provider":"", + "channel_code":"da0", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"អាម៉ារិន", + "channel_name_eng":"Amarin TV", + "channel_name_mm":"Amarin TV", + "channel_name_th":"อมรินทร์" + }, + "views":6407, + "isLiveChat":false + }, + { + "id":"yYk6PvXwXDb", + "thumb":"https://cms.dmpcdn.com/livetv/2023/11/17/2a1de990-852d-11ee-bf98-41acc8fd04fc_webp_original.webp", + "slug":"workpointtv", + "title":"WorkPoint TV", + "content_type":"livetv", + "category":"livetv-ca|digitaltv-ca|entertainment-ca|freetv-ca", + "content_provider":"", + "channel_code":"d83", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"វើកភ័ញ គ្រីអ៊ែតធិវ ធីវី​", + "channel_name_eng":"Workpoint TV", + "channel_name_mm":"Workpoint TV", + "channel_name_th":"เวิร์คพอยท์ ทีวี" + }, + "views":6075, + "isLiveChat":false + }, + { + "id":"qvgeWLPGMY6", + "thumb":"https://cms.dmpcdn.com/livetv/2020/11/19/ed873d50-2a22-11eb-bed4-0972e345f90c_original.png", + "slug":"gmm25", + "title":"GMM 25", + "content_type":"livetv", + "category":"livetv-ca|digitaltv-ca|entertainment-ca|freetv-ca", + "content_provider":"", + "channel_code":"d76", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"GMM 25", + "channel_name_eng":"GMM 25", + "channel_name_mm":"GMM 25", + "channel_name_th":"จีเอ็มเอ็ม 25" + }, + "views":4861, + "isLiveChat":false + }, + { + "id":"zMLBpX7AWmk", + "thumb":"https://cms.dmpcdn.com/livetv/2023/09/09/9c6f59c0-4ebe-11ee-99a7-832609069236_webp_original.webp", + "slug":"nationtv", + "title":"Nation TV", + "content_type":"livetv", + "category":"livetv-ca|digitaltv-ca|freetv-ca|news-ca", + "content_provider":"", + "channel_code":"d78", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"Nation TV 22", + "channel_name_chi":"Nation TV 22", + "channel_name_eng":"Nation TV 22", + "channel_name_mm":"Nation TV 22", + "channel_name_rus":"Nation TV 22", + "channel_name_th":"เนชั่น ทีวี", + "channel_name_vie":"Nation TV 22" + }, + "views":4733, + "isLiveChat":false + }, + { + "id":"QNBwOpdaxpQ", + "thumb":"https://cms.dmpcdn.com/livetv/2023/08/28/012eed00-458a-11ee-bd2b-6734a2d9e428_webp_original.webp", + "slug":"pptv-hd", + "title":"PPTV", + "content_type":"livetv", + "category":"livetv-ca|digitaltv-ca|freetv-ca", + "content_provider":"", + "channel_code":"da7", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"ភីភីធីវី", + "channel_name_eng":"PPTV", + "channel_name_mm":"PPTV", + "channel_name_th":"พีพีทีวี" + }, + "views":4723, + "isLiveChat":false + }, + { + "id":"xqY73dWBoZye", + "thumb":"https://cms.dmpcdn.com/livetv/2023/05/03/ba425a00-e966-11ed-be07-cbff4c6d2c94_webp_original.png", + "slug":"truepremierfootballhd1", + "title":"True Premier Football 1", + "content_type":"livetv", + "category":"livetv-ca|football-ca|sports-ca|tvsnow|sports", + "content_provider":"true_vision", + "channel_code":"ht111", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"True Premier Football 1", + "channel_name_th":"ทรู พรีเมียร์ ฟุตบอล 1" + }, + "views":3123, + "isLiveChat":true + }, + { + "id":"QRP2K658b7G", + "thumb":"https://cms.dmpcdn.com/livetv/2023/09/15/ab170410-5377-11ee-8e1b-194edbb69638_webp_original.webp", + "slug":"thaipbs", + "title":"Thai PBS", + "content_type":"livetv", + "category":"livetv-ca|digitaltv-ca|freetv-ca|news-ca", + "content_provider":"", + "channel_code":"c12", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"ធីភីបីអេស", + "channel_name_eng":"TPBS", + "channel_name_mm":"TPBS", + "channel_name_th":"ไทยพีบีเอส" + }, + "views":2526, + "isLiveChat":false + }, + { + "id":"OZeq8ZLPldY", + "thumb":"https://cms.dmpcdn.com/livetv/2023/10/02/75023d90-60f1-11ee-935a-5d4eba985103_webp_original.webp", + "slug":"tnn16", + "title":"TNN 16", + "content_type":"livetv", + "category":"livetv-ca|digitaltv-ca|freetv-ca|news-ca|tvsnow|tvsnews", + "content_provider":"true_vision", + "channel_code":"135", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"ធីអិនអិន 16", + "channel_name_eng":"TNN​ 16", + "channel_name_mm":"TNN​ 16", + "channel_name_th":"ทีเอ็นเอ็น 16" + }, + "views":2444, + "isLiveChat":false + }, + { + "id":"LY2j6Pyxbla", + "thumb":"https://cms.dmpcdn.com/livetv/2023/10/02/4a2afc60-60f1-11ee-a78e-f70ba0052fab_webp_original.webp", + "slug":"nbt", + "title":"NBT", + "content_type":"livetv", + "category":"livetv-ca|digitaltv-ca|freetv-ca", + "content_provider":"", + "channel_code":"c11", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"អិនបីធី​", + "channel_name_eng":"NBT", + "channel_name_mm":"์NBT", + "channel_name_th":"เอ็นบีที" + }, + "views":2043, + "isLiveChat":false + }, + { + "id":"Z9E4LnAbgjKy", + "thumb":"https://cms.dmpcdn.com/livetv/2021/06/15/3e4e0540-cdb4-11eb-9a22-7958179a38a7_original.png", + "slug":"jkn18", + "title":"JKN 18", + "content_type":"livetv", + "category":"livetv-ca|digitaltv-ca|freetv-ca", + "content_provider":"", + "channel_code":"d11", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"JKN 18", + "channel_name_mm":"JKN 18", + "channel_name_th":"เจเคเอ็น 18" + }, + "views":1725, + "isLiveChat":false + }, + { + "id":"rBWOx89v9Rk", + "thumb":"https://cms.dmpcdn.com/livetv/2023/09/09/9cc40970-4ebe-11ee-9801-97f95b5eed9a_webp_original.webp", + "slug":"9mcot-hd", + "title":"9 MCOT", + "content_type":"livetv", + "category":"livetv-ca|digitaltv-ca|freetv-ca", + "content_provider":"", + "channel_code":"c09", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"ប៉ុស្តិ៍ 9 HD", + "channel_name_eng":"9 MCOT HD", + "channel_name_mm":"9 MCOT HD", + "channel_name_th":"9 เอ็มคอต HD" + }, + "views":1323, + "isLiveChat":false + }, + { + "id":"5PKobQk5gLOP", + "thumb":"https://cms.dmpcdn.com/livetv/2023/07/05/b74a2460-1b05-11ee-8ce6-b102b53cb4a2_webp_original.webp", + "slug":"boomerang-hd", + "title":"Boomerang", + "content_type":"livetv", + "category":"livetv-ca|freetv-ca|kids-ca", + "content_provider":"", + "channel_code":"i007", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"Boomerang", + "channel_name_th":"บูมเมอแรง" + }, + "views":707, + "isLiveChat":false + }, + { + "id":"KEN52vz3o6M", + "thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/296e96a0-e593-11ed-8507-4fc0b025fedb_webp_original.png", + "slug":"truesport-hd-3", + "title":"True Sports 3", + "content_type":"livetv", + "category":"livetv-ca|sports-ca|tvsnow|sports", + "content_provider":"true_vision", + "channel_code":"ht117", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"True Sports 3", + "channel_name_chi":"True Sports 3", + "channel_name_eng":"True Sports 3", + "channel_name_mm":"True Sports 3", + "channel_name_rus":"True Sports 3", + "channel_name_th":"ทรูสปอร์ต 3", + "channel_name_vie":"True Sports 3" + }, + "views":652, + "isLiveChat":true + }, + { + "id":"1KDEkNJDZ9r", + "thumb":"https://cms.dmpcdn.com/livetv/2023/07/18/7e060a10-2515-11ee-864f-a52221dad038_webp_original.webp", + "slug":"ch5", + "title":"TV5 HD", + "content_type":"livetv", + "category":"livetv-ca|digitaltv-ca|freetv-ca|news-ca", + "content_provider":"", + "channel_code":"c05", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"ប៉ុស្តិ៍ 5", + "channel_name_eng":"CH 5", + "channel_name_mm":"နံပါတ္ 5 အစီအစဥ", + "channel_name_th":"ช่อง 5" + }, + "views":648, + "isLiveChat":false + }, + { + "id":"NopZ5gjkGmE", + "thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/45345d10-e599-11ed-86b8-bb40638e3c49_webp_original.png", + "slug":"true-movie-hits", + "title":"True Movie Hits", + "content_type":"livetv", + "category":"livetv-ca|movies-series-ca|trueunlock-ca|tvsnow|movieseries", + "content_provider":"true_vision", + "channel_code":"057", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"True Movie Hits", + "channel_name_chi":"True Movie Hits", + "channel_name_eng":"True Movie Hits", + "channel_name_mm":"True Movie Hits", + "channel_name_rus":"True Movie Hits", + "channel_name_th":"True Movie Hits", + "channel_name_vie":"True Movie Hits" + }, + "views":640, + "isLiveChat":false + }, + { + "id":"9xQq7Yk7Jzr", + "thumb":"https://cms.dmpcdn.com/livetv/2023/07/18/7d74eda0-2515-11ee-864f-a52221dad038_webp_original.webp", + "slug":"realitychannel-hd", + "title":"Reality", + "content_type":"livetv", + "category":"livetv-ca|education-ca|entertainment-ca|freetv-ca|kids-ca|truelittlemonk|tvsnow|entertainment", + "content_provider":"true_vision", + "channel_code":"107", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"ប៉ុស្តិ៍ រែលអាលីធី", + "channel_name_eng":"Reality", + "channel_name_mm":"ထရူး ပူပန္ယာ", + "channel_name_th":"เรียลลิตี้" + }, + "views":518, + "isLiveChat":false + }, + { + "id":"GPVMYwpnzKv", + "thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/f63723d0-e595-11ed-abcb-c792e696f885_webp_original.png", + "slug":"truesport-7", + "title":"True Sports 7", + "content_type":"livetv", + "category":"livetv-ca|sports-ca|trueunlock-ca|tvsnow|sports", + "content_provider":"true_vision", + "channel_code":"105", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"True Sports 7", + "channel_name_chi":"True Sports 7", + "channel_name_eng":"True Sports 7", + "channel_name_mm":"True Sports 7", + "channel_name_rus":"True Sports 7", + "channel_name_th":"ทรูสปอร์ต 7", + "channel_name_vie":"True Sports 7" + }, + "views":499, + "isLiveChat":true + }, + { + "id":"RN8ALdyRovrj", + "thumb":"https://cms.dmpcdn.com/livetv/2022/02/10/e00c0e00-8a3c-11ec-8f9e-831d2ccecc69_webp_original.png", + "slug":"t-sports-7-sd", + "title":"T Sports 7", + "content_type":"livetv", + "category":"livetv-ca|digitaltv-ca|freetv-ca|sports-ca", + "content_provider":"", + "channel_code":"t514", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"T Sports 7", + "channel_name_th":"สถานีโทรทัศน์เพื่อการท่องเที่ยวและกีฬา" + }, + "views":484, + "isLiveChat":false + }, + { + "id":"AlPo3NzNZa62", + "thumb":"https://cms.dmpcdn.com/livetv/2023/05/03/ba4c4510-e966-11ed-896e-69ce273284a6_webp_original.png", + "slug":"truepremierfootballhd2", + "title":"True Premier Football 2", + "content_type":"livetv", + "category":"livetv-ca|football-ca|sports-ca|tvsnow|sports", + "content_provider":"true_vision", + "channel_code":"ht112", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"True Premier Football 2", + "channel_name_th":"ทรู พรีเมียร์ ฟุตบอล 2" + }, + "views":449, + "isLiveChat":true + }, + { + "id":"PanRBOzKovQ", + "thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/43f28e40-e599-11ed-844f-795506bf0bf9_webp_original.png", + "slug":"true-film-hd", + "title":"True Film 1", + "content_type":"livetv", + "category":"hbtv-trueidtv-all|livetv-ca|movies-series-ca|trueidtv-movies-series|tvsnow|movieseries", + "content_provider":"true_vision", + "channel_code":"176", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"True Film 1", + "channel_name_chi":"True Film 1", + "channel_name_eng":"True Film 1", + "channel_name_mm":"True Film 1", + "channel_name_rus":"True Film 1", + "channel_name_th":"True Film 1", + "channel_name_vie":"True Film 1" + }, + "views":388, + "isLiveChat":false + }, + { + "id":"GNd67OBJ6pv", + "thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/46f1c480-e599-11ed-96ec-4d05b9e2ca86_webp_original.png", + "slug":"thai-film", + "title":"True Thai Film", + "content_type":"livetv", + "category":"livetv-ca|movies-series-ca|trueunlock-ca|tvsnow|movieseries", + "content_provider":"true_vision", + "channel_code":"094", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"True Thai Film", + "channel_name_chi":"True Thai Film", + "channel_name_eng":"True Thai Film", + "channel_name_mm":"True Thai Film", + "channel_name_rus":"True Thai Film", + "channel_name_th":"True Thai Film", + "channel_name_vie":"True Thai Film" + }, + "views":359, + "isLiveChat":false + }, + { + "id":"a0k7zw9OPrr0", + "thumb":"https://cms.dmpcdn.com/livetv/2020/07/14/72c22620-c5aa-11ea-a8d3-2b56c8ce453d_original.png", + "slug":"altv", + "title":"ALTV", + "content_type":"livetv", + "category":"livetv-ca|digitaltv-ca|education-ca|freetv-ca", + "content_provider":"", + "channel_code":"dum024", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"ALTV", + "channel_name_th":"เอแอลทีวี" + }, + "views":255, + "isLiveChat":false + }, + { + "id":"9WmoQMj0NOp", + "thumb":"https://cms.dmpcdn.com/livetv/2023/11/22/932dbce0-8919-11ee-820d-0ff332ca746f_webp_original.webp", + "slug":"trueplookpanya", + "title":"True Plook Panya", + "content_type":"livetv", + "category":"livetv-ca|documentary-ca|tvsnow|documentary", + "content_provider":"true_vision", + "channel_code":"139", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"ទ្រូបណ្តុះគំណិត", + "channel_name_eng":"True Plookpanya", + "channel_name_mm":"True Plookpanya", + "channel_name_th":"ทรู ปลูกปัญญา" + }, + "views":242, + "isLiveChat":false + }, + { + "id":"KlW9OymBRqrD", + "thumb":"https://cms.dmpcdn.com/livetv/2023/09/25/c3898e20-5b4f-11ee-a599-1d1a4f7c1125_webp_original.webp", + "slug":"trueid-sports", + "title":"TrueID Sports", + "content_type":"livetv", + "category":"livetv-ca|sports-ca", + "content_provider":"", + "channel_code":"he003", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"TrueID Sports", + "channel_name_th":"ทรูไอดี สปอร์ต" + }, + "views":240, + "isLiveChat":true + }, + { + "id":"Vwz1j7XVRkdn", + "thumb":"https://cms.dmpcdn.com/livetv/2023/08/02/89262f60-30e1-11ee-b445-3703761d6f4d_webp_original.webp", + "slug":"true-ball-thai-1", + "title":"True Ball Thai 1", + "content_type":"livetv", + "category":"livetv-ca|football-ca|sports-ca|tvsnow|sports", + "content_provider":"true_vision", + "channel_code":"vc01", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"True Ball Thai 1", + "channel_name_th":"True Ball Thai 1" + }, + "views":234, + "isLiveChat":false + }, + { + "id":"YmaygkwgE6Lm", + "thumb":"https://cms.dmpcdn.com/livetv/2023/10/02/75023d90-60f1-11ee-935a-5d4eba985103_webp_original.webp", + "slug":"tnn16-hd", + "title":"TNN 16 HD", + "content_type":"livetv", + "category":"livetv-ca|news-ca|tnn|tvsnow|tvsnews", + "content_provider":"true_vision", + "channel_code":"t516", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"TNN​ 16 HD", + "channel_name_th":"TNN 16 HD" + }, + "views":233, + "isLiveChat":false + }, + { + "id":"GdgqaeMewGp4", + "thumb":"https://cms.dmpcdn.com/livetv/2023/05/03/baf9ea30-e966-11ed-a3d3-f3f98ac7a1a1_webp_original.png", + "slug":"truepremierfootballhd3", + "title":"True Premier Football 3", + "content_type":"livetv", + "category":"livetv-ca|football-ca|sports-ca|tvsnow|sports", + "content_provider":"true_vision", + "channel_code":"ht113", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"True Premier Football 3", + "channel_name_th":"ทรู พรีเมียร์ ฟุตบอล 3" + }, + "views":199, + "isLiveChat":true + }, + { + "id":"A36nrdXGn3V", + "thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/47a53600-e599-11ed-94a2-8feec94a4a3b_webp_original.png", + "slug":"true-asian-more", + "title":"True Asian More", + "content_type":"livetv", + "category":"livetv-ca|movies-series-ca|trueunlock-ca|tvsnow|movieseries", + "content_provider":"true_vision", + "channel_code":"081", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"True Asian More", + "channel_name_chi":"True Asian More", + "channel_name_eng":"True Asian More", + "channel_name_mm":"True Asian More", + "channel_name_rus":"True Asian More", + "channel_name_th":"True Asian More", + "channel_name_vie":"True Asian More" + }, + "views":190, + "isLiveChat":false + }, + { + "id":"74ngXBo8ke0", + "thumb":"https://cms.dmpcdn.com/livetv/2019/01/21/a01a26bb-ed4a-45c5-88a9-ff30f6bbb039.png", + "slug":"cartoonclub", + "title":"Cartoon Club", + "content_type":"livetv", + "category":"cartoon|hbtv-trueidtv-all|hbtv-truetv-kids|trueidtv-all|trueidtv-kids|kids|livetv-ca|kids-ca", + "content_provider":"", + "channel_code":"143", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"កាទូនខ្លឹប", + "channel_name_eng":"Cartoon Club", + "channel_name_mm":"ကာတြန္းကလပ္", + "channel_name_th":"การ์ตูน คลับ" + }, + "views":189, + "isLiveChat":false + }, + { + "id":"4QmJ09AyPm4", + "thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/43ffada0-e599-11ed-abcb-c792e696f885_webp_original.png", + "slug":"true-film-hd-2", + "title":"True Film 2", + "content_type":"livetv", + "category":"hbtv-truetv-movies-series|livetv-ca|movies-series-ca|tvsnow|movieseries", + "content_provider":"true_vision", + "channel_code":"221", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"True Film 2", + "channel_name_chi":"True Film 2", + "channel_name_eng":"True Film 2", + "channel_name_mm":"True Film 2", + "channel_name_rus":"True Film 2", + "channel_name_th":"True Film 2", + "channel_name_vie":"True Film 2" + }, + "views":185, + "isLiveChat":false + }, + { + "id":"wQZrKd3mo65", + "thumb":"https://cms.dmpcdn.com/livetv/2023/04/24/81825540-e28a-11ed-9bb2-7fe2e28bfd8c_webp_original.png", + "slug":"truesport-hd", + "title":"True Sports 1", + "content_type":"livetv", + "category":"livetv-ca|sports-ca|tvsnow|sports", + "content_provider":"true_vision", + "channel_code":"097", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"True Sports 1", + "channel_name_chi":"True Sports 1", + "channel_name_eng":"True Sports 1", + "channel_name_mm":"True Sports 1", + "channel_name_rus":"True Sports 1", + "channel_name_th":"ทรูสปอร์ต 1", + "channel_name_vie":"True Sports 1" + }, + "views":179, + "isLiveChat":true + }, + { + "id":"Lzz61DA3zYL", + "thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/486c7da0-e599-11ed-b481-1b121c78e74e_webp_original.png", + "slug":"true-explore-life", + "title":"True Explore Life", + "content_type":"livetv", + "category":"livetv-ca|documentary-ca|trueunlock-ca|tvsnow|documentary", + "content_provider":"true_vision", + "channel_code":"060", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"True Explore Life", + "channel_name_chi":"True Explore Life", + "channel_name_eng":"True Explore Life", + "channel_name_mm":"True Explore Life", + "channel_name_rus":"True Explore Life", + "channel_name_th":"True Explore Life", + "channel_name_vie":"True Explore Life" + }, + "views":124, + "isLiveChat":false + }, + { + "id":"vNG2L371k5W", + "thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/433fe010-e599-11ed-96ec-4d05b9e2ca86_webp_original.png", + "slug":"true-explore-wild", + "title":"True Explore Wild", + "content_type":"livetv", + "category":"livetv-ca|documentary-ca|trueunlock-ca|true-unlock|true-unlock-atv|tvsnow|documentary", + "content_provider":"true_vision", + "channel_code":"058", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"True Explore Wild", + "channel_name_eng":"True Explore Wild", + "channel_name_mm":"True Explore Wild", + "channel_name_th":"True Explore Wild" + }, + "views":119, + "isLiveChat":false + }, + { + "id":"jqepWV3ka8j", + "thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/46fb6170-e599-11ed-b606-c19576cb8b29_webp_original.png", + "slug":"true-x-zyte-hd", + "title":"True X-Zyte", + "content_type":"livetv", + "category":"livetv-ca|entertainment-ca|trueunlock-ca|tvsnow|entertainment", + "content_provider":"true_vision", + "channel_code":"034", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"True X-Zyte", + "channel_name_chi":"True X-Zyte", + "channel_name_eng":"True X-Zyte", + "channel_name_mm":"True X-Zyte", + "channel_name_rus":"True X-Zyte", + "channel_name_th":"True X-Zyte", + "channel_name_vie":"True X-Zyte" + }, + "views":107, + "isLiveChat":false + }, + { + "id":"3wLvyKyryPAD", + "thumb":"https://cms.dmpcdn.com/livetv/2023/07/24/5fda18e0-29cc-11ee-846b-a1c4e5181c87_webp_original.webp", + "slug":"bein-sports-hd3", + "title":"beIN SPORTS 3", + "content_type":"livetv", + "category":"livetv-ca|football-ca|sports-ca|tvsnow|sports", + "content_provider":"true_vision", + "channel_code":"215", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"beIN SPORTS 3", + "channel_name_th":"บีอินสปอตส์ 3" + }, + "views":96, + "isLiveChat":false + }, + { + "id":"mVoXV1rk4B5", + "thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/488c61b0-e599-11ed-94a2-8feec94a4a3b_webp_original.png", + "slug":"true-explore-3", + "title":"True Explore Sci", + "content_type":"livetv", + "category":"livetv-ca|documentary-ca|trueunlock-ca|tvsnow|documentary", + "content_provider":"true_vision", + "channel_code":"061", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"True Explore Sci", + "channel_name_chi":"True Explore Sci", + "channel_name_eng":"True Explore Sci", + "channel_name_mm":"True Explore Sci", + "channel_name_rus":"True Explore Sci", + "channel_name_th":"True Explore Sci", + "channel_name_vie":"True Explore Sci" + }, + "views":91, + "isLiveChat":false + }, + { + "id":"D1029rjaV6GQ", + "thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/0c14dd80-9407-11ee-b625-274874732f96_webp_original.webp", + "slug":"manchester-united", + "title":"Manchester United", + "content_type":"livetv", + "category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all", + "content_provider":"", + "channel_code":"mun01", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"Manchester United", + "channel_name_th":"แมนยู" + }, + "views":91, + "isLiveChat":false + }, + { + "id":"peWQgAb52vk", + "thumb":"https://cms.dmpcdn.com/livetv/2023/07/18/7cb4aae0-2515-11ee-9407-9367a664b338_webp_original.webp", + "slug":"golf-channel", + "title":"Golf Channel Thailand", + "content_type":"livetv", + "category":"livetv-ca|sports-ca|tvsnow|sports", + "content_provider":"true_vision", + "channel_code":"095", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"Golf Channel Thailand HD", + "channel_name_chi":"Golf Channel Thailand HD", + "channel_name_eng":"Golf Channel Thailand HD", + "channel_name_mm":"Golf Channel Thailand HD", + "channel_name_rus":"Golf Channel Thailand HD", + "channel_name_th":"Golf Channel Thailand HD", + "channel_name_vie":"Golf Channel Thailand HD" + }, + "views":88, + "isLiveChat":false + }, + { + "id":"A8aVZWzlOmDE", + "thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/d7783cc0-9406-11ee-b445-0b5cfb8bf6f8_webp_original.webp", + "slug":"liverpool", + "title":"Liverpool", + "content_type":"livetv", + "category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all", + "content_provider":"", + "channel_code":"liv01", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"Liverpool", + "channel_name_th":"ลิเวอร์พูล" + }, + "views":88, + "isLiveChat":false + }, + { + "id":"Ay93Q8zlOeA", + "thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/456c5d00-e599-11ed-b550-9935ba8025b9_webp_original.png", + "slug":"true-series", + "title":"True Series", + "content_type":"livetv", + "category":"livetv-ca|movies-series-ca|tvsnow|movieseries", + "content_provider":"true_vision", + "channel_code":"st006", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"True Series", + "channel_name_chi":"True Series", + "channel_name_eng":"True Series", + "channel_name_mm":"True Series", + "channel_name_rus":"True Series", + "channel_name_th":"True Series", + "channel_name_vie":"True Series" + }, + "views":82, + "isLiveChat":false + }, + { + "id":"g9ONWXWJV5pq", + "thumb":"https://cms.dmpcdn.com/livetv/2023/07/24/5f3c5240-29cc-11ee-b2f4-e9de482d866e_webp_original.webp", + "slug":"bein-sports-hd1", + "title":"beIN SPORTS 1", + "content_type":"livetv", + "category":"livetv-ca|football-ca|sports-ca|tvsnow|sports", + "content_provider":"true_vision", + "channel_code":"202", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"beIN SPORTS 1", + "channel_name_th":"บีอินสปอตส์ 1" + }, + "views":80, + "isLiveChat":false + }, + { + "id":"xR0n6ePG7wL", + "thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/2960b3f0-e593-11ed-b26c-6b89d082d464_webp_original.png", + "slug":"truesport-hd-2", + "title":"True Sports 2", + "content_type":"livetv", + "category":"livetv-ca|football-ca|sports-ca|trueunlock-ca|tvsnow|sports", + "content_provider":"true_vision", + "channel_code":"ht116", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"True Sports 2", + "channel_name_chi":"True Sports 2", + "channel_name_eng":"True Sports 2", + "channel_name_mm":"True Sports 2", + "channel_name_rus":"True Sports 2", + "channel_name_th":"ทรูสปอร์ต 2", + "channel_name_vie":"True Sports 2" + }, + "views":80, + "isLiveChat":true + }, + { + "id":"JlrpNK19py0M", + "thumb":"https://cms.dmpcdn.com/livetv/2019/04/11/19b4bd2a-750c-4ee6-9d41-5080e1310bc3_original.png", + "slug":"Mangorn", + "title":"Mangorn", + "content_type":"livetv", + "category":"free-tv|livetv-ca|freetv-ca|movies-series-ca", + "content_provider":"", + "channel_code":"o020", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"Mangorn", + "channel_name_th":"มังกร" + }, + "views":73, + "isLiveChat":false + }, + { + "id":"lPXDJR6gN6l", + "thumb":"https://cms.dmpcdn.com/livetv/2019/02/28/a9490c72-7387-4409-b5a8-80db28585ca4.png", + "slug":"true-select", + "title":"True Select", + "content_type":"livetv", + "category":"livetv-ca|entertainment-ca|variety-ca|tvsnow|entertainment", + "content_provider":"true_vision", + "channel_code":"218", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"True Select", + "channel_name_chi":"True Select", + "channel_name_eng":"True Select", + "channel_name_mm":"True Select", + "channel_name_rus":"True Select", + "channel_name_th":"True Select", + "channel_name_vie":"True Select" + }, + "views":71, + "isLiveChat":false + }, + { + "id":"NWY5K7ZELP2", + "thumb":"https://cms.dmpcdn.com/livetv/2018/12/17/0c30b192-953b-49b9-a9bf-a4c6e3e71de3.png", + "slug":"true-select-hd", + "title":"True Shopping", + "content_type":"livetv", + "category":"livetv-ca|entertainment-ca|tvsnow|entertainment", + "content_provider":"true_vision", + "channel_code":"127", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"True Shopping", + "channel_name_chi":"True Shopping", + "channel_name_eng":"True Shopping", + "channel_name_mm":"True Shopping", + "channel_name_rus":"True Shopping", + "channel_name_th":"True Shopping", + "channel_name_vie":"True Shopping" + }, + "views":70, + "isLiveChat":true + }, + { + "id":"r71LNbqjaKe", + "thumb":"https://cms.dmpcdn.com/livetv/2019/01/31/a5aeb78c-c4db-474f-a5af-345cb9e2f5b5.png", + "slug":"rama-channel", + "title":"Rama Channel", + "content_type":"livetv", + "category":"livetv-ca|documentary-ca|news-ca|tvsnow|documentary", + "content_provider":"true_vision", + "channel_code":"128", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"Rama Channel", + "channel_name_chi":"Rama Channel", + "channel_name_eng":"Rama Channel", + "channel_name_mm":"Rama Channel", + "channel_name_rus":"Rama Channel", + "channel_name_th":"Rama Channel", + "channel_name_vie":"Rama Channel" + }, + "views":69, + "isLiveChat":false + }, + { + "id":"YLN6d3oYyXEL", + "thumb":"https://cms.dmpcdn.com/livetv/2023/11/22/2a4de600-8919-11ee-8416-3dc6bea66698_webp_original.webp", + "slug":"tptv", + "title":"TPTV", + "content_type":"livetv", + "category":"livetv-ca|digitaltv-ca|education-ca|freetv-ca", + "content_provider":"", + "channel_code":"d31", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"TPTV - Thai Parliament TV", + "channel_name_th":"ทีพีทีวี" + }, + "views":61, + "isLiveChat":false + }, + { + "id":"eXlvvZ4EA5aY", + "thumb":"https://cms.dmpcdn.com/livetv/2022/12/22/d9313340-81d9-11ed-a7f9-412bbba270e9_webp_original.png", + "slug":"tv-nfl-nba", + "title":"NFL & NBA TV", + "content_type":"livetv", + "category":"livetv-ca|sports-ca", + "content_provider":"true_vision", + "channel_code":"t513", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"NFL & NBA TV", + "channel_name_th":"เอ็นเอฟแอล แอนด์ เอ็นบีเอ ทีวี" + }, + "views":59, + "isLiveChat":false + }, + { + "id":"zmvD0RO72nL", + "thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/f493fb20-e595-11ed-b26c-6b89d082d464_webp_original.png", + "slug":"truesport-5", + "title":"True Sports 5", + "content_type":"livetv", + "category":"livetv-ca|sports-ca|tvsnow|sports", + "content_provider":"true_vision", + "channel_code":"056", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"True Sports 5", + "channel_name_chi":"True Sports 5", + "channel_name_eng":"True Sports 5", + "channel_name_mm":"True Sports 5", + "channel_name_rus":"True Sports 5", + "channel_name_th":"ทรูสปอร์ต 5", + "channel_name_vie":"True Sports 5" + }, + "views":58, + "isLiveChat":false + }, + { + "id":"mXQoNYKda2L9", + "thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/434696d0-e599-11ed-b26c-6b89d082d464_webp_original.png", + "slug":"film-asia-hd", + "title":"True Film Asia", + "content_type":"livetv", + "category":"livetv-ca|movies-series-ca|tvsnow|movieseries", + "content_provider":"true_vision", + "channel_code":"t500", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"True Film Asia", + "channel_name_chi":"True Film Asia", + "channel_name_eng":"True Film Asia", + "channel_name_mm":"True Film Asia", + "channel_name_rus":"True Film Asia", + "channel_name_th":"True Film Asia", + "channel_name_vie":"True Film Asia" + }, + "views":55, + "isLiveChat":false + }, + { + "id":"P83vkq1M1Lp", + "thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/46065310-e599-11ed-96ec-4d05b9e2ca86_webp_original.png", + "slug":"true-spark", + "title":"True Spark Play", + "content_type":"livetv", + "category":"livetv-ca|kids-ca|trueunlock-ca|tvsnow|kids", + "content_provider":"true_vision", + "channel_code":"007", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"True Spark Play", + "channel_name_chi":"True Spark Play", + "channel_name_eng":"True Spark Play", + "channel_name_mm":"True Spark Play", + "channel_name_rus":"True Spark Play", + "channel_name_th":"True Spark Play", + "channel_name_vie":"True Spark Play" + }, + "views":54, + "isLiveChat":false + }, + { + "id":"2L1ZZdJGxPej", + "thumb":"https://cms.dmpcdn.com/livetv/2023/07/24/61050450-29cc-11ee-b2f4-e9de482d866e_webp_original.webp", + "slug":"spotv2-hd", + "title":"SPOTV 2", + "content_type":"livetv", + "category":"livetv-ca|sports-ca|tvsnow|sports", + "content_provider":"true_vision", + "channel_code":"t511", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"SPOTV 2", + "channel_name_th":"SPOTV 2" + }, + "views":46, + "isLiveChat":false + }, + { + "id":"Mbx79DOD44J", + "thumb":"https://cms.dmpcdn.com/livetv/2021/06/01/e2f61c80-c234-11eb-92e3-4bf272c5d086_original.png", + "slug":"true-music-channel-hd", + "title":"True Music", + "content_type":"livetv", + "category":"livetv-ca|entertainment-ca|trueunlock-ca|tvsnow|entertainment", + "content_provider":"true_vision", + "channel_code":"159", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"True Music", + "channel_name_chi":"True Music", + "channel_name_eng":"True Music", + "channel_name_mm":"True Music", + "channel_name_rus":"True Music", + "channel_name_th":"True Music", + "channel_name_vie":"True Music" + }, + "views":40, + "isLiveChat":false + }, + { + "id":"leVMNwY8LA1B", + "thumb":"https://cms.dmpcdn.com/livetv/2021/02/24/cc08cbe0-764d-11eb-b272-17d04980ce1e_original.png", + "slug":"ATTV", + "title":"@TV", + "content_type":"livetv", + "category":"free-tv|livetv-ca|freetv-ca", + "content_provider":"", + "channel_code":"i002", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"@TV", + "channel_name_th":"แอททีวี" + }, + "views":39, + "isLiveChat":false + }, + { + "id":"V14w2AL9grW6", + "thumb":"https://cms.dmpcdn.com/livetv/2023/11/13/bd6a6d20-8205-11ee-822c-6bbb3f82c35b_webp_original.webp", + "slug":"voicetv-2023", + "title":"VOICE TV", + "content_type":"livetv", + "category":"livetv-ca|freetv-ca|news-ca", + "content_provider":"", + "channel_code":"154", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"វ៉យធីវី", + "channel_name_eng":"Voice TV", + "channel_name_mm":"Voice TV", + "channel_name_th":"วอยซ์ ทีวี" + }, + "views":33, + "isLiveChat":false + }, + { + "id":"NB2d2A9Zd94z", + "thumb":"https://cms.dmpcdn.com/livetv/2023/09/25/c389b530-5b4f-11ee-a6f1-ffa978a40b9f_webp_original.webp", + "slug":"trueid-live", + "title":"TrueID Live", + "content_type":"livetv", + "category":"livetv-ca|entertainment-ca|variety-ca", + "content_provider":"", + "channel_code":"ev04", + "content_rights":null, + "channel_info":{ + "channel_name_th":"ทรูไอดี ไลฟ์" + }, + "views":31, + "isLiveChat":true + }, + { + "id":"GOPVJMln56Y", + "thumb":"https://cms.dmpcdn.com/livetv/2020/06/23/816989d0-b550-11ea-8fac-236a281cd6c5_original.png", + "slug":"dharmatv", + "title":"Dhamma TV", + "content_type":"livetv", + "category":"knowledge|livetv-ca|digitaltv-ca|documentary-ca|trueidtv-all|trueidtv-digital-tv|variety", + "content_provider":"", + "channel_code":"o016", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"ព្រះធម៌ធីវី", + "channel_name_eng":"Dhamma TV", + "channel_name_mm":"Dhamma TV", + "channel_name_th":"ธรรมะทีวี" + }, + "views":31, + "isLiveChat":false + }, + { + "id":"09BRRXKbgge9", + "thumb":"https://cms.dmpcdn.com/livetv/2022/03/23/f7108720-aa94-11ec-9b91-03afdbb2e824_webp_original.png", + "slug":"truepremierfootballhd6", + "title":"True Premier Football 6", + "content_type":"livetv", + "category":"livetv-ca|football-ca|sports-ca", + "content_provider":"true_vision", + "channel_code":"t502", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"True Premier Football 6", + "channel_name_th":"ทรู พรีเมียร์ ฟุตบอล 6" + }, + "views":31, + "isLiveChat":false + }, + { + "id":"Q7vaEm8O9e4", + "thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/f65ea900-e595-11ed-86b8-bb40638e3c49_webp_original.png", + "slug":"true-tennis-hd", + "title":"True Tennis", + "content_type":"livetv", + "category":"livetv-ca|sports-ca|tvsnow|sports", + "content_provider":"true_vision", + "channel_code":"045", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"True Tennis", + "channel_name_chi":"True Tennis", + "channel_name_eng":"True Tennis", + "channel_name_mm":"True Tennis", + "channel_name_rus":"True Tennis", + "channel_name_th":"True Tennis", + "channel_name_vie":"True Tennis" + }, + "views":29, + "isLiveChat":false + }, + { + "id":"N8E7v0JlM15e", + "thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/94a32ae0-9406-11ee-a0fd-836d91d2dd6e_webp_original.webp", + "slug":"chelsea", + "title":"Chelsea", + "content_type":"livetv", + "category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all", + "content_provider":"", + "channel_code":"che01", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"Chelsea", + "channel_name_th":"เชลซี" + }, + "views":28, + "isLiveChat":false + }, + { + "id":"PdOXKN4O1vDr", + "thumb":"https://cms.dmpcdn.com/livetv/2023/09/25/c3898e20-5b4f-11ee-a599-1d1a4f7c1125_webp_original.webp", + "slug":"trueid-sports02", + "title":"TrueID Sports 2", + "content_type":"livetv", + "category":"livetv-ca|sports-ca|trueidtv-sport", + "content_provider":"", + "channel_code":"he004", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"TrueID Sports 2", + "channel_name_th":"ทรูไอดี สปอร์ต 2" + }, + "views":26, + "isLiveChat":false + }, + { + "id":"k3B64mk9ELl3", + "thumb":"https://cms.dmpcdn.com/livetv/2023/07/24/6057ad50-29cc-11ee-846b-a1c4e5181c87_webp_original.webp", + "slug":"golfchannel-thhdplus", + "title":"Golf Channel Thailand HD+", + "content_type":"livetv", + "category":"livetv-ca|sports-ca|tvsnow|sports", + "content_provider":"true_vision", + "channel_code":"t501", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"Golf Channel Thailand HD Plus", + "channel_name_chi":"Golf Channel Thailand HD Plus", + "channel_name_eng":"Golf Channel Thailand HD Plus", + "channel_name_mm":"Golf Channel Thailand HD Plus", + "channel_name_rus":"Golf Channel Thailand HD Plus", + "channel_name_th":"Golf Channel Thailand HD Plus", + "channel_name_vie":"Golf Channel Thailand HD Plus" + }, + "views":26, + "isLiveChat":false + }, + { + "id":"zWoZqZv6J6N5", + "thumb":"https://cms.dmpcdn.com/livetv/2022/10/11/f09e41a0-492e-11ed-bb17-0527d4e1664c_webp_original.png", + "slug":"crime-investigation", + "title":"Crime + Investigation", + "content_type":"livetv", + "category":"livetv-ca|documentary-ca|tvsnow|documentary", + "content_provider":"true_vision", + "channel_code":"t517", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"Crime Investigation", + "channel_name_th":"ไคร์ม แอนด์ อินเวสทิเกชั่น" + }, + "views":25, + "isLiveChat":false + }, + { + "id":"bDKPPGOdyAmn", + "thumb":"https://cms.dmpcdn.com/livetv/2023/07/24/60cba4d0-29cc-11ee-b2f4-e9de482d866e_webp_original.webp", + "slug":"spotv1-hd", + "title":"SPOTV 1", + "content_type":"livetv", + "category":"livetv-ca|sports-ca|tvsnow|sports", + "content_provider":"true_vision", + "channel_code":"t510", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"SPOTV 1", + "channel_name_th":"SPOTV 1" + }, + "views":25, + "isLiveChat":false + }, + { + "id":"zpwxwAgYOV7n", + "thumb":"https://cms.dmpcdn.com/livetv/2022/02/17/3943ca00-8fd6-11ec-b076-dffedf0eab22_webp_original.png", + "slug":"white-channel-hd", + "title":"White Channel", + "content_type":"livetv", + "category":"hbtv-trueidtv-all|livetv-ca|freetv-ca", + "content_provider":"", + "channel_code":"i006", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"White Channel", + "channel_name_th":"ไวท์แชนแนล" + }, + "views":25, + "isLiveChat":false + }, + { + "id":"vW6BOL0AzxdW", + "thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/21b70060-9406-11ee-906d-89adbc3169c1_webp_original.webp", + "slug":"arsenal", + "title":"Arsenal", + "content_type":"livetv", + "category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all", + "content_provider":"", + "channel_code":"ars01", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"Arsenal", + "channel_name_th":"อาร์เซน่อล" + }, + "views":25, + "isLiveChat":false + }, + { + "id":"5YQaWExRqD5", + "thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/87773550-98c4-11ea-b284-2bff0287c295_original.png", + "slug":"dltv-3", + "title":"DLTV 3", + "content_type":"livetv", + "category":"education|hbtv-trueidtv-all|livetv-ca|education-ca", + "content_provider":"", + "channel_code":"dum003", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"DLTV 3", + "channel_name_chi":"DLTV 3", + "channel_name_eng":"DLTV 3", + "channel_name_mm":"DLTV 3", + "channel_name_rus":"DLTV 3", + "channel_name_th":"DLTV 3", + "channel_name_vie":"DLTV 3" + }, + "views":23, + "isLiveChat":false + }, + { + "id":"xPgxpqoyqQ62", + "thumb":"https://cms.dmpcdn.com/livetv/2021/01/06/68be8520-500f-11eb-8d28-4b8e3f30b51b_original.png", + "slug":"zing", + "title":"Zing", + "content_type":"livetv", + "category":"livetv-ca|entertainment-ca|movies-series-ca|trueidtv-all", + "content_provider":"", + "channel_code":"i001", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"Zing", + "channel_name_chi":"Zing", + "channel_name_eng":"Zing", + "channel_name_mm":"Zing", + "channel_name_rus":"Zing", + "channel_name_th":"Zing", + "channel_name_vie":"Zing" + }, + "views":22, + "isLiveChat":false + }, + { + "id":"5XaDjQd1JJgw", + "thumb":"https://cms.dmpcdn.com/livetv/2023/07/24/5f346300-29cc-11ee-b2f4-e9de482d866e_webp_original.webp", + "slug":"bein-sports-hd2", + "title":"beIN SPORTS 2", + "content_type":"livetv", + "category":"livetv-ca|football-ca|sports-ca|trueidtv-all|tvsnow|sports", + "content_provider":"true_vision", + "channel_code":"t521", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"beIN SPORTS 2", + "channel_name_th":"บีอินสปอตส์ 2" + }, + "views":22, + "isLiveChat":false + }, + { + "id":"pmXrb1NjLeP0", + "thumb":"https://cms.dmpcdn.com/livetv/2023/05/03/bbea1690-e966-11ed-935b-df134f58d288_webp_original.png", + "slug":"truepremierfootballhd5", + "title":"True Premier Football 5", + "content_type":"livetv", + "category":"livetv-ca|football-ca|sports-ca|tvsnow|sports", + "content_provider":"true_vision", + "channel_code":"ht115", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"True Premier Football 5", + "channel_name_th":"ทรู พรีเมียร์ ฟุตบอล 5" + }, + "views":21, + "isLiveChat":false + }, + { + "id":"GDna51EdVk4", + "thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/f4888970-e595-11ed-8507-4fc0b025fedb_webp_original.png", + "slug":"truesport-hd-4", + "title":"True Sports 4", + "content_type":"livetv", + "category":"livetv-ca|sports-ca|tvsnow|sports", + "content_provider":"true_vision", + "channel_code":"062", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"True Sports 4", + "channel_name_chi":"True Sports 4", + "channel_name_eng":"True Sports 4", + "channel_name_mm":"True Sports 4", + "channel_name_rus":"True Sports 4", + "channel_name_th":"ทรูสปอร์ต 4", + "channel_name_vie":"True Sports 4" + }, + "views":21, + "isLiveChat":false + }, + { + "id":"o9vKOR0dLVm7", + "thumb":"https://cms.dmpcdn.com/livetv/2023/09/25/c3898e20-5b4f-11ee-a599-1d1a4f7c1125_webp_original.webp", + "slug":"trueid-sports03", + "title":"TrueID Sports 3", + "content_type":"livetv", + "category":"livetv-ca|sports-ca|trueidtv-sport", + "content_provider":"", + "channel_code":"he005", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"TrueID Sports 3", + "channel_name_th":"ทรูไอดี สปอร์ต 3" + }, + "views":20, + "isLiveChat":false + }, + { + "id":"rO7WMREyepr", + "thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/c273ea90-98c4-11ea-bcb3-0320ce420b5e_original.png", + "slug":"dltv-15", + "title":"DLTV 15", + "content_type":"livetv", + "category":"education|hbtv-trueidtv-all|livetv-ca|education-ca", + "content_provider":"", + "channel_code":"dum015", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"DLTV 15", + "channel_name_chi":"DLTV 15", + "channel_name_eng":"DLTV 15", + "channel_name_mm":"DLTV 15", + "channel_name_rus":"DLTV 15", + "channel_name_th":"DLTV 15", + "channel_name_vie":"DLTV 15" + }, + "views":20, + "isLiveChat":false + }, + { + "id":"67ollp0Raz2V", + "thumb":"https://cms.dmpcdn.com/livetv/2022/03/23/f7114a70-aa94-11ec-9b91-03afdbb2e824_webp_original.png", + "slug":"truepremierfootballhd7", + "title":"True Premier Football 7", + "content_type":"livetv", + "category":"livetv-ca|football-ca|sports-ca", + "content_provider":"true_vision", + "channel_code":"t503", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"True Premier Football 7", + "channel_name_th":"ทรู พรีเมียร์ ฟุตบอล 7" + }, + "views":18, + "isLiveChat":false + }, + { + "id":"2KyzkV6AyPZ", + "thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/65dc9bb0-98c4-11ea-bcb3-0320ce420b5e_original.png", + "slug":"dltv-1", + "title":"DLTV 1", + "content_type":"livetv", + "category":"education|hbtv-trueidtv-all|livetv-ca|education-ca", + "content_provider":"", + "channel_code":"dum001", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"DLTV 1", + "channel_name_chi":"DLTV 1", + "channel_name_eng":"DLTV 1", + "channel_name_mm":"DLTV 1", + "channel_name_rus":"DLTV 1", + "channel_name_th":"DLTV 1", + "channel_name_vie":"DLTV 1" + }, + "views":17, + "isLiveChat":false + }, + { + "id":"gqVn9n7MeYXq", + "thumb":"https://cms.dmpcdn.com/livetv/2022/09/08/e55200a0-2f27-11ed-a458-efe831982670_webp_original.png", + "slug":"arirang-tv", + "title":"Arirang TV", + "content_type":"livetv", + "category":"livetv-ca|entertainment-ca|tvsnow|entertainment", + "content_provider":"true_vision", + "channel_code":"t519", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"Arirang TV", + "channel_name_th":"Arirang TV" + }, + "views":16, + "isLiveChat":false + }, + { + "id":"r4PaaOpzr0Ow", + "thumb":"https://cms.dmpcdn.com/livetv/2022/03/23/f868c420-aa94-11ec-9b91-03afdbb2e824_webp_original.png", + "slug":"truepremierfootballhd8", + "title":"True Premier Football 8", + "content_type":"livetv", + "category":"livetv-ca|football-ca|sports-ca", + "content_provider":"true_vision", + "channel_code":"t504", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"True Premier Football 8", + "channel_name_th":"ทรู พรีเมียร์ ฟุตบอล 8" + }, + "views":16, + "isLiveChat":false + }, + { + "id":"Kz5zjkGyDVA", + "thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/66ce9cd0-98c4-11ea-b284-2bff0287c295_original.png", + "slug":"dltv-6", + "title":"DLTV 6", + "content_type":"livetv", + "category":"education|hbtv-trueidtv-all|livetv-ca|education-ca", + "content_provider":"", + "channel_code":"dum006", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"DLTV 6", + "channel_name_chi":"DLTV 6", + "channel_name_eng":"DLTV 6", + "channel_name_mm":"DLTV 6", + "channel_name_rus":"DLTV 6", + "channel_name_th":"DLTV 6", + "channel_name_vie":"DLTV 6" + }, + "views":16, + "isLiveChat":false + }, + { + "id":"Veb1NRpQ6LXk", + "thumb":"https://cms.dmpcdn.com/livetv/2022/02/10/16689820-8a46-11ec-8573-9fd52c482da3_webp_original.png", + "slug":"zee-anmol-sd", + "title":"Zee Anmol", + "content_type":"livetv", + "category":"livetv-ca|entertainment-ca|freetv-ca|movies-series-ca", + "content_provider":"", + "channel_code":"i005", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"Zee Anmol", + "channel_name_th":"Zee Anmol" + }, + "views":15, + "isLiveChat":false + }, + { + "id":"L3Jbvn0BnbA", + "thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/65debe90-98c4-11ea-b284-2bff0287c295_original.png", + "slug":"dltv-2", + "title":"DLTV 2", + "content_type":"livetv", + "category":"education|hbtv-trueidtv-all|livetv-ca|education-ca", + "content_provider":"", + "channel_code":"dum002", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"DLTV 2", + "channel_name_chi":"DLTV 2", + "channel_name_eng":"DLTV 2", + "channel_name_mm":"DLTV 2", + "channel_name_rus":"DLTV 2", + "channel_name_th":"DLTV 2", + "channel_name_vie":"DLTV 2" + }, + "views":15, + "isLiveChat":false + }, + { + "id":"v2M0K4kgbrN", + "thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/66317270-98c4-11ea-b284-2bff0287c295_original.png", + "slug":"dltv-4", + "title":"DLTV 4", + "content_type":"livetv", + "category":"education|hbtv-trueidtv-all|livetv-ca|education-ca", + "content_provider":"", + "channel_code":"dum004", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"DLTV 4", + "channel_name_chi":"DLTV 4", + "channel_name_eng":"DLTV 4", + "channel_name_mm":"DLTV 4", + "channel_name_rus":"DLTV 4", + "channel_name_th":"DLTV 4", + "channel_name_vie":"DLTV 4" + }, + "views":15, + "isLiveChat":false + }, + { + "id":"RGdlapJnLQNG", + "thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/f8af80b0-9406-11ee-8d65-879d2e0f23a3_webp_original.webp", + "slug":"manchester-city", + "title":"Manchester City", + "content_type":"livetv", + "category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all", + "content_provider":"", + "channel_code":"mci01", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"Manchester City", + "channel_name_th":"แมนซิตี้" + }, + "views":15, + "isLiveChat":false + }, + { + "id":"JkpG4LeljXJ0", + "thumb":"https://cms.dmpcdn.com/livetv/2023/08/02/8a0c49a0-30e1-11ee-b220-4544ede97b74_webp_original.webp", + "slug":"true-ball-thai-2", + "title":"True Ball Thai 2", + "content_type":"livetv", + "category":"livetv-ca|football-ca|sports-ca|tvsnow|sports", + "content_provider":"true_vision", + "channel_code":"vc02", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"True Ball Thai 2", + "channel_name_th":"True Ball Thai 2" + }, + "views":14, + "isLiveChat":false + }, + { + "id":"Yb4p39lbgvN", + "thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/6683b120-98c4-11ea-b284-2bff0287c295_original.png", + "slug":"dltv-5", + "title":"DLTV 5", + "content_type":"livetv", + "category":"education|hbtv-trueidtv-all|livetv-ca|education-ca", + "content_provider":"", + "channel_code":"dum005", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"DLTV 5", + "channel_name_chi":"DLTV 5", + "channel_name_eng":"DLTV 5", + "channel_name_mm":"DLTV 5", + "channel_name_rus":"DLTV 5", + "channel_name_th":"DLTV 5", + "channel_name_vie":"DLTV 5" + }, + "views":14, + "isLiveChat":false + }, + { + "id":"D38Lb540KAE3", + "thumb":"https://cms.dmpcdn.com/livetv/2021/02/24/cc358130-764d-11eb-9057-2d10fb4d0cf4_original.png", + "slug":"MediaTV", + "title":"Media TV", + "content_type":"livetv", + "category":"free-tv|livetv-ca|freetv-ca", + "content_provider":"", + "channel_code":"i003", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"Media TV", + "channel_name_th":"มีเดีย ทีวี" + }, + "views":13, + "isLiveChat":false + }, + { + "id":"JGAQ7VZpX9Y", + "thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/044a2a10-98c5-11ea-bcb3-0320ce420b5e_original.png", + "slug":"dltv-12", + "title":"DLTV 12", + "content_type":"livetv", + "category":"education|hbtv-trueidtv-all|livetv-ca|education-ca", + "content_provider":"", + "channel_code":"dum012", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"DLTV 12", + "channel_name_chi":"DLTV 12", + "channel_name_eng":"DLTV 12", + "channel_name_mm":"DLTV 12", + "channel_name_rus":"DLTV 12", + "channel_name_th":"DLTV 12", + "channel_name_vie":"DLTV 12" + }, + "views":13, + "isLiveChat":false + }, + { + "id":"zyab4aWZ0OWx", + "thumb":"https://cms.dmpcdn.com/livetv/2023/05/03/bb0beb90-e966-11ed-993c-b59183950f79_webp_original.png", + "slug":"truepremierfootballhd4", + "title":"True Premier Football 4", + "content_type":"livetv", + "category":"livetv-ca|football-ca|sports-ca|tvsnow|sports", + "content_provider":"true_vision", + "channel_code":"ht114", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"True Premier Football 4", + "channel_name_th":"ทรู พรีเมียร์ ฟุตบอล 4" + }, + "views":12, + "isLiveChat":false + }, + { + "id":"pQ6ok8M72AD", + "thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/bfd21690-98c4-11ea-bcb3-0320ce420b5e_original.png", + "slug":"dltv-10", + "title":"DLTV 10", + "content_type":"livetv", + "category":"education|hbtv-trueidtv-all|livetv-ca|education-ca", + "content_provider":null, + "channel_code":"dum010", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"DLTV 10", + "channel_name_chi":"DLTV 10", + "channel_name_eng":"DLTV 10", + "channel_name_mm":"DLTV 10", + "channel_name_rus":"DLTV 10", + "channel_name_th":"DLTV 10", + "channel_name_vie":"DLTV 10" + }, + "views":12, + "isLiveChat":false + }, + { + "id":"wkrQgY603zM", + "thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/c2364550-98c4-11ea-bcb3-0320ce420b5e_original.png", + "slug":"dltv-14", + "title":"DLTV 14", + "content_type":"livetv", + "category":"education|hbtv-trueidtv-all|livetv-ca|education-ca", + "content_provider":"", + "channel_code":"dum014", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"DLTV 14", + "channel_name_chi":"DLTV 14", + "channel_name_eng":"DLTV 14", + "channel_name_mm":"DLTV 14", + "channel_name_rus":"DLTV 14", + "channel_name_th":"DLTV 14", + "channel_name_vie":"DLTV 14" + }, + "views":12, + "isLiveChat":false + }, + { + "id":"nYvz5QLWjyD", + "thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/67145860-98c4-11ea-b284-2bff0287c295_original.png", + "slug":"dltv-7", + "title":"DLTV 7", + "content_type":"livetv", + "category":"education|hbtv-trueidtv-all|livetv-ca|education-ca", + "content_provider":"", + "channel_code":"dum007", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"DLTV 7", + "channel_name_chi":"DLTV 7", + "channel_name_eng":"DLTV 7", + "channel_name_mm":"DLTV 7", + "channel_name_rus":"DLTV 7", + "channel_name_th":"DLTV 7", + "channel_name_vie":"DLTV 7" + }, + "views":11, + "isLiveChat":false + }, + { + "id":"MndX5W8rWaMn", + "thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/7003e750-9407-11ee-b445-0b5cfb8bf6f8_webp_original.webp", + "slug":"tottenham-hotspur", + "title":"Tottenham Hotspur", + "content_type":"livetv", + "category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all", + "content_provider":"", + "channel_code":"tot01", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"Tottenham Hotspur", + "channel_name_th":"สเปอร์" + }, + "views":11, + "isLiveChat":false + }, + { + "id":"E9k68z9aKDp", + "thumb":"https://cms.dmpcdn.com/livetv/2017/10/18/f1b957db-b175-45fc-ab2b-60150f9c570a.png", + "slug":"tnn-2", + "title":"TNN 2", + "content_type":"livetv", + "category":"livetv-ca|freetv-ca|news-ca|tvsnow|tvsnews", + "content_provider":"true_vision", + "channel_code":"074", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"TNN 2", + "channel_name_chi":"TNN 2", + "channel_name_eng":"TNN 2", + "channel_name_mm":"TNN 2", + "channel_name_rus":"TNN 2", + "channel_name_th":"TNN 2", + "channel_name_vie":"TNN 2" + }, + "views":10, + "isLiveChat":false + }, + { + "id":"JeQ5L9PpVBJ", + "thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/bf927580-98c4-11ea-bcb3-0320ce420b5e_original.png", + "slug":"dltv-9", + "title":"DLTV 9", + "content_type":"livetv", + "category":"education|hbtv-trueidtv-all|livetv-ca|education-ca", + "content_provider":"", + "channel_code":"dum009", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"DLTV 9", + "channel_name_chi":"DLTV 9", + "channel_name_eng":"DLTV 9", + "channel_name_mm":"DLTV 9", + "channel_name_rus":"DLTV 9", + "channel_name_th":"DLTV 9", + "channel_name_vie":"DLTV 9" + }, + "views":10, + "isLiveChat":false + }, + { + "id":"M34YDGLk2wVj", + "thumb":"https://cms.dmpcdn.com/livetv/2023/08/02/8a3b21d0-30e1-11ee-a53e-b3f87dc8ba1e_webp_original.webp", + "slug":"true-ball-thai-3", + "title":"True Ball Thai 3", + "content_type":"livetv", + "category":"livetv-ca|football-ca|sports-ca|tvsnow|sports", + "content_provider":"true_vision", + "channel_code":"vc03", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"True Ball Thai 3", + "channel_name_th":"True Ball Thai 3" + }, + "views":9, + "isLiveChat":false + }, + { + "id":"R4WyxL6Mp8b", + "thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/bf9386f0-98c4-11ea-b284-2bff0287c295_original.png", + "slug":"dltv-8", + "title":"DLTV 8", + "content_type":"livetv", + "category":"education|hbtv-trueidtv-all|livetv-ca|education-ca", + "content_provider":"", + "channel_code":"dum008", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"DLTV 8", + "channel_name_chi":"DLTV 8", + "channel_name_eng":"DLTV 8", + "channel_name_mm":"DLTV 8", + "channel_name_rus":"DLTV 8", + "channel_name_th":"DLTV 8", + "channel_name_vie":"DLTV 8" + }, + "views":9, + "isLiveChat":false + }, + { + "id":"6Qna2oVjq3P", + "thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/c2335f20-98c4-11ea-bcb3-0320ce420b5e_original.png", + "slug":"dltv-13", + "title":"DLTV 13", + "content_type":"livetv", + "category":"education|hbtv-trueidtv-all|livetv-ca|education-ca", + "content_provider":"", + "channel_code":"dum013", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"DLTV 13", + "channel_name_chi":"DLTV 13", + "channel_name_eng":"DLTV 13", + "channel_name_mm":"DLTV 13", + "channel_name_rus":"DLTV 13", + "channel_name_th":"DLTV 13", + "channel_name_vie":"DLTV 13" + }, + "views":9, + "isLiveChat":false + }, + { + "id":"3JYow6Dx7zx0", + "thumb":"https://cms.dmpcdn.com/livetv/2023/08/02/2f7ad050-30f6-11ee-b57d-a9829f092f3e_webp_original.webp", + "slug":"bein-sports-6", + "title":"beIN SPORTS 6", + "content_type":"livetv", + "category":"livetv-ca|football-ca|sports-ca|tvsnow|sports", + "content_provider":"", + "channel_code":"216", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"beIN SPORTS 6", + "channel_name_th":"บีอินสปอตส์ 6" + }, + "views":7, + "isLiveChat":false + }, + { + "id":"EGvbeMNZOwq", + "thumb":"https://cms.dmpcdn.com/livetv/2020/05/18/bfdaa210-98c4-11ea-bcb3-0320ce420b5e_original.png", + "slug":"dltv-11", + "title":"DLTV 11", + "content_type":"livetv", + "category":"education|hbtv-trueidtv-all|livetv-ca|education-ca", + "content_provider":"", + "channel_code":"dum011", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"DLTV 11", + "channel_name_chi":"DLTV 11", + "channel_name_eng":"DLTV 11", + "channel_name_mm":"DLTV 11", + "channel_name_rus":"DLTV 11", + "channel_name_th":"DLTV 11", + "channel_name_vie":"DLTV 11" + }, + "views":6, + "isLiveChat":false + }, + { + "id":"JpawvVMe6aXO", + "thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/1bf1afd0-9407-11ee-8d65-879d2e0f23a3_webp_original.webp", + "slug":"newcastle-united", + "title":"Newcastle United", + "content_type":"livetv", + "category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all", + "content_provider":"", + "channel_code":"new01", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"Newcastle United", + "channel_name_th":"นิวคาสเซิล" + }, + "views":6, + "isLiveChat":false + }, + { + "id":"RVrAxAOGx21v", + "thumb":"https://cms.dmpcdn.com/livetv/2022/09/08/e5b667c0-2f27-11ed-9e57-d98920d4c462_webp_original.png", + "slug":"dw-english", + "title":"DW English", + "content_type":"livetv", + "category":"livetv-ca|entertainment-ca|trueunlock-ca|tvsnow|entertainment", + "content_provider":"true_vision", + "channel_code":"t518", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"DW English", + "channel_name_th":"ดี ดับเบิ้ลยู อิงลิช" + }, + "views":5, + "isLiveChat":false + }, + { + "id":"eyEPa8A2WaJN", + "thumb":"https://cms.dmpcdn.com/livetv/2023/08/02/2f7a5b20-30f6-11ee-8c65-b3a6cba5ed9d_webp_original.webp", + "slug":"beinsports-4", + "title":"beIN SPORTS 4", + "content_type":"livetv", + "category":"livetv-ca|football-ca|sports-ca|tvsnow|sports", + "content_provider":"", + "channel_code":"217", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"beIN SPORTS 4", + "channel_name_th":"บีอินสปอตส์ 4" + }, + "views":5, + "isLiveChat":false + }, + { + "id":"pQNm6nA20a6e", + "thumb":"https://cms.dmpcdn.com/livetv/2023/08/02/300adb50-30f6-11ee-b3e7-85edd640cc04_webp_original.webp", + "slug":"beinsports-5", + "title":"beIN SPORTS 5", + "content_type":"livetv", + "category":"livetv-ca|football-ca|sports-ca|tvsnow|sports", + "content_provider":"", + "channel_code":"219", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"beIN SPORTS 5", + "channel_name_th":"บีอินสปอตส์ 5" + }, + "views":2, + "isLiveChat":false + }, + { + "id":"YZzDXGM1Yd68", + "thumb":"https://cms.dmpcdn.com/livetv/2021/10/28/3965c0a0-3794-11ec-8e1f-6bce3683de8c_webp_original.png", + "slug":"Event2", + "title":"Event 2", + "content_type":"livetv", + "category":"hbtv-trueidtv-all|hbtv-truetv-sport|ott-trueidtv-sport|sport|trueidtv-all|trueidtv-sport", + "content_provider":"", + "channel_code":"ev03", + "content_rights":null, + "channel_info":null, + "views":1, + "isLiveChat":false + }, + { + "id":"LVQzz7xplYpP", + "thumb":"https://cms.dmpcdn.com/livetv/2021/10/28/3965c0a0-3794-11ec-8e1f-6bce3683de8c_webp_original.png", + "slug":"Event5", + "title":"Event 5", + "content_type":"livetv", + "category":"hbtv-trueidtv-all|hbtv-truetv-sport|ott-trueidtv-sport|sport|trueidtv-all|trueidtv-sport", + "content_provider":null, + "channel_code":"emer05", + "content_rights":null, + "channel_info":null, + "views":1, + "isLiveChat":false + }, + { + "id":"xVW7oVd8Gen", + "thumb":"https://cms.dmpcdn.com/livetv/2020/06/23/99bc7f60-b550-11ea-8fac-236a281cd6c5_original.png", + "slug":"super-entertain", + "title":"Super Bunteung", + "content_type":"livetv", + "category":"hbtv-trueidtv-all|hbtv-truetv-entertainment|tvsnow|entertainment|livetv-ca|entertainment-ca", + "content_provider":"true_vision", + "channel_code":"108", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"ស៊ុបព័រអិនធើធែនមិន", + "channel_name_eng":"Super Bunteung", + "channel_name_mm":"အထူးေဖ်ာ္ေျဖမႈမ်ား", + "channel_name_th":"ซุปเปอร์ บันเทิง" + }, + "views":1, + "isLiveChat":false + }, + { + "id":"rVWJGN1VLOB", + "thumb":"https://cms.dmpcdn.com/livetv/2017/10/17/31a68f7b-d24e-43e0-9403-22f5e48f081b.png", + "slug":"etv", + "title":"ETV", + "content_type":"livetv", + "category":"hbtv-trueidtv-all|tvsnow|entertainment|livetv-ca|entertainment-ca|education-ca", + "content_provider":"true_vision", + "channel_code":"da2", + "content_rights":null, + "channel_info":{ + "channel_name_cbd":"ETV", + "channel_name_chi":"ETV", + "channel_name_eng":"ETV", + "channel_name_mm":"ETV", + "channel_name_rus":"ETV", + "channel_name_th":"ETV", + "channel_name_vie":"ETV" + }, + "views":1, + "isLiveChat":false + }, + { + "id":"vAG5EZznD1Kl", + "thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/7e72f8d0-9407-11ee-b32f-2d43ff6700d5_webp_original.webp", + "slug":"west-ham-united", + "title":"West Ham United", + "content_type":"livetv", + "category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all", + "content_provider":"", + "channel_code":"whu01", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"West Ham United", + "channel_name_th":"เวสต์แฮม" + }, + "views":1, + "isLiveChat":false + }, + { + "id":"nle3eNnyVpag", + "thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/a4879e50-9406-11ee-b543-51f040e58632_webp_original.webp", + "slug":"crystal-palace", + "title":"Crystal Palace", + "content_type":"livetv", + "category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all", + "content_provider":"", + "channel_code":"cry01", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"Crystal-Palace", + "channel_name_th":"คริสตัลพาเลซ" + }, + "views":1, + "isLiveChat":false + }, + { + "id":"GB0gZlzxgnJr", + "thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/0a03a630-9406-11ee-906d-89adbc3169c1_webp_original.webp", + "slug":"bournemouth", + "title":"A.F.C. Bournemouth", + "content_type":"livetv", + "category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all", + "content_provider":"", + "channel_code":"bou01", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"Bournemouth", + "channel_name_th":"บอร์นมัธ" + }, + "views":1, + "isLiveChat":false + }, + { + "id":"1ZrGkEk3qP7L", + "thumb":"https://cms.dmpcdn.com/livetv/2022/07/18/79461e70-0667-11ed-b687-85f145af88ed_webp_original.png", + "slug":"test3", + "title":"ช่องทดสอบออกอากาศที่ 3", + "content_type":"livetv", + "category":"livetv-ca|sports-ca", + "content_provider":"", + "channel_code":"tmp005", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"ช่องทดสอบออกอากาศที่ 3", + "channel_name_th":"ช่องทดสอบออกอากาศที่ 3" + }, + "views":0, + "isLiveChat":false + }, + { + "id":"6G190MBm2kkG", + "thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/7bb41c60-9406-11ee-a0fd-836d91d2dd6e_webp_original.webp", + "slug":"burnley", + "title":"Burnley", + "content_type":"livetv", + "category":"livetv-ca|football-ca|sports-ca|trueunlock-ca", + "content_provider":"", + "channel_code":"brn01", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"Burnley", + "channel_name_th":"เบิร์นลีย์" + }, + "views":0, + "isLiveChat":false + }, + { + "id":"4GePx966Dzao", + "thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/e8af9600-9406-11ee-b625-274874732f96_webp_original.webp", + "slug":"luton-town", + "title":"Luton Town", + "content_type":"livetv", + "category":"livetv-ca|football-ca|sports-ca|trueunlock-ca", + "content_provider":"", + "channel_code":"lut01", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"Luton Town", + "channel_name_th":"ลูตัน ทาวน์" + }, + "views":0, + "isLiveChat":false + }, + { + "id":"4NYqR5KyQArN", + "thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/61985840-9407-11ee-a0fd-836d91d2dd6e_webp_original.webp", + "slug":"sheffield-united", + "title":"Sheffield United", + "content_type":"livetv", + "category":"livetv-ca|football-ca|sports-ca|trueunlock-ca", + "content_provider":"", + "channel_code":"shu01", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"Sheffield United", + "channel_name_th":"เชฟฟิลด์ ยูไนเต็ด" + }, + "views":0, + "isLiveChat":false + }, + { + "id":"xAzllg2VXjRm", + "thumb":"https://cms.dmpcdn.com/livetv/2021/10/28/3965c0a0-3794-11ec-8e1f-6bce3683de8c_webp_original.png", + "slug":"Event3", + "title":"Event 3", + "content_type":"livetv", + "category":"hbtv-trueidtv-all|hbtv-truetv-sport|ott-trueidtv-sport|sport|trueidtv-all|trueidtv-sport", + "content_provider":null, + "channel_code":"emer03", + "content_rights":null, + "channel_info":null, + "views":0, + "isLiveChat":false + }, + { + "id":"WGVqq6zeAzaZ", + "thumb":"https://cms.dmpcdn.com/livetv/2021/10/28/3965c0a0-3794-11ec-8e1f-6bce3683de8c_webp_original.png", + "slug":"Event4", + "title":"Event 4", + "content_type":"livetv", + "category":"hbtv-trueidtv-all|hbtv-truetv-sport|ott-trueidtv-sport|sport|trueidtv-all|trueidtv-sport", + "content_provider":null, + "channel_code":"emer04", + "content_rights":null, + "channel_info":null, + "views":0, + "isLiveChat":false + }, + { + "id":"RjY3XkeL5Mwl", + "thumb":"https://cms.dmpcdn.com/livetv/2021/10/28/3965c0a0-3794-11ec-8e1f-6bce3683de8c_webp_original.png", + "slug":"Event1", + "title":"Event 1", + "content_type":"livetv", + "category":"hbtv-trueidtv-all|hbtv-truetv-sport|ott-trueidtv-sport|sport|trueidtv-all|trueidtv-sport", + "content_provider":null, + "channel_code":"ev02", + "content_rights":null, + "channel_info":null, + "views":0, + "isLiveChat":false + }, + { + "id":"glmE5eNRz47l", + "thumb":"https://cms.dmpcdn.com/livetv/2022/07/18/79461e70-0667-11ed-b687-85f145af88ed_webp_original.png", + "slug":"test", + "title":"ช่องทดสอบการออกอากาศ", + "content_type":"livetv", + "category":"livetv-ca", + "content_provider":"", + "channel_code":"tmp003", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"ทดสอบการออกอากาศ", + "channel_name_th":"ทดสอบการออกอากาศ" + }, + "views":0, + "isLiveChat":false + }, + { + "id":"RvJwkNg06Qre", + "thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/35f494c0-9406-11ee-b32f-2d43ff6700d5_webp_original.webp", + "slug":"aston-villa", + "title":"Aston Villa", + "content_type":"livetv", + "category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all", + "content_provider":"", + "channel_code":"avl01", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"Aston Villa", + "channel_name_th":"แอสตันวิลล่า" + }, + "views":0, + "isLiveChat":false + }, + { + "id":"Vjb43gpNAVnl", + "thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/b6bb0ad0-9406-11ee-b543-51f040e58632_webp_original.webp", + "slug":"everton", + "title":"Everton", + "content_type":"livetv", + "category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all", + "content_provider":"", + "channel_code":"eve01", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"Everton", + "channel_name_th":"เอเวอร์ตัน" + }, + "views":0, + "isLiveChat":false + }, + { + "id":"K24pNw8k5Kj2", + "thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/94ed36c0-9407-11ee-906d-89adbc3169c1_webp_original.webp", + "slug":"wolves", + "title":"Wolves", + "content_type":"livetv", + "category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all", + "content_provider":"", + "channel_code":"wol01", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"Wolves", + "channel_name_th":"วูลฟ์" + }, + "views":0, + "isLiveChat":false + }, + { + "id":"vQEl8Do0nK46", + "thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/c843df70-9406-11ee-8d65-879d2e0f23a3_webp_original.webp", + "slug":"fulham", + "title":"Fulham", + "content_type":"livetv", + "category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all", + "content_provider":"", + "channel_code":"ful01", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"Fulham", + "channel_name_th":"ฟูแล่ม" + }, + "views":0, + "isLiveChat":false + }, + { + "id":"oDqg2NPZdJz5", + "thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/600d67f0-9406-11ee-ab3e-a51daa175c33_webp_original.webp", + "slug":"brighton-and-hove-albion", + "title":"Brighton & Hove Albion", + "content_type":"livetv", + "category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all", + "content_provider":"", + "channel_code":"bha01", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"Brighton-and-Hove-Albion", + "channel_name_th":"ไบร์ทตัน" + }, + "views":0, + "isLiveChat":false + }, + { + "id":"ykaXNqEMoPZR", + "thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/30299ee0-9407-11ee-a469-0b60cd4a260f_webp_original.webp", + "slug":"nottingham-forest", + "title":"Nottingham Forest", + "content_type":"livetv", + "category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all", + "content_provider":"", + "channel_code":"nfo01", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"Nottingham Forest", + "channel_name_th":"ฟอร์เรสต์" + }, + "views":0, + "isLiveChat":false + }, + { + "id":"3gaL3mjZoxrE", + "thumb":"https://cms.dmpcdn.com/livetv/2023/12/06/4738bf40-9406-11ee-a0fd-836d91d2dd6e_webp_original.webp", + "slug":"brentford", + "title":"Brentford", + "content_type":"livetv", + "category":"hbtv-trueidtv-all|livetv-ca|football-ca|sports-ca|true-unlock|true-unlock-atv|trueidtv-all", + "content_provider":"", + "channel_code":"bre01", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"Brentford", + "channel_name_th":"เบรนท์ฟอร์ด" + }, + "views":0, + "isLiveChat":false + }, + { + "id":"2Ag1bgVdNwoL", + "thumb":"https://cms.dmpcdn.com/livetv/2022/07/18/79461e70-0667-11ed-b687-85f145af88ed_webp_original.png", + "slug":"test2", + "title":"ช่องทดสอบออกอากาศที่2", + "content_type":"livetv", + "category":"livetv-ca|sports-ca", + "content_provider":"", + "channel_code":"tmp004", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"ช่องทดสอบออกอากาศที่2", + "channel_name_th":"ช่องทดสอบออกอากาศที่2" + }, + "views":0, + "isLiveChat":false + }, + { + "id":"OzKE5d4pdNwy", + "thumb":"https://cms.dmpcdn.com/livetv/2022/11/16/88521af0-6598-11ed-8215-b386a7bd4f58_webp_original.png", + "slug":"ufc", + "title":"UFC", + "content_type":"livetv", + "category":"livetv-ca|sports-ca|tvsnow|sports", + "content_provider":"true_vision", + "channel_code":"vc04", + "content_rights":null, + "channel_info":{ + "channel_name_eng":"UFC", + "channel_name_th":"ยูเอฟซี" + }, + "views":0, + "isLiveChat":false + } + ], + "channelSlug":"true-movie-hits", + "baseShelves":{ + "adsConfig":{ + "adsNetworkId":"", + "adsUnit":"21682623839/TrueID_Web/TV" + }, + "id":"3MPnXKpGjKqQ", + "shelfItems":[ + { + "id":"G3rooMXA2b4Z", + "title":{ + "th":"แนะนำ", + "en":"Featured" + }, + "type":"by_banner_homepage", + "viewType":"horizontal", + "shelfItems":[ + { + "id":"J1X89KQE7001", + "title":"ContentMkt_AVOD_Series_SpiceAndSpell", + "thumb":"https://cms.dmpcdn.com/hilight/2023/11/24/890e7c60-8aac-11ee-ad2b-e5fcaaed0aed_webp_original.webp", + "redirectUrl":"https://movie.trueid.net/series/kbNxLxw5Eg52/rV1rP8Dj27kB/AkMBP4z2VOKG/XrVjVjeWXz2r", + "article_category":null, + "content_type":"hilight" + }, + { + "id":"xrGQRbjWx1EL", + "title":"ContentMkt_TVOD_Movie_MI7Part1", + "thumb":"https://cms.dmpcdn.com/hilight/2023/11/23/32f66c30-89cf-11ee-976f-abdcd950f267_webp_original.webp", + "redirectUrl":"https://movie.trueid.net/movie/xDnNeqGkreJD", + "article_category":null, + "content_type":"hilight" + }, + { + "id":"J52JxN0jXkA5", + "title":"ContentMkt_SVOD_Asianseries_MyPrecious", + "thumb":"https://cms.dmpcdn.com/hilight/2023/11/06/00af5020-7c19-11ee-a9a1-41799b41aff4_webp_original.webp", + "redirectUrl":"", + "article_category":null, + "content_type":"hilight" + }, + { + "id":"yb22AdYZnQGb", + "title":"ContentMkt_AVOD_Lakorn_BakeMePlease", + "thumb":"https://cms.dmpcdn.com/hilight/2023/11/24/48523fc0-8aa9-11ee-bf9d-a586c3ad0143_webp_original.webp", + "redirectUrl":"https://movie.trueid.net/series/kN0QYgOQ0EJ5/zWBxNa65n6Vv/Eadp1WLqDXra/yqJgdKrkyRLY", + "article_category":null, + "content_type":"hilight" + }, + { + "id":"VmW2pEP9q3rw", + "title":"ContentMkt_AVOD_Anime_JujutsuKaisen", + "thumb":"https://cms.dmpcdn.com/hilight/2023/11/24/88238020-8aac-11ee-873d-3f4e5da5fd9c_webp_original.webp", + "redirectUrl":"https://movie.trueid.net/series/LgR5wpRnPQVA/qYQ00Mby07Rl/MvKZ6RnxKRaQ/jdAX0Wagxl4R", + "article_category":null, + "content_type":"hilight" + }, + { + "id":"BXGpEAn51mwW", + "title":"ContentMkt_TVOD_Movie_Ambulance", + "thumb":"https://cms.dmpcdn.com/hilight/2023/11/23/32aa20f0-89cf-11ee-ae81-d157aa7f87b4_webp_original.webp", + "redirectUrl":"https://movie.trueid.net/movie/Eq63XJL4okzw", + "article_category":null, + "content_type":"hilight" + }, + { + "id":"DoLWMe0YeQR8", + "title":"ContentMkt_AVOD_Anime_DarkGathering", + "thumb":"https://cms.dmpcdn.com/hilight/2023/11/23/316e93b0-89cf-11ee-a5ec-b3b59a2ebd97_webp_original.webp", + "redirectUrl":"https://movie.trueid.net/series/d6425Em1DPjQ/QMrQQgYaEDGZ/OAqkmxqX05DQ/1G8xwp6ja86G", + "article_category":null, + "content_type":"hilight" + }, + { + "id":"1PddoL4xG12P", + "title":"ContentMkt_AVOD_Anime_OnePiece", + "thumb":"https://cms.dmpcdn.com/hilight/2023/11/24/88241c60-8aac-11ee-bf9d-a586c3ad0143_webp_original.webp", + "redirectUrl":"https://movie.trueid.net/th-th/series/kxqkPYqVBq0D/4AoxBYpn4L1W/p2N5lYZGlVlL/lZ78MWznOElG", + "article_category":null, + "content_type":"hilight" + }, + { + "id":"nYa5A41VxmXY", + "title":"TruelD One Package ความบันเทิงระดับโลก แบบไร้ขีดจำกัด V21-23", + "thumb":"https://cms.dmpcdn.com/hilight/2023/11/13/84d3ef30-8215-11ee-84cd-3b76e2935cfd_webp_original.webp", + "redirectUrl":"https://home.trueid.net/external-browser?website=https://myaccount.trueid.net/checkout?promotionCode=SUPERBUNDLE_TID_IQIYI_WETV_PRIME&utm_campaign=Package_NA_NA_TrueIDOne&utm_medium=inside-platform&utm_source=Today_new_release", + "article_category":null, + "content_type":"hilight" + }, + { + "id":"VKvARdba10aW", + "title":"What's Wrong with My Princess", + "thumb":"https://cms.dmpcdn.com/hilight/2023/11/27/77b75b30-8cd2-11ee-9352-bd2663fbb8ed_webp_original.webp", + "redirectUrl":"https://movie.trueid.net/series/mP3G7aOXQGXP/QeDwwRKOL2Mp/RvD2p1OpZMRQ/zxnrl6JJnZ2x", + "article_category":null, + "content_type":"hilight" + } + ] + }, + { + "id":"k42naQeVKbK4", + "title":{ + "th":"", + "en":"" + }, + "type":"by_ads", + "viewType":"horizontal", + "shelfItems":[ + { + "ALL":{ + "targetingArguments":{ + "TrueID_page":[ + + ], + "Device":[ + + ] + }, + "sizeMapping":[ + { + "viewport":[ + 1280, + 0 + ], + "sizes":[ + [ + 750, + 200 + ], + [ + 970, + 90 + ], + [ + 728, + 90 + ], + "fluid", + [ + 800, + 250 + ], + [ + 970, + 250 + ], + [ + 1, + 1 + ], + [ + 1280, + 250 + ] + ] + }, + { + "viewport":[ + 375, + 0 + ], + "sizes":[ + [ + 1, + 1 + ], + [ + 320, + 250 + ], + [ + 375, + 250 + ], + "fluid", + [ + 300, + 250 + ], + [ + 320, + 100 + ] + ] + }, + { + "viewport":[ + 800, + 0 + ], + "sizes":[ + "fluid", + [ + 640, + 250 + ], + [ + 800, + 250 + ], + [ + 1, + 1 + ], + [ + 728, + 90 + ] + ] + }, + { + "viewport":[ + 0, + 0 + ], + "sizes":[ + [ + 320, + 50 + ], + [ + 320, + 100 + ], + [ + 1, + 1 + ] + ] + } + ], + "slotId":"div-gpt-ad-lb-1", + "adUnit":"21682623839/TrueID_Web/TV", + "sizes":[ + [ + 970, + 90 + ], + [ + 728, + 90 + ] + ] + } + } + ] + }, + { + "id":"O8pKrLmQlj2a", + "title":{ + "th":"ช่องฟรีทีวีฮิต", + "en":"Free TV" + }, + "type":"by_livetv_channel", + "viewType":"vertical", + "shelfItems":[ + { + "id":"wKngqJ2Vqnl", + "title":"MONO 29", + "thumb":"https://cms.dmpcdn.com/livetv/2019/01/10/35a35017-8473-4953-8474-5c58d805b74a.png", + "redirectUrl":"mono29", + "channel_code":"d43", + "views":33721, + "article_category":[ + "livetv-ca", + "digitaltv-ca", + "freetv-ca", + "movies-series-ca" + ], + "content_type":"livetv", + "content_rights":"", + "isLiveChat":false + }, + { + "id":"yYk6PvXwXDb", + "title":"WorkPoint TV", + "thumb":"https://cms.dmpcdn.com/livetv/2023/11/17/2a1de990-852d-11ee-bf98-41acc8fd04fc_webp_original.webp", + "redirectUrl":"workpointtv", + "channel_code":"d83", + "views":6075, + "article_category":[ + "livetv-ca", + "digitaltv-ca", + "entertainment-ca", + "freetv-ca" + ], + "content_type":"livetv", + "content_rights":"", + "isLiveChat":false + }, + { + "id":"QRP2K658b7G", + "title":"Thai PBS", + "thumb":"https://cms.dmpcdn.com/livetv/2023/09/15/ab170410-5377-11ee-8e1b-194edbb69638_webp_original.webp", + "redirectUrl":"thaipbs", + "channel_code":"c12", + "views":2526, + "article_category":[ + "livetv-ca", + "digitaltv-ca", + "freetv-ca", + "news-ca" + ], + "content_type":"livetv", + "content_rights":"", + "isLiveChat":false + }, + { + "id":"vqbr1WgEnGQ", + "title":"Channel 8", + "thumb":"https://cms.dmpcdn.com/livetv/2023/09/15/5408a390-5377-11ee-8e1b-194edbb69638_webp_original.webp", + "redirectUrl":"ch8", + "channel_code":"d62", + "views":8295, + "article_category":[ + "livetv-ca", + "digitaltv-ca", + "entertainment-ca", + "freetv-ca" + ], + "content_type":"livetv", + "content_rights":"", + "isLiveChat":false + }, + { + "id":"9O54lyP5Rqx", + "title":"Channel 7HD", + "thumb":"https://cms.dmpcdn.com/livetv/2023/07/19/212d15e0-25e7-11ee-bfc1-85e95548413c_webp_original.webp", + "redirectUrl":"ch7-hd", + "channel_code":"c07", + "views":12092, + "article_category":[ + "livetv-ca", + "digitaltv-ca", + "entertainment-ca", + "freetv-ca" + ], + "content_type":"livetv", + "content_rights":"", + "isLiveChat":false + }, + { + "id":"zMLBpX7AWmk", + "title":"Nation TV", + "thumb":"https://cms.dmpcdn.com/livetv/2023/09/09/9c6f59c0-4ebe-11ee-99a7-832609069236_webp_original.webp", + "redirectUrl":"nationtv", + "channel_code":"d78", + "views":4733, + "article_category":[ + "livetv-ca", + "digitaltv-ca", + "freetv-ca", + "news-ca" + ], + "content_type":"livetv", + "content_rights":"", + "isLiveChat":false + }, + { + "id":"nQlqONGyoa4", + "title":"Channel 3", + "thumb":"https://cms.dmpcdn.com/livetv/2023/07/24/5ff3e270-29cc-11ee-b2f4-e9de482d866e_webp_original.webp", + "redirectUrl":"ch3-hd", + "channel_code":"c03", + "views":96184, + "article_category":[ + "livetv-ca", + "digitaltv-ca", + "entertainment-ca", + "freetv-ca" + ], + "content_type":"livetv", + "content_rights":"", + "isLiveChat":false + }, + { + "id":"5PKobQk5gLOP", + "title":"Boomerang", + "thumb":"https://cms.dmpcdn.com/livetv/2023/07/05/b74a2460-1b05-11ee-8ce6-b102b53cb4a2_webp_original.webp", + "redirectUrl":"boomerang-hd", + "channel_code":"i007", + "views":707, + "article_category":[ + "livetv-ca", + "freetv-ca", + "kids-ca" + ], + "content_type":"livetv", + "content_rights":"", + "isLiveChat":false + }, + { + "id":"OVKwZle4eop", + "title":"True4U", + "thumb":"https://cms.dmpcdn.com/livetv/2023/09/15/84504210-5377-11ee-aaa1-7d584d8ca7a4_webp_original.webp", + "redirectUrl":"true4u", + "channel_code":"207", + "views":6489, + "article_category":[ + "livetv-ca", + "digitaltv-ca", + "entertainment-ca", + "freetv-ca", + "movies-series-ca" + ], + "content_type":"livetv", + "content_rights":"", + "isLiveChat":false + }, + { + "id":"0z4lvq6Xwoa", + "title":"One31", + "thumb":"https://cms.dmpcdn.com/livetv/2019/01/16/396384be-35dc-4d11-bf04-06c9546ec7bc.png", + "redirectUrl":"one-hd", + "channel_code":"d56", + "views":10182, + "article_category":[ + "livetv-ca", + "digitaltv-ca", + "entertainment-ca", + "freetv-ca" + ], + "content_type":"livetv", + "content_rights":"", + "isLiveChat":false + }, + { + "id":"rBWOx89v9Rk", + "title":"9 MCOT", + "thumb":"https://cms.dmpcdn.com/livetv/2023/09/09/9cc40970-4ebe-11ee-9801-97f95b5eed9a_webp_original.webp", + "redirectUrl":"9mcot-hd", + "channel_code":"c09", + "views":1323, + "article_category":[ + "livetv-ca", + "digitaltv-ca", + "freetv-ca" + ], + "content_type":"livetv", + "content_rights":"", + "isLiveChat":false + } + ] + }, + { + "id":"agbxxnP7GZQ4", + "title":{ + "th":"โปรแกรมทีวียอดนิยม", + "en":"Trending TV Program" + }, + "type":"by_trending_tv_program", + "viewType":"horizontal", + "shelfItems":[ + { + "id":"nQlqONGyoa4", + "title":"แชนแนลทรี ซีรีส์ สายใยรัก เหนือบัลลังก์", + "thumb":"https://cms.dmpcdn.com/livetv/2023/07/24/5ff3e270-29cc-11ee-b2f4-e9de482d866e_webp_original.webp", + "thumbLarge":"https://epgthumb.dmpcdn.com/thumbnail_large/c03.jpg?time=1702312743612", + "redirectUrl":"https://tv.trueid.net/th-en/live/ch3-hd", + "views":96184, + "program_id":"wDGy4q2m963K", + "program_name":"แชนแนลทรี ซีรีส์ สายใยรัก เหนือบัลลังก์", + "article_category":[ + "livetv-ca", + "digitaltv-ca", + "entertainment-ca", + "freetv-ca" + ], + "content_type":"livetv", + "channel_info":{ + "channel_name_cbd":"ប៉ុស្តិ៍ 3 HD", + "channel_name_eng":"CH3 HD", + "channel_name_mm":"CH3 HD", + "channel_name_th":"ช่อง 3 HD" + } + }, + { + "id":"8v732AYomo9", + "title":"ไทยรัฐเจาะประเด็น", + "thumb":"https://cms.dmpcdn.com/livetv/2023/07/18/7dc7a180-2515-11ee-b8b2-77e2a8f4c31e_webp_original.webp", + "thumbLarge":"https://epgthumb.dmpcdn.com/thumbnail_large/d05.jpg?time=1702312743612", + "redirectUrl":"https://tv.trueid.net/th-en/live/thairathtv-hd", + "views":17228, + "program_id":"xkKBjq0VA926", + "program_name":"ไทยรัฐเจาะประเด็น", + "article_category":[ + "livetv-ca", + "digitaltv-ca", + "freetv-ca", + "news-ca" + ], + "content_type":"livetv", + "channel_info":{ + "channel_name_cbd":"ថៃរ៉ាត់ ធីវី HD", + "channel_name_eng":"Thairath TV HD", + "channel_name_mm":"Thairath TV HD", + "channel_name_th":"ไทยรัฐ ทีวี HD" + } + }, + { + "id":"0z4lvq6Xwoa", + "title":"ละคร เสน่หาข้ามเส้น (ตอนอวสาน)", + "thumb":"https://cms.dmpcdn.com/livetv/2019/01/16/396384be-35dc-4d11-bf04-06c9546ec7bc.png", + "thumbLarge":"https://epgthumb.dmpcdn.com/thumbnail_large/d56.jpg?time=1702312743612", + "redirectUrl":"https://tv.trueid.net/th-en/live/one-hd", + "views":10182, + "program_id":"VZ8mMrWEKxQ3", + "program_name":"ละคร เสน่หาข้ามเส้น (ตอนอวสาน)", + "article_category":[ + "livetv-ca", + "digitaltv-ca", + "entertainment-ca", + "freetv-ca" + ], + "content_type":"livetv", + "channel_info":{ + "channel_name_cbd":"វ័ន HD", + "channel_name_eng":"One HD", + "channel_name_mm":"One HD", + "channel_name_th":"วัน HD" + } + }, + { + "id":"OVKwZle4eop", + "title":"ภาพยนตร์ อันธพาล", + "thumb":"https://cms.dmpcdn.com/livetv/2023/09/15/84504210-5377-11ee-aaa1-7d584d8ca7a4_webp_original.webp", + "thumbLarge":"https://epgthumb.dmpcdn.com/thumbnail_large/207.jpg?time=1702312743612", + "redirectUrl":"https://tv.trueid.net/th-en/live/true4u", + "views":6489, + "program_id":"4A8jX4rrX58J", + "program_name":"ภาพยนตร์ อันธพาล", + "article_category":[ + "livetv-ca", + "digitaltv-ca", + "entertainment-ca", + "freetv-ca", + "movies-series-ca" + ], + "content_type":"livetv", + "channel_info":{ + "channel_name_cbd":"ទ្រូ4យូ", + "channel_name_chi":"True4U", + "channel_name_eng":"True4U", + "channel_name_mm":"True4U", + "channel_name_rus":"True4U", + "channel_name_th":"ทรูโฟร์ยู", + "channel_name_vie":"True4U" + } + }, + { + "id":"9O54lyP5Rqx", + "title":"One Lumpinee Heroes", + "thumb":"https://cms.dmpcdn.com/livetv/2023/07/19/212d15e0-25e7-11ee-bfc1-85e95548413c_webp_original.webp", + "thumbLarge":"https://epgthumb.dmpcdn.com/thumbnail_large/c07.jpg?time=1702312743612", + "redirectUrl":"https://tv.trueid.net/th-en/live/ch7-hd", + "views":12092, + "program_id":"vbQqm8mnpyYb", + "program_name":"One Lumpinee Heroes", + "article_category":[ + "livetv-ca", + "digitaltv-ca", + "entertainment-ca", + "freetv-ca" + ], + "content_type":"livetv", + "channel_info":{ + "channel_name_cbd":"ប៉ុស្តិ៍​ 7", + "channel_name_eng":"CH 7HD", + "channel_name_mm":"Channel 7", + "channel_name_th":"ช่อง 7HD" + } + }, + { + "id":"yYk6PvXwXDb", + "title":"เคลียร์ชัดชัด รีรัน", + "thumb":"https://cms.dmpcdn.com/livetv/2023/11/17/2a1de990-852d-11ee-bf98-41acc8fd04fc_webp_original.webp", + "thumbLarge":"https://epgthumb.dmpcdn.com/thumbnail_large/d83.jpg?time=1702312743612", + "redirectUrl":"https://tv.trueid.net/th-en/live/workpointtv", + "views":6075, + "program_id":"KzKlKXKPDkDY", + "program_name":"เคลียร์ชัดชัด รีรัน", + "article_category":[ + "livetv-ca", + "digitaltv-ca", + "entertainment-ca", + "freetv-ca" + ], + "content_type":"livetv", + "channel_info":{ + "channel_name_cbd":"វើកភ័ញ គ្រីអ៊ែតធិវ ធីវី​", + "channel_name_eng":"Workpoint TV", + "channel_name_mm":"Workpoint TV", + "channel_name_th":"เวิร์คพอยท์ ทีวี" + } + }, + { + "id":"OBb6NzoJX7O", + "title":"ทรูช้อปปิ้ง", + "thumb":"https://cms.dmpcdn.com/livetv/2023/10/02/d2ec4b30-60f1-11ee-92a4-8597bcef0049_webp_original.webp", + "thumbLarge":"https://epgthumb.dmpcdn.com/thumbnail_large/da0.jpg?time=1702312743612", + "redirectUrl":"https://tv.trueid.net/th-en/live/amarintv-hd", + "views":6407, + "program_id":"PwP4AzA5KBXw", + "program_name":"ทรูช้อปปิ้ง", + "article_category":[ + "livetv-ca", + "digitaltv-ca", + "freetv-ca" + ], + "content_type":"livetv", + "channel_info":{ + "channel_name_cbd":"អាម៉ារិន", + "channel_name_eng":"Amarin TV", + "channel_name_mm":"Amarin TV", + "channel_name_th":"อมรินทร์" + } + }, + { + "id":"vqbr1WgEnGQ", + "title":"เด็ดมวยเดือด", + "thumb":"https://cms.dmpcdn.com/livetv/2023/09/15/5408a390-5377-11ee-8e1b-194edbb69638_webp_original.webp", + "thumbLarge":"https://epgthumb.dmpcdn.com/thumbnail_large/d62.jpg?time=1702312743612", + "redirectUrl":"https://tv.trueid.net/th-en/live/ch8", + "views":8295, + "program_id":"XJ4NgMgdr7K2", + "program_name":"เด็ดมวยเดือด", + "article_category":[ + "livetv-ca", + "digitaltv-ca", + "entertainment-ca", + "freetv-ca" + ], + "content_type":"livetv", + "channel_info":{ + "channel_name_cbd":"ប៉ុស្តិ៍ 8", + "channel_name_eng":"CH8", + "channel_name_th":"ช่อง 8" + } + }, + { + "id":"zMLBpX7AWmk", + "title":"ยุคลชนข่าว", + "thumb":"https://cms.dmpcdn.com/livetv/2023/09/09/9c6f59c0-4ebe-11ee-99a7-832609069236_webp_original.webp", + "thumbLarge":"https://epgthumb.dmpcdn.com/thumbnail_large/d78.jpg?time=1702312743612", + "redirectUrl":"https://tv.trueid.net/th-en/live/nationtv", + "views":4733, + "program_id":"m6ejRJ5pKvdL", + "program_name":"ยุคลชนข่าว", + "article_category":[ + "livetv-ca", + "digitaltv-ca", + "freetv-ca", + "news-ca" + ], + "content_type":"livetv", + "channel_info":{ + "channel_name_cbd":"Nation TV 22", + "channel_name_chi":"Nation TV 22", + "channel_name_eng":"Nation TV 22", + "channel_name_mm":"Nation TV 22", + "channel_name_rus":"Nation TV 22", + "channel_name_th":"เนชั่น ทีวี", + "channel_name_vie":"Nation TV 22" + } + }, + { + "id":"QNBwOpdaxpQ", + "title":"Highlights Bundesliga", + "thumb":"https://cms.dmpcdn.com/livetv/2023/08/28/012eed00-458a-11ee-bd2b-6734a2d9e428_webp_original.webp", + "thumbLarge":"https://epgthumb.dmpcdn.com/thumbnail_large/da7.jpg?time=1702312743612", + "redirectUrl":"https://tv.trueid.net/th-en/live/pptv-hd", + "views":4723, + "program_id":"DwYXj5Jbvex1", + "program_name":"Highlights Bundesliga", + "article_category":[ + "livetv-ca", + "digitaltv-ca", + "freetv-ca" + ], + "content_type":"livetv", + "channel_info":{ + "channel_name_cbd":"ភីភីធីវី", + "channel_name_eng":"PPTV", + "channel_name_mm":"PPTV", + "channel_name_th":"พีพีทีวี" + } + } + ] + } + ] + }, + "channelDetail":{ + "display_country":"th", + "display_lang":"en", + "id":"NopZ5gjkGmE", + "content_type":"livetv", + "original_id":"279", + "title":"True Movie Hits", + "article_category":[ + "livetv-ca", + "movies-series-ca", + "trueunlock-ca", + "tvsnow", + "movieseries" + ], + "thumb":"https://cms.dmpcdn.com/livetv/2023/04/28/45345d10-e599-11ed-86b8-bb40638e3c49_webp_original.png", + "tags":null, + "status":"publish", + "count_views":538976, + "publish_date":"2020-07-26T17:00:00.000Z", + "create_date":"2017-10-17T22:01:00.000Z", + "update_date":"2023-11-26T11:08:14.333Z", + "searchable":"Y", + "create_by":"Live TV", + "create_by_ssoid":null, + "update_by":"KANT", + "update_by_ssoid":"112710659", + "source_url":null, + "count_likes":null, + "count_ratings":null, + "source_country":null, + "channel_code":"057", + "drm":"WV_FPS", + "channel_info":{ + "channel_name_cbd":"True Movie Hits", + "channel_name_chi":"True Movie Hits", + "channel_name_eng":"True Movie Hits", + "channel_name_mm":"True Movie Hits", + "channel_name_rus":"True Movie Hits", + "channel_name_th":"True Movie Hits", + "channel_name_vie":"True Movie Hits" + }, + "lang_dual":"yes", + "setting":null, + "slug":"true-movie-hits", + "allow_app":[ + "trueidapp", + "trueidweb", + "trueidott", + "hybrid" + ], + "detail":"

      ช่องภาพยนต์ต่างประเทศ รับชมได้ทั้งครบครัวด้วยระบบเสียงภาษาไทย

      ", + "content_provider":"true_vision", + "playready":"", + "score":null + }, + "liveChatConfig":{ + "channelId":"NopZ5gjkGmE", + "isLiveChat":false, + "slug":"true-movie-hits", + "disabledChat":false, + "supportBrowser":{ + "chrome_browser_version":{ + "min_version":83, + "live_chat":false + }, + "firefox_browser_version":{ + "min_version":92, + "live_chat":false + }, + "msedge_browser_version":{ + "min_version":80, + "live_chat":false + }, + "off_livechat":false + }, + "disabledChatList":[ + + ] + }, + "epgList":[ + { + "id":"w2xyzKz9eyb3", + "original_id":"057:20231212_020500", + "content_type":"epg", + "title":"The Last Witch Hunter", + "detail":"", + "status":"publish", + "channel_code":"057", + "title_id":"715307", + "ep_id":"2397606", + "ep_no":"1", + "ep_name":"LAST WITCH HUNTER, THE (2015) [MHS] [R]", + "movie_type":"series", + "first_run":"Y", + "cast_type":"tape", + "start_date":"2023-12-11T19:05:00.000Z", + "end_date":"2023-12-11T20:55:00.000Z", + "publish_date":"2023-12-11T10:36:41.801Z", + "lang":"en", + "thumb":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_020500.jpg", + "thumb_list":{ + "thumb":"https://epgthumb.dmpcdn.com/thumbnail/057/20231212/20231212_020500.jpg", + "thumb_catchup":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_021000.jpg", + "thumb_large":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_020500.jpg" + }, + "black_out":0, + "catch_up":0, + "flag":"N", + "info":{ + "channel_name":"TR MOVIE HITS", + "image":"https://bms.dmpcdn.com/uploads/pic/381f853da5f4a310bf248357fed21a57.jpg", + "synopsis_en":"A young man is all that stands between humanity and the most horrifying witches in history.", + "director":"Breck Eisner", + "title_id":"715307", + "video":"pBNufkr4KkU", + "channel_code":"057", + "type":"Movie", + "synopsis_th":"หนุ่มนักล่าแม่มดถูกสาปให้เป็นอมตะจนกระทั่งราชินีแม่มดได้ฟื้นคืนชีพขึ้นมาจึงมีเพียงเขาคนเดียวเท่านั้นที่จะสามารถกอบกู้มวลมนุษยชาติได้", + "imdb_image":"https://bms.dmpcdn.com/uploads/pic/44b0eb2f46eed6a3953014cb5abdbff3.jpg", + "cast":"Vin Diesel, Rose Leslie, Elijah Wood", + "genres":"action", + "program_title":"LAST WITCH HUNTER, THE", + "production_year":"2015" + }, + "isShowTime":"02:05", + "isActive":false + }, + { + "id":"GPApa0aZzprE", + "original_id":"057:20231212_035500", + "content_type":"epg", + "title":"Point Break", + "detail":"", + "status":"publish", + "channel_code":"057", + "title_id":"718258", + "ep_id":"2413906", + "ep_no":"1", + "ep_name":"POINT BREAK (2015) [MHS] [R]", + "movie_type":"series", + "first_run":"Y", + "cast_type":"tape", + "start_date":"2023-12-11T20:55:00.000Z", + "end_date":"2023-12-11T22:55:00.000Z", + "publish_date":"2023-12-11T10:36:41.801Z", + "lang":"en", + "thumb":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_035500.jpg", + "thumb_list":{ + "thumb":"https://epgthumb.dmpcdn.com/thumbnail/057/20231212/20231212_035500.jpg", + "thumb_catchup":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_040000.jpg", + "thumb_large":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_035500.jpg" + }, + "black_out":0, + "catch_up":0, + "flag":"N", + "info":{ + "channel_name":"TR MOVIE HITS", + "image":"https://bms.dmpcdn.com/uploads/pic/e143a6f05ce8e87bf3e7c0f8dfca9914.jpeg", + "synopsis_en":"An FBI agent infiltrates a gang of thrill-seeking athlete thieves who are suspects in a spate of daring robberies.", + "director":"Ericson Core", + "title_id":"718258", + "video":"jcDD2-s4vWA", + "channel_code":"057", + "type":"Movie", + "synopsis_th":"เรื่องราวของเจ้าหน้าที่เอฟบีไอกับปฏิบัติการสืบสวนเพื่อตามล่าตัวมิจฉาชีพระดับโลกด้วยการแฝงตัวเข้าไปในกลุ่มนักเล่นกระดานโต้คลื่น", + "imdb_image":"https://bms.dmpcdn.com/uploads/pic/5a0fc22d59c8aef7e9693119687b2172.jpg", + "cast":"Edgar Ramirez, Luke Bracey, Ray Winstone", + "genres":"action", + "program_title":"POINT BREAK", + "production_year":"2015" + }, + "isShowTime":"03:55", + "isActive":false + }, + { + "id":"Va15bqbQn58a", + "original_id":"057:20231212_055500", + "content_type":"epg", + "title":"The Art of War", + "detail":"", + "status":"publish", + "channel_code":"057", + "title_id":"712097", + "ep_id":"2372786", + "ep_no":"1", + "ep_name":"ART OF WAR, THE [2000] [MHS]", + "movie_type":"series", + "first_run":"Y", + "cast_type":"tape", + "start_date":"2023-12-11T22:55:00.000Z", + "end_date":"2023-12-12T00:55:00.000Z", + "publish_date":"2023-12-11T10:36:41.801Z", + "lang":"en", + "thumb":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_055500.jpg", + "thumb_list":{ + "thumb":"https://epgthumb.dmpcdn.com/thumbnail/057/20231212/20231212_055500.jpg", + "thumb_catchup":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_060000.jpg", + "thumb_large":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_055500.jpg" + }, + "black_out":0, + "catch_up":0, + "flag":"N", + "info":{ + "channel_name":"TR MOVIE HITS", + "image":"https://bms.dmpcdn.com/uploads/pic/4f12f9d90f2e8d29b9e427ef415bcb4e.jpg", + "synopsis_en":"After a US agent is framed for the assassination of the Chinese ambassador, he faces a race against time to catch the real killers.", + "director":"Christian Duguay", + "title_id":"712097", + "video":"rKFmSpB-uGQ", + "channel_code":"057", + "type":"Movie", + "synopsis_th":"เมื่อสายลับถูกใส่ร้ายว่าเป็นฆาตกรเขาจึงต้องหลบหนีการตามไล่ล่าและแข่งกับเวลาเพื่อสืบหาตามล่าฆาตกรตัวจริงให้ได้โดยเร็วที่สุด", + "imdb_image":"https://bms.dmpcdn.com/uploads/pic/28ab499dffdff191fba497f64131e744.jpg", + "cast":"Wesley Snipes, Anne Archer, Maury Chaykin", + "genres":"crime", + "program_title":"ART OF WAR, THE", + "production_year":"2000" + }, + "isShowTime":"05:55", + "isActive":false + }, + { + "id":"ALxXy6y32XOL", + "original_id":"057:20231212_075500", + "content_type":"epg", + "title":"The Marksman", + "detail":"", + "status":"publish", + "channel_code":"057", + "title_id":"726358", + "ep_id":"2466252", + "ep_no":"1", + "ep_name":"MARKSMAN, THE (2021) [MHS]", + "movie_type":"series", + "first_run":"Y", + "cast_type":"tape", + "start_date":"2023-12-12T00:55:00.000Z", + "end_date":"2023-12-12T02:50:00.000Z", + "publish_date":"2023-12-11T10:36:41.801Z", + "lang":"en", + "thumb":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_075500.jpg", + "thumb_list":{ + "thumb":"https://epgthumb.dmpcdn.com/thumbnail/057/20231212/20231212_075500.jpg", + "thumb_catchup":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_080000.jpg", + "thumb_large":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_075500.jpg" + }, + "black_out":0, + "catch_up":0, + "flag":"N", + "info":{ + "channel_name":"TR MOVIE HITS", + "image":"https://bms.dmpcdn.com/uploads/pic/3945dc2c6cfff5ebeb61f021b58104ab.jpg", + "synopsis_en":"An Arizona rancher becomes the unlikely defender of a Mexican boy desperately fleeing the cartel assassins who've pursued him into the US.", + "director":"Robert Lorenz", + "title_id":"726358", + "video":"lEBPNi4bEbc", + "channel_code":"057", + "type":"Movie", + "synopsis_th":"อดีตทหารเรือ ที่หนีความวุ่นวายมาใช้ชีวิตอย่างสงบสุขในฟาร์มนอกเมือง แต่กลับต้องไปพัวพันกับสองแม่ลูกที่หลบหนีเอาตัวรอดจากกลุ่มนักฆ่าค้ายา", + "imdb_image":"https://bms.dmpcdn.com/uploads/pic/573349fd5394caccce8c4f818fdb57b5.jpg", + "cast":"Liam Neeson, Katheryn Winnick, Juan Pablo Raba", + "genres":"action", + "program_title":"MARKSMAN, THE", + "production_year":"2021" + }, + "isShowTime":"07:55", + "isActive":false + }, + { + "id":"XzDNDnDrXNJ9", + "original_id":"057:20231212_095000", + "content_type":"epg", + "title":"Gods of Egypt", + "detail":"", + "status":"publish", + "channel_code":"057", + "title_id":"718274", + "ep_id":"2414078", + "ep_no":"1", + "ep_name":"GODS OF EGYPT (2016) [MHS] [R]", + "movie_type":"series", + "first_run":"Y", + "cast_type":"tape", + "start_date":"2023-12-12T02:50:00.000Z", + "end_date":"2023-12-12T05:05:00.000Z", + "publish_date":"2023-12-11T10:36:41.801Z", + "lang":"en", + "thumb":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_095000.jpg", + "thumb_list":{ + "thumb":"https://epgthumb.dmpcdn.com/thumbnail/057/20231212/20231212_095000.jpg", + "thumb_catchup":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_095500.jpg", + "thumb_large":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_095000.jpg" + }, + "black_out":0, + "catch_up":0, + "flag":"N", + "info":{ + "channel_name":"TR MOVIE HITS", + "image":"https://bms.dmpcdn.com/uploads/pic/e772028844f60526e6dc0fe5b666425a.jpg", + "synopsis_en":"A young hero teams with the god Horus to fight against the god of darkness, who has usurped Egypt's throne.", + "director":"Alex Proyas", + "title_id":"718274", + "video":"Oijdb-a9GKY", + "channel_code":"057", + "type":"Movie", + "synopsis_th":"เรื่องราวความขัดแย้งและการช่วงชิงที่อุบัติขึ้นท่ามกลางความร้อนระอุแห่งทะเลทรายในดินแดนลุ่มแม่น้ำไนล์อันเต็มไปด้วยมนตรา ทวยเทพ และ เหล่าอสูร", + "imdb_image":"https://bms.dmpcdn.com/uploads/pic/0a945dc3853317779946f5b9f38269a1.jpg", + "cast":"Gerard Butler, Brenton Thwaites, Nikolaj Coster-Waldau", + "genres":"action", + "program_title":"GODS OF EGYPT", + "production_year":"2016" + }, + "isShowTime":"09:50", + "isActive":false + }, + { + "id":"4DnvNENgamwD", + "original_id":"057:20231212_120500", + "content_type":"epg", + "title":"The Last Witch Hunter", + "detail":"", + "status":"publish", + "channel_code":"057", + "title_id":"715307", + "ep_id":"2397606", + "ep_no":"1", + "ep_name":"LAST WITCH HUNTER, THE (2015) [MHS] [R]", + "movie_type":"series", + "first_run":"Y", + "cast_type":"tape", + "start_date":"2023-12-12T05:05:00.000Z", + "end_date":"2023-12-12T06:55:00.000Z", + "publish_date":"2023-12-11T10:36:41.801Z", + "lang":"en", + "thumb":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_120500.jpg", + "thumb_list":{ + "thumb":"https://epgthumb.dmpcdn.com/thumbnail/057/20231212/20231212_120500.jpg", + "thumb_catchup":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_121000.jpg", + "thumb_large":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_120500.jpg" + }, + "black_out":0, + "catch_up":0, + "flag":"N", + "info":{ + "channel_name":"TR MOVIE HITS", + "image":"https://bms.dmpcdn.com/uploads/pic/381f853da5f4a310bf248357fed21a57.jpg", + "synopsis_en":"A young man is all that stands between humanity and the most horrifying witches in history.", + "director":"Breck Eisner", + "title_id":"715307", + "video":"pBNufkr4KkU", + "channel_code":"057", + "type":"Movie", + "synopsis_th":"หนุ่มนักล่าแม่มดถูกสาปให้เป็นอมตะจนกระทั่งราชินีแม่มดได้ฟื้นคืนชีพขึ้นมาจึงมีเพียงเขาคนเดียวเท่านั้นที่จะสามารถกอบกู้มวลมนุษยชาติได้", + "imdb_image":"https://bms.dmpcdn.com/uploads/pic/44b0eb2f46eed6a3953014cb5abdbff3.jpg", + "cast":"Vin Diesel, Rose Leslie, Elijah Wood", + "genres":"action", + "program_title":"LAST WITCH HUNTER, THE", + "production_year":"2015" + }, + "isShowTime":"12:05", + "isActive":false + }, + { + "id":"AEMb1g1LnzpQ", + "original_id":"057:20231212_135500", + "content_type":"epg", + "title":"Leon", + "detail":"", + "status":"publish", + "channel_code":"057", + "title_id":"729656", + "ep_id":"2488591", + "ep_no":"1", + "ep_name":"LEON [1994] [MHS]", + "movie_type":"series", + "first_run":"Y", + "cast_type":"tape", + "start_date":"2023-12-12T06:55:00.000Z", + "end_date":"2023-12-12T09:05:00.000Z", + "publish_date":"2023-12-11T10:36:41.801Z", + "lang":"en", + "thumb":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_135500.jpg", + "thumb_list":{ + "thumb":"https://epgthumb.dmpcdn.com/thumbnail/057/20231212/20231212_135500.jpg", + "thumb_catchup":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_140000.jpg", + "thumb_large":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_135500.jpg" + }, + "black_out":0, + "catch_up":0, + "flag":"N", + "info":{ + "channel_name":"TR MOVIE HITS", + "image":"https://bms.dmpcdn.com/uploads/pic/79eabd21fdb1da338cca6b598de46cde.jpg", + "synopsis_en":"A hitman forms an unlikely bond with a young girl, teaching her his deadly skills while protecting her from ruthless criminals.", + "director":"Luc Besson", + "title_id":"729656", + "video":"aNQqoExfQsg", + "channel_code":"057", + "type":"Movie", + "synopsis_th":"เรื่องราวของนักฆ่าที่ได้สร้างความผูกพันธ์ที่ไม่น่าจะเป็นไปได้กับเด็กหญิง โดยสอนทักษะอันอันตรายแก่เธอพร้อมทั้งปกป้องเธอจากอาชญากรผู้โหดเหี้ยม", + "imdb_image":"https://bms.dmpcdn.com/uploads/pic/20be5d12ff2b8f86fb40f9db619d4cb8.jpg", + "cast":"Jean Reno, Gary Oldman, Natalie Portman", + "genres":"crime", + "program_title":"LEON", + "production_year":"1994" + }, + "isShowTime":"13:55", + "isActive":false + }, + { + "id":"2xe7R1Rgamq4", + "original_id":"057:20231212_160500", + "content_type":"epg", + "title":"Gunpowder Milkshake", + "detail":"", + "status":"publish", + "channel_code":"057", + "title_id":"710376", + "ep_id":"2363208", + "ep_no":"1", + "ep_name":"GUNPOWDER MILKSHAKE (2021) [MHS]", + "movie_type":"series", + "first_run":"Y", + "cast_type":"tape", + "start_date":"2023-12-12T09:05:00.000Z", + "end_date":"2023-12-12T11:00:00.000Z", + "publish_date":"2023-12-11T10:36:41.801Z", + "lang":"en", + "thumb":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_160500.jpg", + "thumb_list":{ + "thumb":"https://epgthumb.dmpcdn.com/thumbnail/057/20231212/20231212_160500.jpg", + "thumb_catchup":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_161000.jpg", + "thumb_large":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_160500.jpg" + }, + "black_out":0, + "catch_up":0, + "flag":"N", + "info":{ + "channel_name":"TR MOVIE HITS", + "image":"https://bms.dmpcdn.com/uploads/pic/0039112f2fef876ebf32f9bfb3a9fcf9.jpg", + "synopsis_en":"Three generations of women fight back against those who aim to take everything from them.", + "director":"Navot Papushado", + "title_id":"710376", + "video":"yxuAroBqt2c", + "channel_code":"057", + "type":"Movie", + "synopsis_th":"เรื่องราวของสามหญิงสามวัยที่ต้องต่อสู้กับผู้ซึ่งแย่งชิงทุกสิ่งทุกอย่างไปจากพวกเธอ", + "imdb_image":"https://bms.dmpcdn.com/uploads/pic/756c225bc8f5f2ed1268945c979b01a1.jpg", + "cast":"Karen Gillan, Lena Headey, Carla Gugino", + "genres":"action", + "program_title":"GUNPOWDER MILKSHAKE", + "production_year":"2021" + }, + "isShowTime":"16:05", + "isActive":false + }, + { + "id":"QoeyO1O0Q3no", + "original_id":"057:20231212_180000", + "content_type":"epg", + "title":"Point Break", + "detail":"", + "status":"publish", + "channel_code":"057", + "title_id":"718258", + "ep_id":"2413906", + "ep_no":"1", + "ep_name":"POINT BREAK (2015) [MHS] [R]", + "movie_type":"series", + "first_run":"Y", + "cast_type":"tape", + "start_date":"2023-12-12T11:00:00.000Z", + "end_date":"2023-12-12T13:00:00.000Z", + "publish_date":"2023-12-11T10:36:41.801Z", + "lang":"en", + "thumb":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_180000.jpg", + "thumb_list":{ + "thumb":"https://epgthumb.dmpcdn.com/thumbnail/057/20231212/20231212_180000.jpg", + "thumb_catchup":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_180500.jpg", + "thumb_large":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_180000.jpg" + }, + "black_out":0, + "catch_up":0, + "flag":"N", + "info":{ + "channel_name":"TR MOVIE HITS", + "image":"https://bms.dmpcdn.com/uploads/pic/e143a6f05ce8e87bf3e7c0f8dfca9914.jpeg", + "synopsis_en":"An FBI agent infiltrates a gang of thrill-seeking athlete thieves who are suspects in a spate of daring robberies.", + "director":"Ericson Core", + "title_id":"718258", + "video":"jcDD2-s4vWA", + "channel_code":"057", + "type":"Movie", + "synopsis_th":"เรื่องราวของเจ้าหน้าที่เอฟบีไอกับปฏิบัติการสืบสวนเพื่อตามล่าตัวมิจฉาชีพระดับโลกด้วยการแฝงตัวเข้าไปในกลุ่มนักเล่นกระดานโต้คลื่น", + "imdb_image":"https://bms.dmpcdn.com/uploads/pic/5a0fc22d59c8aef7e9693119687b2172.jpg", + "cast":"Edgar Ramirez, Luke Bracey, Ray Winstone", + "genres":"action", + "program_title":"POINT BREAK", + "production_year":"2015" + }, + "isShowTime":"18:00", + "isActive":false + }, + { + "id":"voAargrBvRQo", + "original_id":"057:20231212_200000", + "content_type":"epg", + "title":"Max Steel", + "detail":"", + "status":"publish", + "channel_code":"057", + "title_id":"718257", + "ep_id":"2413902", + "ep_no":"1", + "ep_name":"MAX STEEL (2016) [MHS] [R]", + "movie_type":"series", + "first_run":"Y", + "cast_type":"tape", + "start_date":"2023-12-12T13:00:00.000Z", + "end_date":"2023-12-12T14:35:00.000Z", + "publish_date":"2023-12-11T10:36:41.801Z", + "lang":"en", + "thumb":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_200000.jpg", + "thumb_list":{ + "thumb":"https://epgthumb.dmpcdn.com/thumbnail/057/20231212/20231212_200000.jpg", + "thumb_catchup":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_200500.jpg", + "thumb_large":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_200000.jpg" + }, + "black_out":0, + "catch_up":0, + "flag":"N", + "info":{ + "channel_name":"TR MOVIE HITS", + "image":"https://bms.dmpcdn.com/uploads/pic/a10785bc40cd82e82ae702d8a7827393.jpg", + "synopsis_en":"A young teen and an alien companion harness and combine their tremendous new powers to evolve into the turbo-charged superhero Max Steel.", + "director":"Stewart Hendler", + "title_id":"718257", + "video":"Tf4sa0BVJVw", + "channel_code":"057", + "type":"Movie", + "synopsis_th":"ชายหนุ่มที่ชีวิตต้องแปรเปลี่ยนไปตลอดกาลจากอุบัติเหตุภายในห้องทดลองซึ่งทำให้เขากลายเป็นยอดมนุษย์แกร่ง", + "imdb_image":"https://bms.dmpcdn.com/uploads/pic/c8e9e6d49546fbde72d0f0b552db97a6.jpg", + "cast":"Ben Winchell, Josh Brener, Maria Bello", + "genres":"action", + "program_title":"MAX STEEL", + "production_year":"2016" + }, + "isShowTime":"20:00", + "isActive":false + }, + { + "id":"Ena7xBxkNK3z", + "original_id":"057:20231212_213500", + "content_type":"epg", + "title":"Pompeii", + "detail":"", + "status":"publish", + "channel_code":"057", + "title_id":"715311", + "ep_id":"2397620", + "ep_no":"1", + "ep_name":"POMPEII (2014) [MHS] [R]", + "movie_type":"series", + "first_run":"Y", + "cast_type":"tape", + "start_date":"2023-12-12T14:35:00.000Z", + "end_date":"2023-12-12T16:20:00.000Z", + "publish_date":"2023-12-11T10:36:41.801Z", + "lang":"en", + "thumb":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_213500.jpg", + "thumb_list":{ + "thumb":"https://epgthumb.dmpcdn.com/thumbnail/057/20231212/20231212_213500.jpg", + "thumb_catchup":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_214000.jpg", + "thumb_large":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_213500.jpg" + }, + "black_out":0, + "catch_up":0, + "flag":"N", + "info":{ + "channel_name":"TR MOVIE HITS", + "image":"https://bms.dmpcdn.com/uploads/pic/28b7c486d1d8c52060413fb58c869c76.jpg", + "synopsis_en":"Just before the fateful eruption of Mt Vesuvius, a gladiator must save the love of his life from a corrupt Roman.", + "director":"Paul Anderson", + "title_id":"715311", + "video":"t6TRwfxDICM", + "channel_code":"057", + "type":"Movie", + "synopsis_th":"เรื่องราวของหนุ่มนักรบซึ่งเสี่ยงชีพช่วยเหลือหญิงสาวผู้เป็นที่รักจากมหาวิบัติกัมปนาทครั้งใหญ่แห่งประวัติศาสตร์เมื่อภูเขาไฟวิซูเวียสเกิดปะทุขึ้น", + "imdb_image":"https://bms.dmpcdn.com/uploads/pic/8675b7f9a08f3f0587bed52c7a8015e1.jpg", + "cast":"Kit Harington, Emily Browning, Kiefer Sutherland", + "genres":"action", + "program_title":"POMPEII", + "production_year":"2014" + }, + "isShowTime":"21:35", + "isActive":false + }, + { + "id":"WNxrPpPwwkQl", + "original_id":"057:20231212_232000", + "content_type":"epg", + "title":"Leon", + "detail":"", + "status":"publish", + "channel_code":"057", + "title_id":"729656", + "ep_id":"2488591", + "ep_no":"1", + "ep_name":"LEON [1994] [MHS]", + "movie_type":"series", + "first_run":"Y", + "cast_type":"tape", + "start_date":"2023-12-12T16:20:00.000Z", + "end_date":"2023-12-12T18:30:00.000Z", + "publish_date":"2023-12-11T10:36:41.801Z", + "lang":"en", + "thumb":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_232000.jpg", + "thumb_list":{ + "thumb":"https://epgthumb.dmpcdn.com/thumbnail/057/20231212/20231212_232000.jpg", + "thumb_catchup":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_232500.jpg", + "thumb_large":"https://epgthumb.dmpcdn.com/thumbnail_large/057/20231212/20231212_232000.jpg" + }, + "black_out":0, + "catch_up":0, + "flag":"N", + "info":{ + "channel_name":"TR MOVIE HITS", + "image":"https://bms.dmpcdn.com/uploads/pic/79eabd21fdb1da338cca6b598de46cde.jpg", + "synopsis_en":"A hitman forms an unlikely bond with a young girl, teaching her his deadly skills while protecting her from ruthless criminals.", + "director":"Luc Besson", + "title_id":"729656", + "video":"aNQqoExfQsg", + "channel_code":"057", + "type":"Movie", + "synopsis_th":"เรื่องราวของนักฆ่าที่ได้สร้างความผูกพันธ์ที่ไม่น่าจะเป็นไปได้กับเด็กหญิง โดยสอนทักษะอันอันตรายแก่เธอพร้อมทั้งปกป้องเธอจากอาชญากรผู้โหดเหี้ยม", + "imdb_image":"https://bms.dmpcdn.com/uploads/pic/20be5d12ff2b8f86fb40f9db619d4cb8.jpg", + "cast":"Jean Reno, Gary Oldman, Natalie Portman", + "genres":"crime", + "program_title":"LEON", + "production_year":"1994" + }, + "isShowTime":"23:20", + "isActive":false + } + ], + "audioData":{ + "lang_locale":"", + "voice_commentary":"" + }, + "playerLanguage":{ + "data":{ + "aa":"Afar", + "ab":"Abkhazian", + "af":"Afrikaans", + "ak":"Akan", + "am":"Amharic", + "ar":"Arabic", + "an":"Aragonese", + "as":"Assamese", + "av":"Avaric", + "ae":"Avestan", + "ay":"Aymara", + "az":"Azerbaijani", + "ba":"Bashkir", + "bm":"Bambara", + "be":"Belarusian", + "bn":"Bengali", + "bh":"Biharilanguages", + "bi":"Bislama", + "bs":"Bosnian", + "br":"Breton", + "bg":"Bulgarian", + "ca":"CatalanValencian", + "ch":"Chamorro", + "ce":"Chechen", + "cu":"ChurchSlavicOldSlavonicChurchSlavonicOldBulgarianOldChurchSlavonic", + "cv":"Chuvash", + "kw":"Cornish", + "co":"Corsican", + "cr":"Cree", + "cs":"Czech", + "da":"Danish", + "dv":"DivehiDhivehiMaldivian", + "dz":"Dzongkha", + "en":"English", + "eo":"Esperanto", + "et":"Estonian", + "eu":"Basque", + "ee":"Ewe", + "fo":"Faroese", + "fj":"Fijian", + "fi":"Finnish", + "fr":"French", + "fy":"WesternFrisian", + "ff":"Fulah", + "de":"German", + "gd":"GaelicScottishGaelic", + "ga":"Irish", + "gl":"Galician", + "gv":"Manx", + "el":"Greek,Modern(1453-)", + "gn":"Guarani", + "gu":"Gujarati", + "ht":"HaitianHaitianCreole", + "ha":"Hausa", + "he":"Hebrew", + "hz":"Herero", + "hi":"Hindi", + "ho":"HiriMotu", + "hr":"Croatian", + "hu":"Hungarian", + "hy":"Armenian", + "ig":"Igbo", + "io":"Ido", + "ii":"SichuanYiNuosu", + "iu":"Inuktitut", + "ie":"InterlingueOccidental", + "ia":"Interlingua(InternationalAuxiliaryLanguageAssociation)", + "id":"Indonesian", + "ik":"Inupiaq", + "is":"Icelandic", + "it":"Italian", + "jv":"Javanese", + "ja":"Japanese", + "kl":"KalaallisutGreenlandic", + "kn":"Kannada", + "ks":"Kashmiri", + "ka":"Georgian", + "kr":"Kanuri", + "kk":"Kazakh", + "km":"CentralKhmer", + "ki":"KikuyuGikuyu", + "rw":"Kinyarwanda", + "ky":"KirghizKyrgyz", + "kv":"Komi", + "kg":"Kongo", + "ko":"Korean", + "kj":"KuanyamaKwanyama", + "ku":"Kurdish", + "lo":"Lao", + "la":"Latin", + "lv":"Latvian", + "li":"LimburganLimburgerLimburgish", + "ln":"Lingala", + "lt":"Lithuanian", + "lb":"LuxembourgishLetzeburgesch", + "lu":"Luba-Katanga", + "lg":"Ganda", + "mh":"Marshallese", + "ml":"Malayalam", + "mr":"Marathi", + "mk":"Macedonian", + "mg":"Malagasy", + "mt":"Maltese", + "mn":"Mongolian", + "mi":"Maori", + "ms":"Malay", + "my":"Burmese", + "na":"Nauru", + "nv":"NavajoNavaho", + "nr":"Ndebele,SouthSouthNdebele", + "nd":"Ndebele,NorthNorthNdebele", + "ng":"Ndonga", + "ne":"Nepali", + "nl":"DutchFlemish", + "nn":"NorwegianNynorskNynorsk,Norwegian", + "nb":"Bokmål,NorwegianNorwegianBokmål", + "no":"Norwegian", + "ny":"ChichewaChewaNyanja", + "oc":"Occitan(post1500)", + "oj":"Ojibwa", + "or":"Oriya", + "om":"Oromo", + "os":"OssetianOssetic", + "pa":"PanjabiPunjabi", + "fa":"Persian", + "pi":"Pali", + "pl":"Polish", + "pt":"Portuguese", + "ps":"PushtoPashto", + "qu":"Quechua", + "rm":"Romansh", + "ro":"RomanianMoldavianMoldovan", + "rn":"Rundi", + "ru":"Russian", + "sg":"Sango", + "sa":"Sanskrit", + "si":"SinhalaSinhalese", + "sk":"Slovak", + "sl":"Slovenian", + "se":"NorthernSami", + "sm":"Samoan", + "sn":"Shona", + "sd":"Sindhi", + "so":"Somali", + "st":"Sotho,Southern", + "es":"SpanishCastilian", + "sq":"Albanian", + "sc":"Sardinian", + "sr":"Serbian", + "ss":"Swati", + "su":"Sundanese", + "sw":"Swahili", + "sv":"Swedish", + "ty":"Tahitian", + "ta":"Tamil", + "tt":"Tatar", + "te":"Telugu", + "tg":"Tajik", + "tl":"Tagalog", + "th":"Thai", + "bo":"Tibetan", + "ti":"Tigrinya", + "to":"Tonga(TongaIslands)", + "tn":"Tswana", + "ts":"Tsonga", + "tk":"Turkmen", + "tr":"Turkish", + "tw":"Twi", + "ug":"UighurUyghur", + "uk":"Ukrainian", + "ur":"Urdu", + "uz":"Uzbek", + "ve":"Venda", + "vi":"Vietnamese", + "vo":"Volapük", + "cy":"Welsh", + "wa":"Walloon", + "wo":"Wolof", + "xh":"Xhosa", + "yi":"Yiddish", + "yo":"Yoruba", + "za":"ZhuangChuang", + "zh":"Chinese", + "zu":"Zulu", + "aar":"Afar", + "abk":"Abkhazian", + "ace":"Achinese", + "ach":"Acoli", + "ada":"Adangme", + "ady":"AdygheAdygei", + "afa":"Afro-Asiaticlanguages", + "afh":"Afrihili", + "afr":"Afrikaans", + "ain":"Ainu", + "aka":"Akan", + "akk":"Akkadian", + "ale":"Aleut", + "alg":"Algonquianlanguages", + "alt":"SouthernAltai", + "amh":"Amharic", + "ang":"English,Old(ca.450-1100)", + "anp":"Angika", + "apa":"Apachelanguages", + "ara":"Arabic", + "arc":"OfficialAramaic(700-300BCE)ImperialAramaic(700-300BCE)", + "arg":"Aragonese", + "arn":"MapudungunMapuche", + "arp":"Arapaho", + "art":"Artificiallanguages", + "arw":"Arawak", + "asm":"Assamese", + "ast":"AsturianBableLeoneseAsturleonese", + "ath":"Athapascanlanguages", + "aus":"Australianlanguages", + "ava":"Avaric", + "ave":"Avestan", + "awa":"Awadhi", + "aym":"Aymara", + "aze":"Azerbaijani", + "bad":"Bandalanguages", + "bai":"Bamilekelanguages", + "bak":"Bashkir", + "bal":"Baluchi", + "bam":"Bambara", + "ban":"Balinese", + "bas":"Basa", + "bat":"Balticlanguages", + "bej":"BejaBedawiyet", + "bel":"Belarusian", + "bem":"Bemba", + "ben":"Bengali", + "ber":"Berberlanguages", + "bho":"Bhojpuri", + "bih":"Biharilanguages", + "bik":"Bikol", + "bin":"BiniEdo", + "bis":"Bislama", + "bla":"Siksika", + "bnt":"Bantulanguages", + "bos":"Bosnian", + "bra":"Braj", + "bre":"Breton", + "btk":"Bataklanguages", + "bua":"Buriat", + "bug":"Buginese", + "bul":"Bulgarian", + "bur(B)mya(T)":"Burmese", + "byn":"BlinBilin", + "cad":"Caddo", + "cai":"CentralAmericanIndianlanguages", + "car":"GalibiCarib", + "cat":"CatalanValencian", + "cau":"Caucasianlanguages", + "ceb":"Cebuano", + "cel":"Celticlanguages", + "cha":"Chamorro", + "chb":"Chibcha", + "che":"Chechen", + "chg":"Chagatai", + "chk":"Chuukese", + "chm":"Mari", + "chn":"Chinookjargon", + "cho":"Choctaw", + "chp":"ChipewyanDeneSuline", + "chr":"Cherokee", + "chu":"ChurchSlavicOldSlavonicChurchSlavonicOldBulgarianOldChurchSlavonic", + "chv":"Chuvash", + "chy":"Cheyenne", + "cmc":"Chamiclanguages", + "cnr":"Montenegrin", + "cop":"Coptic", + "cor":"Cornish", + "cos":"Corsican", + "cpe":"Creolesandpidgins,Englishbased", + "cpf":"Creolesandpidgins,French-based", + "cpp":"Creolesandpidgins,Portuguese-based", + "cre":"Cree", + "crh":"CrimeanTatarCrimeanTurkish", + "crp":"Creolesandpidgins", + "csb":"Kashubian", + "cus":"Cushiticlanguages", + "cze(B)ces(T)":"Czech", + "dak":"Dakota", + "dan":"Danish", + "dar":"Dargwa", + "day":"LandDayaklanguages", + "del":"Delaware", + "den":"Slave(Athapascan)", + "dgr":"Dogrib", + "din":"Dinka", + "div":"DivehiDhivehiMaldivian", + "doi":"Dogri", + "dra":"Dravidianlanguages", + "dsb":"LowerSorbian", + "dua":"Duala", + "dum":"Dutch,Middle(ca.1050-1350)", + "dyu":"Dyula", + "dzo":"Dzongkha", + "efi":"Efik", + "egy":"Egyptian(Ancient)", + "eka":"Ekajuk", + "elx":"Elamite", + "eng":"English", + "enm":"English,Middle(1100-1500)", + "epo":"Esperanto", + "est":"Estonian", + "baq(B)eus(T)":"Basque", + "ewo":"Ewondo", + "fan":"Fang", + "fao":"Faroese", + "fat":"Fanti", + "fij":"Fijian", + "fil":"FilipinoPilipino", + "fin":"Finnish", + "fiu":"Finno-Ugrianlanguages", + "fre(B)fra(T)":"French", + "frm":"French,Middle(ca.1400-1600)", + "fro":"French,Old(842-ca.1400)", + "frr":"NorthernFrisian", + "frs":"EasternFrisian", + "fry":"WesternFrisian", + "ful":"Fulah", + "fur":"Friulian", + "gaa":"Ga", + "gay":"Gayo", + "gba":"Gbaya", + "gem":"Germaniclanguages", + "ger(B)deu(T)":"German", + "gez":"Geez", + "gil":"Gilbertese", + "gla":"GaelicScottishGaelic", + "gle":"Irish", + "glg":"Galician", + "glv":"Manx", + "gmh":"German,MiddleHigh(ca.1050-1500)", + "goh":"German,OldHigh(ca.750-1050)", + "gon":"Gondi", + "gor":"Gorontalo", + "got":"Gothic", + "grb":"Grebo", + "grc":"Greek,Ancient(to1453)", + "gre(B)ell(T)":"Greek,Modern(1453-)", + "grn":"Guarani", + "gsw":"SwissGermanAlemannicAlsatian", + "guj":"Gujarati", + "gwi":"Gwich'in", + "hai":"Haida", + "hat":"HaitianHaitianCreole", + "hau":"Hausa", + "haw":"Hawaiian", + "heb":"Hebrew", + "her":"Herero", + "hil":"Hiligaynon", + "him":"HimachalilanguagesWesternPaharilanguages", + "hin":"Hindi", + "hit":"Hittite", + "hmn":"HmongMong", + "hmo":"HiriMotu", + "hrv":"Croatian", + "hsb":"UpperSorbian", + "hun":"Hungarian", + "hup":"Hupa", + "arm(B)hye(T)":"Armenian", + "iba":"Iban", + "ibo":"Igbo", + "iii":"SichuanYiNuosu", + "ijo":"Ijolanguages", + "iku":"Inuktitut", + "ile":"InterlingueOccidental", + "ilo":"Iloko", + "ina":"Interlingua(InternationalAuxiliaryLanguageAssociation)", + "inc":"Indiclanguages", + "ind":"Indonesian", + "ine":"Indo-Europeanlanguages", + "inh":"Ingush", + "ipk":"Inupiaq", + "ira":"Iranianlanguages", + "iro":"Iroquoianlanguages", + "ice(B)isl(T)":"Icelandic", + "ita":"Italian", + "jav":"Javanese", + "jbo":"Lojban", + "jpn":"Japanese", + "jpr":"Judeo-Persian", + "jrb":"Judeo-Arabic", + "kaa":"Kara-Kalpak", + "kab":"Kabyle", + "kac":"KachinJingpho", + "kal":"KalaallisutGreenlandic", + "kam":"Kamba", + "kan":"Kannada", + "kar":"Karenlanguages", + "kas":"Kashmiri", + "geo(B)kat(T)":"Georgian", + "kau":"Kanuri", + "kaw":"Kawi", + "kaz":"Kazakh", + "kbd":"Kabardian", + "kha":"Khasi", + "khi":"Khoisanlanguages", + "khm":"CentralKhmer", + "kho":"KhotaneseSakan", + "kik":"KikuyuGikuyu", + "kin":"Kinyarwanda", + "kir":"KirghizKyrgyz", + "kmb":"Kimbundu", + "kok":"Konkani", + "kom":"Komi", + "kon":"Kongo", + "kor":"Korean", + "kos":"Kosraean", + "kpe":"Kpelle", + "krc":"Karachay-Balkar", + "krl":"Karelian", + "kro":"Krulanguages", + "kru":"Kurukh", + "kua":"KuanyamaKwanyama", + "kum":"Kumyk", + "kur":"Kurdish", + "kut":"Kutenai", + "lad":"Ladino", + "lah":"Lahnda", + "lam":"Lamba", + "lat":"Latin", + "lav":"Latvian", + "lez":"Lezghian", + "lim":"LimburganLimburgerLimburgish", + "lin":"Lingala", + "lit":"Lithuanian", + "lol":"Mongo", + "loz":"Lozi", + "ltz":"LuxembourgishLetzeburgesch", + "lua":"Luba-Lulua", + "lub":"Luba-Katanga", + "lug":"Ganda", + "lui":"Luiseno", + "lun":"Lunda", + "luo":"Luo(KenyaandTanzania)", + "lus":"Lushai", + "mac(B)mkd(T)":"Macedonian", + "mad":"Madurese", + "mag":"Magahi", + "mah":"Marshallese", + "mai":"Maithili", + "mak":"Makasar", + "mal":"Malayalam", + "man":"Mandingo", + "mao(B)mri(T)":"Maori", + "map":"Austronesianlanguages", + "mar":"Marathi", + "mas":"Masai", + "may(B)msa(T)":"Malay", + "mdf":"Moksha", + "mdr":"Mandar", + "men":"Mende", + "mga":"Irish,Middle(900-1200)", + "mic":"Mi'kmaqMicmac", + "min":"Minangkabau", + "mis":"Uncodedlanguages", + "mkh":"Mon-Khmerlanguages", + "mlg":"Malagasy", + "mlt":"Maltese", + "mnc":"Manchu", + "mni":"Manipuri", + "mno":"Manobolanguages", + "moh":"Mohawk", + "mon":"Mongolian", + "mos":"Mossi", + "mul":"Multiplelanguages", + "mun":"Mundalanguages", + "mus":"Creek", + "mwl":"Mirandese", + "mwr":"Marwari", + "myn":"Mayanlanguages", + "myv":"Erzya", + "nah":"Nahuatllanguages", + "nai":"NorthAmericanIndianlanguages", + "nap":"Neapolitan", + "nau":"Nauru", + "nav":"NavajoNavaho", + "nbl":"Ndebele,SouthSouthNdebele", + "nde":"Ndebele,NorthNorthNdebele", + "ndo":"Ndonga", + "nds":"LowGermanLowSaxonGerman,LowSaxon,Low", + "nep":"Nepali", + "new":"NepalBhasaNewari", + "nia":"Nias", + "nic":"Niger-Kordofanianlanguages", + "niu":"Niuean", + "dut(B)nld(T)":"DutchFlemish", + "nno":"NorwegianNynorskNynorsk,Norwegian", + "nob":"Bokmål,NorwegianNorwegianBokmål", + "nog":"Nogai", + "non":"Norse,Old", + "nor":"Norwegian", + "nqo":"N'Ko", + "nso":"PediSepediNorthernSotho", + "nub":"Nubianlanguages", + "nwc":"ClassicalNewariOldNewariClassicalNepalBhasa", + "nya":"ChichewaChewaNyanja", + "nym":"Nyamwezi", + "nyn":"Nyankole", + "nyo":"Nyoro", + "nzi":"Nzima", + "oci":"Occitan(post1500)", + "oji":"Ojibwa", + "ori":"Oriya", + "orm":"Oromo", + "osa":"Osage", + "oss":"OssetianOssetic", + "ota":"Turkish,Ottoman(1500-1928)", + "oto":"Otomianlanguages", + "paa":"Papuanlanguages", + "pag":"Pangasinan", + "pal":"Pahlavi", + "pam":"PampangaKapampangan", + "pan":"PanjabiPunjabi", + "pap":"Papiamento", + "pau":"Palauan", + "peo":"Persian,Old(ca.600-400B.C.)", + "per(B)fas(T)":"Persian", + "phi":"Philippinelanguages", + "phn":"Phoenician", + "pli":"Pali", + "pol":"Polish", + "pon":"Pohnpeian", + "por":"Portuguese", + "pra":"Prakritlanguages", + "pro":"Provençal,Old(to1500)Occitan,Old(to1500)", + "pus":"PushtoPashto", + "qaa-qtz":"Reservedforlocaluse", + "que":"Quechua", + "raj":"Rajasthani", + "rap":"Rapanui", + "rar":"RarotonganCookIslandsMaori", + "roa":"Romancelanguages", + "roh":"Romansh", + "rom":"Romany", + "rum(B)ron(T)":"RomanianMoldavianMoldovan", + "run":"Rundi", + "rup":"AromanianArumanianMacedo-Romanian", + "rus":"Russian", + "sad":"Sandawe", + "sag":"Sango", + "sah":"Yakut", + "sai":"SouthAmericanIndianlanguages", + "sal":"Salishanlanguages", + "sam":"SamaritanAramaic", + "san":"Sanskrit", + "sas":"Sasak", + "sat":"Santali", + "scn":"Sicilian", + "sco":"Scots", + "sel":"Selkup", + "sem":"Semiticlanguages", + "sga":"Irish,Old(to900)", + "sgn":"SignLanguages", + "shn":"Shan", + "sid":"Sidamo", + "sin":"SinhalaSinhalese", + "sio":"Siouanlanguages", + "sit":"Sino-Tibetanlanguages", + "sla":"Slaviclanguages", + "slo(B)slk(T)":"Slovak", + "slv":"Slovenian", + "sma":"SouthernSami", + "sme":"NorthernSami", + "smi":"Samilanguages", + "smj":"LuleSami", + "smn":"InariSami", + "smo":"Samoan", + "sms":"SkoltSami", + "sna":"Shona", + "snd":"Sindhi", + "snk":"Soninke", + "sog":"Sogdian", + "som":"Somali", + "son":"Songhailanguages", + "sot":"Sotho,Southern", + "spa":"SpanishCastilian", + "alb(B)sqi(T)":"Albanian", + "srd":"Sardinian", + "srn":"SrananTongo", + "srp":"Serbian", + "srr":"Serer", + "ssa":"Nilo-Saharanlanguages", + "ssw":"Swati", + "suk":"Sukuma", + "sun":"Sundanese", + "sus":"Susu", + "sux":"Sumerian", + "swa":"Swahili", + "swe":"Swedish", + "syc":"ClassicalSyriac", + "syr":"Syriac", + "tah":"Tahitian", + "tai":"Tailanguages", + "tam":"Tamil", + "tat":"Tatar", + "tel":"Telugu", + "tem":"Timne", + "ter":"Tereno", + "tet":"Tetum", + "tgk":"Tajik", + "tgl":"Tagalog", + "tha":"Thai", + "tib(B)bod(T)":"Tibetan", + "tig":"Tigre", + "tir":"Tigrinya", + "tiv":"Tiv", + "tkl":"Tokelau", + "tlh":"KlingontlhIngan-Hol", + "tli":"Tlingit", + "tmh":"Tamashek", + "tog":"Tonga(Nyasa)", + "ton":"Tonga(TongaIslands)", + "tpi":"TokPisin", + "tsi":"Tsimshian", + "tsn":"Tswana", + "tso":"Tsonga", + "tuk":"Turkmen", + "tum":"Tumbuka", + "tup":"Tupilanguages", + "tur":"Turkish", + "tut":"Altaiclanguages", + "tvl":"Tuvalu", + "tyv":"Tuvinian", + "udm":"Udmurt", + "uga":"Ugaritic", + "uig":"UighurUyghur", + "ukr":"Ukrainian", + "umb":"Umbundu", + "und":"Undetermined", + "urd":"Urdu", + "uzb":"Uzbek", + "ven":"Venda", + "vie":"Vietnamese", + "vol":"Volapük", + "vot":"Votic", + "wak":"Wakashanlanguages", + "wal":"WolaittaWolaytta", + "war":"Waray", + "was":"Washo", + "wel(B)cym(T)":"Welsh", + "wen":"Sorbianlanguages", + "wln":"Walloon", + "wol":"Wolof", + "xal":"KalmykOirat", + "xho":"Xhosa", + "yap":"Yapese", + "yid":"Yiddish", + "yor":"Yoruba", + "ypk":"Yupiklanguages", + "zap":"Zapotec", + "zbl":"BlissymbolsBlissymbolicsBliss", + "zen":"Zenaga", + "zgh":"StandardMoroccanTamazight", + "zha":"ZhuangChuang", + "chi(B)zho(T)":"Chinese", + "chi":"Chinese", + "znd":"Zandelanguages", + "zul":"Zulu", + "zun":"Zuni", + "zxx":"NolinguisticcontentNotapplicable", + "zza":"ZazaDimiliDimliKirdkiKirmanjkiZazaki", + "afar":"Afar", + "abkhazian":"Abkhazian", + "achinese":"Achinese", + "acoli":"Acoli", + "adangme":"Adangme", + "adygheadygei":"AdygheAdygei", + "afro-asiaticlanguages":"Afro-Asiaticlanguages", + "afrihili":"Afrihili", + "afrikaans":"Afrikaans", + "ainu":"Ainu", + "akan":"Akan", + "akkadian":"Akkadian", + "aleut":"Aleut", + "algonquianlanguages":"Algonquianlanguages", + "southernaltai":"SouthernAltai", + "amharic":"Amharic", + "english,old(ca.450-1100)":"English,Old(ca.450-1100)", + "angika":"Angika", + "apachelanguages":"Apachelanguages", + "arabic":"Arabic", + "officialaramaic(700-300bce)imperialaramaic(700-300bce)":"OfficialAramaic(700-300BCE)ImperialAramaic(700-300BCE)", + "aragonese":"Aragonese", + "mapudungunmapuche":"MapudungunMapuche", + "arapaho":"Arapaho", + "artificiallanguages":"Artificiallanguages", + "arawak":"Arawak", + "assamese":"Assamese", + "asturianbableleoneseasturleonese":"AsturianBableLeoneseAsturleonese", + "athapascanlanguages":"Athapascanlanguages", + "australianlanguages":"Australianlanguages", + "avaric":"Avaric", + "avestan":"Avestan", + "awadhi":"Awadhi", + "aymara":"Aymara", + "azerbaijani":"Azerbaijani", + "bandalanguages":"Bandalanguages", + "bamilekelanguages":"Bamilekelanguages", + "bashkir":"Bashkir", + "baluchi":"Baluchi", + "bambara":"Bambara", + "balinese":"Balinese", + "basa":"Basa", + "balticlanguages":"Balticlanguages", + "bejabedawiyet":"BejaBedawiyet", + "belarusian":"Belarusian", + "bemba":"Bemba", + "bengali":"Bengali", + "berberlanguages":"Berberlanguages", + "bhojpuri":"Bhojpuri", + "biharilanguages":"Biharilanguages", + "bikol":"Bikol", + "biniedo":"BiniEdo", + "bislama":"Bislama", + "siksika":"Siksika", + "bantulanguages":"Bantulanguages", + "bosnian":"Bosnian", + "braj":"Braj", + "breton":"Breton", + "bataklanguages":"Bataklanguages", + "buriat":"Buriat", + "buginese":"Buginese", + "bulgarian":"Bulgarian", + "blinbilin":"BlinBilin", + "caddo":"Caddo", + "centralamericanindianlanguages":"CentralAmericanIndianlanguages", + "galibicarib":"GalibiCarib", + "catalanvalencian":"CatalanValencian", + "caucasianlanguages":"Caucasianlanguages", + "cebuano":"Cebuano", + "celticlanguages":"Celticlanguages", + "chamorro":"Chamorro", + "chibcha":"Chibcha", + "chechen":"Chechen", + "chagatai":"Chagatai", + "chuukese":"Chuukese", + "mari":"Mari", + "chinookjargon":"Chinookjargon", + "choctaw":"Choctaw", + "chipewyandenesuline":"ChipewyanDeneSuline", + "cherokee":"Cherokee", + "churchslavicoldslavonicchurchslavonicoldbulgarianoldchurchslavonic":"ChurchSlavicOldSlavonicChurchSlavonicOldBulgarianOldChurchSlavonic", + "chuvash":"Chuvash", + "cheyenne":"Cheyenne", + "chamiclanguages":"Chamiclanguages", + "montenegrin":"Montenegrin", + "coptic":"Coptic", + "cornish":"Cornish", + "corsican":"Corsican", + "creolesandpidgins,englishbased":"Creolesandpidgins,Englishbased", + "creolesandpidgins,french-based":"Creolesandpidgins,French-based", + "creolesandpidgins,portuguese-based":"Creolesandpidgins,Portuguese-based", + "cree":"Cree", + "crimeantatarcrimeanturkish":"CrimeanTatarCrimeanTurkish", + "creolesandpidgins":"Creolesandpidgins", + "kashubian":"Kashubian", + "cushiticlanguages":"Cushiticlanguages", + "czech":"Czech", + "dakota":"Dakota", + "danish":"Danish", + "dargwa":"Dargwa", + "landdayaklanguages":"LandDayaklanguages", + "delaware":"Delaware", + "slave(athapascan)":"Slave(Athapascan)", + "dogrib":"Dogrib", + "dinka":"Dinka", + "divehidhivehimaldivian":"DivehiDhivehiMaldivian", + "dogri":"Dogri", + "dravidianlanguages":"Dravidianlanguages", + "lowersorbian":"LowerSorbian", + "duala":"Duala", + "dutch,middle(ca.1050-1350)":"Dutch,Middle(ca.1050-1350)", + "dyula":"Dyula", + "dzongkha":"Dzongkha", + "efik":"Efik", + "egyptian(ancient)":"Egyptian(Ancient)", + "ekajuk":"Ekajuk", + "elamite":"Elamite", + "english":"English", + "english,middle(1100-1500)":"English,Middle(1100-1500)", + "esperanto":"Esperanto", + "estonian":"Estonian", + "basque":"Basque", + "ewe":"Ewe", + "ewondo":"Ewondo", + "fang":"Fang", + "faroese":"Faroese", + "fanti":"Fanti", + "fijian":"Fijian", + "filipinopilipino":"FilipinoPilipino", + "finnish":"Finnish", + "finno-ugrianlanguages":"Finno-Ugrianlanguages", + "fon":"Fon", + "french":"French", + "french,middle(ca.1400-1600)":"French,Middle(ca.1400-1600)", + "french,old(842-ca.1400)":"French,Old(842-ca.1400)", + "northernfrisian":"NorthernFrisian", + "easternfrisian":"EasternFrisian", + "westernfrisian":"WesternFrisian", + "fulah":"Fulah", + "friulian":"Friulian", + "gayo":"Gayo", + "gbaya":"Gbaya", + "germaniclanguages":"Germaniclanguages", + "german":"German", + "geez":"Geez", + "gilbertese":"Gilbertese", + "gaelicscottishgaelic":"GaelicScottishGaelic", + "irish":"Irish", + "galician":"Galician", + "manx":"Manx", + "german,middlehigh(ca.1050-1500)":"German,MiddleHigh(ca.1050-1500)", + "german,oldhigh(ca.750-1050)":"German,OldHigh(ca.750-1050)", + "gondi":"Gondi", + "gorontalo":"Gorontalo", + "gothic":"Gothic", + "grebo":"Grebo", + "greek,ancient(to1453)":"Greek,Ancient(to1453)", + "greek,modern(1453-)":"Greek,Modern(1453-)", + "guarani":"Guarani", + "swissgermanalemannicalsatian":"SwissGermanAlemannicAlsatian", + "gujarati":"Gujarati", + "gwich'in":"Gwich'in", + "haida":"Haida", + "haitianhaitiancreole":"HaitianHaitianCreole", + "hausa":"Hausa", + "hawaiian":"Hawaiian", + "hebrew":"Hebrew", + "herero":"Herero", + "hiligaynon":"Hiligaynon", + "himachalilanguageswesternpaharilanguages":"HimachalilanguagesWesternPaharilanguages", + "hindi":"Hindi", + "hittite":"Hittite", + "hmongmong":"HmongMong", + "hirimotu":"HiriMotu", + "croatian":"Croatian", + "uppersorbian":"UpperSorbian", + "hungarian":"Hungarian", + "hupa":"Hupa", + "armenian":"Armenian", + "iban":"Iban", + "igbo":"Igbo", + "ido":"Ido", + "sichuanyinuosu":"SichuanYiNuosu", + "ijolanguages":"Ijolanguages", + "inuktitut":"Inuktitut", + "interlingueoccidental":"InterlingueOccidental", + "iloko":"Iloko", + "interlingua(internationalauxiliarylanguageassociation)":"Interlingua(InternationalAuxiliaryLanguageAssociation)", + "indiclanguages":"Indiclanguages", + "indonesian":"Indonesian", + "indo-europeanlanguages":"Indo-Europeanlanguages", + "ingush":"Ingush", + "inupiaq":"Inupiaq", + "iranianlanguages":"Iranianlanguages", + "iroquoianlanguages":"Iroquoianlanguages", + "icelandic":"Icelandic", + "italian":"Italian", + "javanese":"Javanese", + "lojban":"Lojban", + "japanese":"Japanese", + "judeo-persian":"Judeo-Persian", + "judeo-arabic":"Judeo-Arabic", + "kara-kalpak":"Kara-Kalpak", + "kabyle":"Kabyle", + "kachinjingpho":"KachinJingpho", + "kalaallisutgreenlandic":"KalaallisutGreenlandic", + "kamba":"Kamba", + "kannada":"Kannada", + "karenlanguages":"Karenlanguages", + "kashmiri":"Kashmiri", + "georgian":"Georgian", + "kanuri":"Kanuri", + "kawi":"Kawi", + "kazakh":"Kazakh", + "kabardian":"Kabardian", + "khasi":"Khasi", + "khoisanlanguages":"Khoisanlanguages", + "centralkhmer":"CentralKhmer", + "khotanesesakan":"KhotaneseSakan", + "kikuyugikuyu":"KikuyuGikuyu", + "kinyarwanda":"Kinyarwanda", + "kirghizkyrgyz":"KirghizKyrgyz", + "kimbundu":"Kimbundu", + "konkani":"Konkani", + "komi":"Komi", + "kongo":"Kongo", + "korean":"Korean", + "kosraean":"Kosraean", + "kpelle":"Kpelle", + "karachay-balkar":"Karachay-Balkar", + "karelian":"Karelian", + "krulanguages":"Krulanguages", + "kurukh":"Kurukh", + "kuanyamakwanyama":"KuanyamaKwanyama", + "kumyk":"Kumyk", + "kurdish":"Kurdish", + "kutenai":"Kutenai", + "ladino":"Ladino", + "lahnda":"Lahnda", + "lamba":"Lamba", + "lao":"Lao", + "latin":"Latin", + "latvian":"Latvian", + "lezghian":"Lezghian", + "limburganlimburgerlimburgish":"LimburganLimburgerLimburgish", + "lingala":"Lingala", + "lithuanian":"Lithuanian", + "mongo":"Mongo", + "lozi":"Lozi", + "luxembourgishletzeburgesch":"LuxembourgishLetzeburgesch", + "luba-lulua":"Luba-Lulua", + "luba-katanga":"Luba-Katanga", + "ganda":"Ganda", + "luiseno":"Luiseno", + "lunda":"Lunda", + "luo(kenyaandtanzania)":"Luo(KenyaandTanzania)", + "lushai":"Lushai", + "madurese":"Madurese", + "magahi":"Magahi", + "marshallese":"Marshallese", + "maithili":"Maithili", + "makasar":"Makasar", + "malayalam":"Malayalam", + "mandingo":"Mandingo", + "austronesianlanguages":"Austronesianlanguages", + "marathi":"Marathi", + "masai":"Masai", + "moksha":"Moksha", + "mandar":"Mandar", + "mende":"Mende", + "irish,middle(900-1200)":"Irish,Middle(900-1200)", + "mi'kmaqmicmac":"Mi'kmaqMicmac", + "minangkabau":"Minangkabau", + "uncodedlanguages":"Uncodedlanguages", + "macedonian":"Macedonian", + "mon-khmerlanguages":"Mon-Khmerlanguages", + "malagasy":"Malagasy", + "maltese":"Maltese", + "manchu":"Manchu", + "manipuri":"Manipuri", + "manobolanguages":"Manobolanguages", + "mohawk":"Mohawk", + "mongolian":"Mongolian", + "mossi":"Mossi", + "maori":"Maori", + "malay":"Malay", + "multiplelanguages":"Multiplelanguages", + "mundalanguages":"Mundalanguages", + "creek":"Creek", + "mirandese":"Mirandese", + "marwari":"Marwari", + "burmese":"Burmese", + "mayanlanguages":"Mayanlanguages", + "erzya":"Erzya", + "nahuatllanguages":"Nahuatllanguages", + "northamericanindianlanguages":"NorthAmericanIndianlanguages", + "neapolitan":"Neapolitan", + "nauru":"Nauru", + "navajonavaho":"NavajoNavaho", + "ndebele,southsouthndebele":"Ndebele,SouthSouthNdebele", + "ndebele,northnorthndebele":"Ndebele,NorthNorthNdebele", + "ndonga":"Ndonga", + "lowgermanlowsaxongerman,lowsaxon,low":"LowGermanLowSaxonGerman,LowSaxon,Low", + "nepali":"Nepali", + "nepalbhasanewari":"NepalBhasaNewari", + "nias":"Nias", + "niger-kordofanianlanguages":"Niger-Kordofanianlanguages", + "niuean":"Niuean", + "dutchflemish":"DutchFlemish", + "norwegiannynorsknynorsk,norwegian":"NorwegianNynorskNynorsk,Norwegian", + "bokmål,norwegiannorwegianbokmål":"Bokmål,NorwegianNorwegianBokmål", + "nogai":"Nogai", + "norse,old":"Norse,Old", + "norwegian":"Norwegian", + "n'ko":"N'Ko", + "pedisepedinorthernsotho":"PediSepediNorthernSotho", + "nubianlanguages":"Nubianlanguages", + "classicalnewarioldnewariclassicalnepalbhasa":"ClassicalNewariOldNewariClassicalNepalBhasa", + "chichewachewanyanja":"ChichewaChewaNyanja", + "nyamwezi":"Nyamwezi", + "nyankole":"Nyankole", + "nyoro":"Nyoro", + "nzima":"Nzima", + "occitan(post1500)":"Occitan(post1500)", + "ojibwa":"Ojibwa", + "oriya":"Oriya", + "oromo":"Oromo", + "osage":"Osage", + "ossetianossetic":"OssetianOssetic", + "turkish,ottoman(1500-1928)":"Turkish,Ottoman(1500-1928)", + "otomianlanguages":"Otomianlanguages", + "papuanlanguages":"Papuanlanguages", + "pangasinan":"Pangasinan", + "pahlavi":"Pahlavi", + "pampangakapampangan":"PampangaKapampangan", + "panjabipunjabi":"PanjabiPunjabi", + "papiamento":"Papiamento", + "palauan":"Palauan", + "persian,old(ca.600-400b.c.)":"Persian,Old(ca.600-400B.C.)", + "persian":"Persian", + "philippinelanguages":"Philippinelanguages", + "phoenician":"Phoenician", + "pali":"Pali", + "polish":"Polish", + "pohnpeian":"Pohnpeian", + "portuguese":"Portuguese", + "prakritlanguages":"Prakritlanguages", + "provençal,old(to1500)occitan,old(to1500)":"Provençal,Old(to1500)Occitan,Old(to1500)", + "pushtopashto":"PushtoPashto", + "reservedforlocaluse":"Reservedforlocaluse", + "quechua":"Quechua", + "rajasthani":"Rajasthani", + "rapanui":"Rapanui", + "rarotongancookislandsmaori":"RarotonganCookIslandsMaori", + "romancelanguages":"Romancelanguages", + "romansh":"Romansh", + "romany":"Romany", + "romanianmoldavianmoldovan":"RomanianMoldavianMoldovan", + "rundi":"Rundi", + "aromanianarumanianmacedo-romanian":"AromanianArumanianMacedo-Romanian", + "russian":"Russian", + "sandawe":"Sandawe", + "sango":"Sango", + "yakut":"Yakut", + "southamericanindianlanguages":"SouthAmericanIndianlanguages", + "salishanlanguages":"Salishanlanguages", + "samaritanaramaic":"SamaritanAramaic", + "sanskrit":"Sanskrit", + "sasak":"Sasak", + "santali":"Santali", + "sicilian":"Sicilian", + "scots":"Scots", + "selkup":"Selkup", + "semiticlanguages":"Semiticlanguages", + "irish,old(to900)":"Irish,Old(to900)", + "signlanguages":"SignLanguages", + "shan":"Shan", + "sidamo":"Sidamo", + "sinhalasinhalese":"SinhalaSinhalese", + "siouanlanguages":"Siouanlanguages", + "sino-tibetanlanguages":"Sino-Tibetanlanguages", + "slaviclanguages":"Slaviclanguages", + "slovak":"Slovak", + "slovenian":"Slovenian", + "southernsami":"SouthernSami", + "northernsami":"NorthernSami", + "samilanguages":"Samilanguages", + "lulesami":"LuleSami", + "inarisami":"InariSami", + "samoan":"Samoan", + "skoltsami":"SkoltSami", + "shona":"Shona", + "sindhi":"Sindhi", + "soninke":"Soninke", + "sogdian":"Sogdian", + "somali":"Somali", + "songhailanguages":"Songhailanguages", + "sotho,southern":"Sotho,Southern", + "spanishcastilian":"SpanishCastilian", + "albanian":"Albanian", + "sardinian":"Sardinian", + "sranantongo":"SrananTongo", + "serbian":"Serbian", + "serer":"Serer", + "nilo-saharanlanguages":"Nilo-Saharanlanguages", + "swati":"Swati", + "sukuma":"Sukuma", + "sundanese":"Sundanese", + "susu":"Susu", + "sumerian":"Sumerian", + "swahili":"Swahili", + "swedish":"Swedish", + "classicalsyriac":"ClassicalSyriac", + "syriac":"Syriac", + "tahitian":"Tahitian", + "tailanguages":"Tailanguages", + "tamil":"Tamil", + "tatar":"Tatar", + "telugu":"Telugu", + "timne":"Timne", + "tereno":"Tereno", + "tetum":"Tetum", + "tajik":"Tajik", + "tagalog":"Tagalog", + "thai":"Thai", + "tibetan":"Tibetan", + "tigre":"Tigre", + "tigrinya":"Tigrinya", + "tokelau":"Tokelau", + "klingontlhingan-hol":"KlingontlhIngan-Hol", + "tlingit":"Tlingit", + "tamashek":"Tamashek", + "tonga(nyasa)":"Tonga(Nyasa)", + "tonga(tongaislands)":"Tonga(TongaIslands)", + "tokpisin":"TokPisin", + "tsimshian":"Tsimshian", + "tswana":"Tswana", + "tsonga":"Tsonga", + "turkmen":"Turkmen", + "tumbuka":"Tumbuka", + "tupilanguages":"Tupilanguages", + "turkish":"Turkish", + "altaiclanguages":"Altaiclanguages", + "tuvalu":"Tuvalu", + "twi":"Twi", + "tuvinian":"Tuvinian", + "udmurt":"Udmurt", + "ugaritic":"Ugaritic", + "uighuruyghur":"UighurUyghur", + "ukrainian":"Ukrainian", + "umbundu":"Umbundu", + "undetermined":"Undetermined", + "urdu":"Urdu", + "uzbek":"Uzbek", + "vai":"Vai", + "venda":"Venda", + "vietnamese":"Vietnamese", + "volapük":"Volapük", + "votic":"Votic", + "wakashanlanguages":"Wakashanlanguages", + "wolaittawolaytta":"WolaittaWolaytta", + "waray":"Waray", + "washo":"Washo", + "welsh":"Welsh", + "sorbianlanguages":"Sorbianlanguages", + "walloon":"Walloon", + "wolof":"Wolof", + "kalmykoirat":"KalmykOirat", + "xhosa":"Xhosa", + "yao":"Yao", + "yapese":"Yapese", + "yiddish":"Yiddish", + "yoruba":"Yoruba", + "yupiklanguages":"Yupiklanguages", + "zapotec":"Zapotec", + "blissymbolsblissymbolicsbliss":"BlissymbolsBlissymbolicsBliss", + "zenaga":"Zenaga", + "standardmoroccantamazight":"StandardMoroccanTamazight", + "zhuangchuang":"ZhuangChuang", + "chinese":"Chinese", + "zandelanguages":"Zandelanguages", + "zulu":"Zulu", + "zuni":"Zuni", + "nolinguisticcontentnotapplicable":"NolinguisticcontentNotapplicable", + "zazadimilidimlikirdkikirmanjkizazaki":"ZazaDimiliDimliKirdkiKirmanjkiZazaki", + "influencer1":"Influencer1", + "influencer2":"Influencer2", + "original":"Original" + } + }, + "ccu":458, + "deviceId":"yxN2Cd3kIhYKTM7aYAxGl5NnYCAdyIlQ", + "isAllowedWeb":true, + "epgActive":{ + + }, + "configBlackout":{ + "duration":300000, + "enable":false, + "btn_channel_list":{ + "title_en":"", + "title_th":"", + "url":"", + "url_th":"", + "url_en":"" + } + }, + "activeCategory":"", + "adsSideBar":{ + "adsData":{ + "ALL":{ + "targetingArguments":{ + "TrueID_page":[ + + ], + "Device":[ + + ] + }, + "sizeMapping":[ + { + "viewport":[ + 0, + 0 + ], + "sizes":[ + [ + 320, + 250 + ], + [ + 300, + 250 + ], + [ + 1, + 1 + ], + "fluid" + ] + } + ], + "slotId":"div-gpt-ad-rt-1", + "adUnit":"21682623839/TrueID_Web/TV", + "sizes":[ + [ + 320, + 250 + ] + ] + } + }, + "adsConfig":{ + "adsNetworkId":"", + "adsUnit":"21682623839/TrueID_Web/TV" + } + }, + "currentURL":"https://tv.trueid.net/th-en/live/true-movie-hits" + }, + "__N_SSP":true +} \ No newline at end of file diff --git a/sites/tv.trueid.net/tv.trueid.net.config.js b/sites/tv.trueid.net/tv.trueid.net.config.js index 2de101cf..b92a317a 100644 --- a/sites/tv.trueid.net/tv.trueid.net.config.js +++ b/sites/tv.trueid.net/tv.trueid.net.config.js @@ -1,74 +1,74 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - delay: 1000, - site: 'tv.trueid.net', - days: 1, - url({ channel }) { - return `https://tv.trueid.net/_next/data/1380644e0f1fb6b14c82894a0c682d147e015c9d/th-${channel.lang}.json?channelSlug=${channel.site_id}&path=${channel.site_id}` - }, - parser({ content, channel }) { - const programs = [] - parseItems(content, channel).forEach(item => { - programs.push({ - title: item.title, - description: parseDescription(item, channel.lang), - image: parseImage(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels({ token, lang = 'en' }) { - const axios = require('axios') - const ACCESS_TOKEN = token - ? token - : 'MTM4MDY0NGUwZjFmYjZiMTRjODI4OTRhMGM2ODJkMTQ3ZTAxNWM5ZDoxZmI2YjE0YzgyODk0YTBjNjgyZDE0N2UwMTVjOWQ=' - - const data = await axios - .get(`https://tv.trueid.net/api/channel/getChannelListByAllCate?lang=${lang}&country=th`, { - headers: { - authorization: `Basic ${ACCESS_TOKEN}` - } - }) - .then(r => r.data) - .catch(console.error) - - return data.data.channelsList - .find(i => i.catSlug === 'TrueID : All') - .channels.map(item => { - return { - lang, - site_id: item.slug, - name: item.title - } - }) - } -} - -function parseDescription(item, lang) { - const description = item.info?.[`synopsis_${lang}`] - return description && description !== '.' ? description : null -} - -function parseImage(item) { - return item.info?.image || null -} - -function parseStart(item) { - return item.start_date ? dayjs.utc(item.start_date) : null -} - -function parseStop(item) { - return item.end_date ? dayjs.utc(item.end_date) : null -} - -function parseItems(content) { - const data = content ? JSON.parse(content) : null - return data?.pageProps?.epgList || [] -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + delay: 1000, + site: 'tv.trueid.net', + days: 1, + url({ channel }) { + return `https://tv.trueid.net/_next/data/1380644e0f1fb6b14c82894a0c682d147e015c9d/th-${channel.lang}.json?channelSlug=${channel.site_id}&path=${channel.site_id}` + }, + parser({ content, channel }) { + const programs = [] + parseItems(content, channel).forEach(item => { + programs.push({ + title: item.title, + description: parseDescription(item, channel.lang), + image: parseImage(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels({ token, lang = 'en' }) { + const axios = require('axios') + const ACCESS_TOKEN = token + ? token + : 'MTM4MDY0NGUwZjFmYjZiMTRjODI4OTRhMGM2ODJkMTQ3ZTAxNWM5ZDoxZmI2YjE0YzgyODk0YTBjNjgyZDE0N2UwMTVjOWQ=' + + const data = await axios + .get(`https://tv.trueid.net/api/channel/getChannelListByAllCate?lang=${lang}&country=th`, { + headers: { + authorization: `Basic ${ACCESS_TOKEN}` + } + }) + .then(r => r.data) + .catch(console.error) + + return data.data.channelsList + .find(i => i.catSlug === 'TrueID : All') + .channels.map(item => { + return { + lang, + site_id: item.slug, + name: item.title + } + }) + } +} + +function parseDescription(item, lang) { + const description = item.info?.[`synopsis_${lang}`] + return description && description !== '.' ? description : null +} + +function parseImage(item) { + return item.info?.image || null +} + +function parseStart(item) { + return item.start_date ? dayjs.utc(item.start_date) : null +} + +function parseStop(item) { + return item.end_date ? dayjs.utc(item.end_date) : null +} + +function parseItems(content) { + const data = content ? JSON.parse(content) : null + return data?.pageProps?.epgList || [] +} diff --git a/sites/tv.trueid.net/tv.trueid.net.test.js b/sites/tv.trueid.net/tv.trueid.net.test.js index 02386c7b..afd6d06f 100644 --- a/sites/tv.trueid.net/tv.trueid.net.test.js +++ b/sites/tv.trueid.net/tv.trueid.net.test.js @@ -1,62 +1,62 @@ -const { parser, url } = require('./tv.trueid.net.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('2023-12-11').startOf('d') -const channel = { - lang: 'en', - site_id: 'true-movie-hits', - xmltv_id: 'TrueMovieHits.th' -} -const channelTh = Object.assign({}, channel, { lang: 'th' }) -const data = fs.readFileSync(path.resolve(__dirname, '__data__/data.json')) - -it('can generate valid url', () => { - const result = url({ channel, date }) - expect(result).toBe( - 'https://tv.trueid.net/_next/data/1380644e0f1fb6b14c82894a0c682d147e015c9d/th-en.json?channelSlug=true-movie-hits&path=true-movie-hits' - ) -}) - -it('can parse English response', () => { - const result = parser({ date, channel, content: data }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(result[0]).toMatchObject({ - start: '2023-12-11T19:05:00.000Z', - stop: '2023-12-11T20:55:00.000Z', - title: 'The Last Witch Hunter', - description: - 'A young man is all that stands between humanity and the most horrifying witches in history.', - image: 'https://bms.dmpcdn.com/uploads/pic/381f853da5f4a310bf248357fed21a57.jpg' - }) -}) - -it('can parse Thai response', () => { - const result = parser({ date, channel: channelTh, content: data }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(result[0]).toMatchObject({ - start: '2023-12-11T19:05:00.000Z', - stop: '2023-12-11T20:55:00.000Z', - title: 'The Last Witch Hunter', - description: - 'หนุ่มนักล่าแม่มดถูกสาปให้เป็นอมตะจนกระทั่งราชินีแม่มดได้ฟื้นคืนชีพขึ้นมาจึงมีเพียงเขาคนเดียวเท่านั้นที่จะสามารถกอบกู้มวลมนุษยชาติได้', - image: 'https://bms.dmpcdn.com/uploads/pic/381f853da5f4a310bf248357fed21a57.jpg' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ date, channel, content: '{}' }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./tv.trueid.net.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('2023-12-11').startOf('d') +const channel = { + lang: 'en', + site_id: 'true-movie-hits', + xmltv_id: 'TrueMovieHits.th' +} +const channelTh = Object.assign({}, channel, { lang: 'th' }) +const data = fs.readFileSync(path.resolve(__dirname, '__data__/data.json')) + +it('can generate valid url', () => { + const result = url({ channel, date }) + expect(result).toBe( + 'https://tv.trueid.net/_next/data/1380644e0f1fb6b14c82894a0c682d147e015c9d/th-en.json?channelSlug=true-movie-hits&path=true-movie-hits' + ) +}) + +it('can parse English response', () => { + const result = parser({ date, channel, content: data }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(result[0]).toMatchObject({ + start: '2023-12-11T19:05:00.000Z', + stop: '2023-12-11T20:55:00.000Z', + title: 'The Last Witch Hunter', + description: + 'A young man is all that stands between humanity and the most horrifying witches in history.', + image: 'https://bms.dmpcdn.com/uploads/pic/381f853da5f4a310bf248357fed21a57.jpg' + }) +}) + +it('can parse Thai response', () => { + const result = parser({ date, channel: channelTh, content: data }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(result[0]).toMatchObject({ + start: '2023-12-11T19:05:00.000Z', + stop: '2023-12-11T20:55:00.000Z', + title: 'The Last Witch Hunter', + description: + 'หนุ่มนักล่าแม่มดถูกสาปให้เป็นอมตะจนกระทั่งราชินีแม่มดได้ฟื้นคืนชีพขึ้นมาจึงมีเพียงเขาคนเดียวเท่านั้นที่จะสามารถกอบกู้มวลมนุษยชาติได้', + image: 'https://bms.dmpcdn.com/uploads/pic/381f853da5f4a310bf248357fed21a57.jpg' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ date, channel, content: '{}' }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv.yandex.ru/__data__/no_content.html b/sites/tv.yandex.ru/__data__/no_content.html new file mode 100644 index 00000000..6fedfd4c --- /dev/null +++ b/sites/tv.yandex.ru/__data__/no_content.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sites/tv.yandex.ru/tv.yandex.ru.config.js b/sites/tv.yandex.ru/tv.yandex.ru.config.js index 0dd19c00..241069dd 100644 --- a/sites/tv.yandex.ru/tv.yandex.ru.config.js +++ b/sites/tv.yandex.ru/tv.yandex.ru.config.js @@ -1,295 +1,295 @@ -const dayjs = require('dayjs') -const doFetch = require('@ntlab/sfetch') -const debug = require('debug')('site:tv.yandex.ru') - -doFetch.setDebugger(debug).setMaxWorker(10) - -// enable to fetch guide description but its take a longer time -const detailedGuide = true - -// update this data by heading to https://tv.yandex.ru and change the values accordingly -const cookies = { - i: 'eIUfSP+/mzQWXcH+Cuz8o1vY+D2K8fhBd6Sj0xvbPZeO4l3cY+BvMp8fFIuM17l6UE1Z5+R2a18lP00ex9iYVJ+VT+c=', - spravka: - 'dD0xNzM0MjA0NjM4O2k9MTI1LjE2NC4xNDkuMjAwO0Q9QTVCQ0IyOTI5RDQxNkU5NkEyOTcwMTNDMzZGMDAzNjRDNTFFNDM4QkE2Q0IyOTJDRjhCOTZDRDIzODdBQzk2MzRFRDc5QTk2Qjc2OEI1MUY5MTM5M0QzNkY3OEQ2OUY3OTUwNkQ3RjBCOEJGOEJDMjAwMTQ0RDUwRkFCMDNEQzJFMDI2OEI5OTk5OUJBNEFERUYwOEQ1MjUwQTE0QTI3RDU1MEQwM0U0O3U9MTczNDIwNDYzODUyNDYyNzg1NDtoPTIxNTc0ZTc2MDQ1ZjcwMDBkYmY0NTVkM2Q2ZWMyM2Y1', - yandexuid: '1197179041732383499', - yashr: '4682342911732383504', - yuidss: '1197179041732383499', - user_display: 824 -} -const headers = { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 OPR/114.0.0.0' -} -const caches = {} - -module.exports = { - site: 'tv.yandex.ru', - days: 2, - url({ date }) { - return getUrl(date) - }, - request: { - cache: { - ttl: 3600000 // 1 hour - }, - headers: getHeaders() - }, - async parser({ content, date, channel }) { - const programs = [] - const events = [] - - if (content && parseContent(content, date, true)) { - const cacheid = date.format('YYYY-MM-DD') - if (!caches[cacheid]) { - debug(`Please wait while fetching schedules for ${cacheid}`) - caches[cacheid] = await fetchSchedules({ date, content }) - } - if (detailedGuide) { - await fetchPrograms({ schedules: caches[cacheid], date, channel }) - } - caches[cacheid].forEach(schedule => { - schedule.events - .filter( - event => event.channelFamilyId == channel.site_id && date.isSame(event.start, 'day') - ) - .forEach(event => { - if (events.indexOf(event.id) < 0) { - events.push(event.id) - programs.push({ - title: event.title, - description: event.program.description, - category: event.program.type.name, - start: dayjs(event.start), - stop: dayjs(event.finish) - }) - } - }) - }) - } - - return programs - }, - async channels() { - const channels = [] - const included = [] - const schedules = await fetchSchedules({ date: dayjs() }) - schedules.forEach(schedule => { - if (schedule.channel && included.indexOf(schedule.channel.familyId) < 0) { - included.push(schedule.channel.familyId) - channels.push({ - lang: 'ru', - site_id: schedule.channel.familyId.toString(), - name: schedule.channel.title - }) - } - }) - - return channels - } -} - -async function fetchSchedules({ date, content = null }) { - const schedules = [] - const queues = [] - const fetches = [] - const url = getUrl(date) - - let mainApi - // parse content as schedules and add to queue if more requests is needed - const f = (src, res, headers) => { - if (src) { - fetches.push(src) - } - if (headers) { - parseCookies(headers) - } - const [q, s] = parseContent(res, date) - if (!mainApi) { - mainApi = true - if (caches.region) { - queues.push(getQueue(getUrl(date, caches.region), src)) - } - } - for (const url of q) { - if (fetches.indexOf(url) < 0) { - queues.push(getQueue(url, src)) - } - } - schedules.push(...s) - } - // is main html already fetched? - if (content) { - f(url, content) - } else { - queues.push(getQueue(url, 'https://tv.yandex.ru/')) - } - // fetch all queues - await doFetch(queues, f) - - return schedules -} - -async function fetchPrograms({ schedules, date, channel }) { - const queues = [] - schedules - .filter(schedule => schedule.channel.familyId == channel.site_id) - .forEach(schedule => { - queues.push( - ...schedule.events - .filter(event => date.isSame(event.start, 'day')) - .map(event => getQueue(getUrl(null, caches.region, null, event), 'https://tv.yandex.ru/')) - ) - }) - await doFetch(queues, (queue, res, headers) => { - if (headers) { - parseCookies(headers) - } - // is it a program? - if (res?.program) { - let updated = false - schedules.forEach(schedule => { - schedule.events.forEach(event => { - if (event.channelFamilyId === res.channelFamilyId && event.id === res.id) { - Object.assign(event, res) - updated = true - return true - } - }) - if (updated) { - return true - } - }) - } - }) -} - -function parseContent(content, date, checkOnly = false) { - const queues = [] - const schedules = [] - let valid = false - if (content) { - if (Buffer.isBuffer(content)) { - content = content.toString() - } - // got captcha, its look like our cookies has expired - if ( - content?.type === 'captcha' || - (typeof content === 'string' && content.match(/SmartCaptcha/)) - ) { - throw new Error('Got captcha, please goto https://tv.yandex.ru and update cookies!') - } - if (typeof content === 'object') { - let items - if (content.schedule) { - // fetch next request based on schedule map - if (Array.isArray(content.schedule.scheduleMap)) { - queues.push(...content.schedule.scheduleMap.map(m => getUrl(date, caches.region, m))) - } - // find some schedules? - if (Array.isArray(content.schedule.schedules)) { - items = content.schedule.schedules - } - } - // find another schedules? - if (Array.isArray(content.schedules)) { - items = content.schedules - } - // add programs - if (items && items.length) { - schedules.push(...getSchedules(items)) - } - } else { - // prepare headers for next http request - const [, region] = content.match(/region: '(\d+)'/i) || [null, null] - const [, initialSk] = content.match(/window.__INITIAL_SK__ = (.*);/i) || [null, null] - const [, sessionId] = content.match(/window.__USER_SESSION_ID__ = "(.*)";/i) || [null, null] - const tvSk = initialSk ? JSON.parse(initialSk) : {} - if (region) { - caches.region = region - } - if (tvSk.key) { - headers['X-Tv-Sk'] = tvSk.key - } - if (sessionId) { - headers['X-User-Session-Id'] = sessionId - } - if (checkOnly && region && tvSk.key && sessionId) { - valid = true - } - } - } - - return checkOnly ? valid : [queues, schedules] -} - -function parseCookies(headers) { - if (Array.isArray(headers['set-cookie'])) { - headers['set-cookie'].forEach(cookie => { - const [key, value] = cookie.split('; ')[0].split('=') - if (cookies[key] !== value) { - cookies[key] = value - debug(`Update cookie ${key}=${value}`) - } - }) - } -} - -function getSchedules(schedules) { - return schedules.filter(schedule => schedule.events.length) -} - -function getHeaders(data = {}) { - return Object.assign( - {}, - headers, - { - Cookie: Object.keys(cookies) - .map(cookie => `${cookie}=${cookies[cookie]}`) - .join('; ') - }, - data - ) -} - -function getUrl(date, region = null, page = null, event = null) { - let url = 'https://tv.yandex.ru/' - if (region) { - url += `api/${region}` - } - if (page && page.id !== undefined) { - url += `${url.endsWith('/') ? '' : '/'}main/chunk?page=${page.id}` - } - if (event && event.id !== undefined) { - url += `${url.endsWith('/') ? '' : '/'}event?eventId=${event.id}&programCoId=` - } - if (date) { - url += `${url.indexOf('?') < 0 ? '?' : '&'}date=${date.format('YYYY-MM-DD')}${ - !page ? '&grid=all' : '' - }&period=all-day` - } - if (page && page.id !== undefined && page.offset !== undefined) { - url += `${url.indexOf('?') < 0 ? '?' : '&'}offset=${page.offset}` - } - if (page && page.id !== undefined && page.limit !== undefined) { - url += `${url.indexOf('?') < 0 ? '?' : '&'}limit=${page.limit}` - } - return url -} - -function getQueue(url, referer) { - const data = { - Origin: 'https://tv.yandex.ru' - } - if (referer) { - data['Referer'] = referer - } - if (url.indexOf('api') > 0) { - data['X-Requested-With'] = 'XMLHttpRequest' - } - const headers = getHeaders(data) - return { - url, - params: { headers } - } -} +const dayjs = require('dayjs') +const doFetch = require('@ntlab/sfetch') +const debug = require('debug')('site:tv.yandex.ru') + +doFetch.setDebugger(debug).setMaxWorker(10) + +// enable to fetch guide description but its take a longer time +const detailedGuide = true + +// update this data by heading to https://tv.yandex.ru and change the values accordingly +const cookies = { + i: 'eIUfSP+/mzQWXcH+Cuz8o1vY+D2K8fhBd6Sj0xvbPZeO4l3cY+BvMp8fFIuM17l6UE1Z5+R2a18lP00ex9iYVJ+VT+c=', + spravka: + 'dD0xNzM0MjA0NjM4O2k9MTI1LjE2NC4xNDkuMjAwO0Q9QTVCQ0IyOTI5RDQxNkU5NkEyOTcwMTNDMzZGMDAzNjRDNTFFNDM4QkE2Q0IyOTJDRjhCOTZDRDIzODdBQzk2MzRFRDc5QTk2Qjc2OEI1MUY5MTM5M0QzNkY3OEQ2OUY3OTUwNkQ3RjBCOEJGOEJDMjAwMTQ0RDUwRkFCMDNEQzJFMDI2OEI5OTk5OUJBNEFERUYwOEQ1MjUwQTE0QTI3RDU1MEQwM0U0O3U9MTczNDIwNDYzODUyNDYyNzg1NDtoPTIxNTc0ZTc2MDQ1ZjcwMDBkYmY0NTVkM2Q2ZWMyM2Y1', + yandexuid: '1197179041732383499', + yashr: '4682342911732383504', + yuidss: '1197179041732383499', + user_display: 824 +} +const headers = { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 OPR/114.0.0.0' +} +const caches = {} + +module.exports = { + site: 'tv.yandex.ru', + days: 2, + url({ date }) { + return getUrl(date) + }, + request: { + cache: { + ttl: 3600000 // 1 hour + }, + headers: getHeaders() + }, + async parser({ content, date, channel }) { + const programs = [] + const events = [] + + if (content && parseContent(content, date, true)) { + const cacheid = date.format('YYYY-MM-DD') + if (!caches[cacheid]) { + debug(`Please wait while fetching schedules for ${cacheid}`) + caches[cacheid] = await fetchSchedules({ date, content }) + } + if (detailedGuide) { + await fetchPrograms({ schedules: caches[cacheid], date, channel }) + } + caches[cacheid].forEach(schedule => { + schedule.events + .filter( + event => event.channelFamilyId == channel.site_id && date.isSame(event.start, 'day') + ) + .forEach(event => { + if (events.indexOf(event.id) < 0) { + events.push(event.id) + programs.push({ + title: event.title, + description: event.program.description, + category: event.program.type.name, + start: dayjs(event.start), + stop: dayjs(event.finish) + }) + } + }) + }) + } + + return programs + }, + async channels() { + const channels = [] + const included = [] + const schedules = await fetchSchedules({ date: dayjs() }) + schedules.forEach(schedule => { + if (schedule.channel && included.indexOf(schedule.channel.familyId) < 0) { + included.push(schedule.channel.familyId) + channels.push({ + lang: 'ru', + site_id: schedule.channel.familyId.toString(), + name: schedule.channel.title + }) + } + }) + + return channels + } +} + +async function fetchSchedules({ date, content = null }) { + const schedules = [] + const queues = [] + const fetches = [] + const url = getUrl(date) + + let mainApi + // parse content as schedules and add to queue if more requests is needed + const f = (src, res, headers) => { + if (src) { + fetches.push(src) + } + if (headers) { + parseCookies(headers) + } + const [q, s] = parseContent(res, date) + if (!mainApi) { + mainApi = true + if (caches.region) { + queues.push(getQueue(getUrl(date, caches.region), src)) + } + } + for (const url of q) { + if (fetches.indexOf(url) < 0) { + queues.push(getQueue(url, src)) + } + } + schedules.push(...s) + } + // is main html already fetched? + if (content) { + f(url, content) + } else { + queues.push(getQueue(url, 'https://tv.yandex.ru/')) + } + // fetch all queues + await doFetch(queues, f) + + return schedules +} + +async function fetchPrograms({ schedules, date, channel }) { + const queues = [] + schedules + .filter(schedule => schedule.channel.familyId == channel.site_id) + .forEach(schedule => { + queues.push( + ...schedule.events + .filter(event => date.isSame(event.start, 'day')) + .map(event => getQueue(getUrl(null, caches.region, null, event), 'https://tv.yandex.ru/')) + ) + }) + await doFetch(queues, (queue, res, headers) => { + if (headers) { + parseCookies(headers) + } + // is it a program? + if (res?.program) { + let updated = false + schedules.forEach(schedule => { + schedule.events.forEach(event => { + if (event.channelFamilyId === res.channelFamilyId && event.id === res.id) { + Object.assign(event, res) + updated = true + return true + } + }) + if (updated) { + return true + } + }) + } + }) +} + +function parseContent(content, date, checkOnly = false) { + const queues = [] + const schedules = [] + let valid = false + if (content) { + if (Buffer.isBuffer(content)) { + content = content.toString() + } + // got captcha, its look like our cookies has expired + if ( + content?.type === 'captcha' || + (typeof content === 'string' && content.match(/SmartCaptcha/)) + ) { + throw new Error('Got captcha, please goto https://tv.yandex.ru and update cookies!') + } + if (typeof content === 'object') { + let items + if (content.schedule) { + // fetch next request based on schedule map + if (Array.isArray(content.schedule.scheduleMap)) { + queues.push(...content.schedule.scheduleMap.map(m => getUrl(date, caches.region, m))) + } + // find some schedules? + if (Array.isArray(content.schedule.schedules)) { + items = content.schedule.schedules + } + } + // find another schedules? + if (Array.isArray(content.schedules)) { + items = content.schedules + } + // add programs + if (items && items.length) { + schedules.push(...getSchedules(items)) + } + } else { + // prepare headers for next http request + const [, region] = content.match(/region: '(\d+)'/i) || [null, null] + const [, initialSk] = content.match(/window.__INITIAL_SK__ = (.*);/i) || [null, null] + const [, sessionId] = content.match(/window.__USER_SESSION_ID__ = "(.*)";/i) || [null, null] + const tvSk = initialSk ? JSON.parse(initialSk) : {} + if (region) { + caches.region = region + } + if (tvSk.key) { + headers['X-Tv-Sk'] = tvSk.key + } + if (sessionId) { + headers['X-User-Session-Id'] = sessionId + } + if (checkOnly && region && tvSk.key && sessionId) { + valid = true + } + } + } + + return checkOnly ? valid : [queues, schedules] +} + +function parseCookies(headers) { + if (Array.isArray(headers['set-cookie'])) { + headers['set-cookie'].forEach(cookie => { + const [key, value] = cookie.split('; ')[0].split('=') + if (cookies[key] !== value) { + cookies[key] = value + debug(`Update cookie ${key}=${value}`) + } + }) + } +} + +function getSchedules(schedules) { + return schedules.filter(schedule => schedule.events.length) +} + +function getHeaders(data = {}) { + return Object.assign( + {}, + headers, + { + Cookie: Object.keys(cookies) + .map(cookie => `${cookie}=${cookies[cookie]}`) + .join('; ') + }, + data + ) +} + +function getUrl(date, region = null, page = null, event = null) { + let url = 'https://tv.yandex.ru/' + if (region) { + url += `api/${region}` + } + if (page && page.id !== undefined) { + url += `${url.endsWith('/') ? '' : '/'}main/chunk?page=${page.id}` + } + if (event && event.id !== undefined) { + url += `${url.endsWith('/') ? '' : '/'}event?eventId=${event.id}&programCoId=` + } + if (date) { + url += `${url.indexOf('?') < 0 ? '?' : '&'}date=${date.format('YYYY-MM-DD')}${ + !page ? '&grid=all' : '' + }&period=all-day` + } + if (page && page.id !== undefined && page.offset !== undefined) { + url += `${url.indexOf('?') < 0 ? '?' : '&'}offset=${page.offset}` + } + if (page && page.id !== undefined && page.limit !== undefined) { + url += `${url.indexOf('?') < 0 ? '?' : '&'}limit=${page.limit}` + } + return url +} + +function getQueue(url, referer) { + const data = { + Origin: 'https://tv.yandex.ru' + } + if (referer) { + data['Referer'] = referer + } + if (url.indexOf('api') > 0) { + data['X-Requested-With'] = 'XMLHttpRequest' + } + const headers = getHeaders(data) + return { + url, + params: { headers } + } +} diff --git a/sites/tv.yandex.ru/tv.yandex.ru.test.js b/sites/tv.yandex.ru/tv.yandex.ru.test.js index 9661089f..a9290ac4 100644 --- a/sites/tv.yandex.ru/tv.yandex.ru.test.js +++ b/sites/tv.yandex.ru/tv.yandex.ru.test.js @@ -1,92 +1,92 @@ -const { parser, url, request } = require('./tv.yandex.ru.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('2023-11-26').startOf('d') -const channel = { - site_id: '16', - xmltv_id: 'ChannelOne.ru' -} -axios.get.mockImplementation(url => { - if (url === 'https://tv.yandex.ru/?date=2023-11-26&grid=all&period=all-day') { - return Promise.resolve({ - headers: {}, - data: fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - }) - } - if (url === 'https://tv.yandex.ru/api/120809?date=2023-11-26&grid=all&period=all-day') { - return Promise.resolve({ - headers: {}, - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/schedule.json'))) - }) - } - if ( - url === - 'https://tv.yandex.ru/api/120809/main/chunk?page=0&date=2023-11-26&period=all-day&offset=0&limit=11' - ) { - return Promise.resolve({ - headers: {}, - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/schedule0.json'))) - }) - } - if (url === 'https://tv.yandex.ru/api/120809/event?eventId=217749657&programCoId=') { - return Promise.resolve({ - headers: {}, - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) - }) - } -}) - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://tv.yandex.ru/?date=2023-11-26&grid=all&period=all-day') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - Cookie: - 'i=eIUfSP+/mzQWXcH+Cuz8o1vY+D2K8fhBd6Sj0xvbPZeO4l3cY+BvMp8fFIuM17l6UE1Z5+R2a18lP00ex9iYVJ+VT+c=; ' + - 'spravka=dD0xNzM0MjA0NjM4O2k9MTI1LjE2NC4xNDkuMjAwO0Q9QTVCQ0IyOTI5RDQxNkU5NkEyOTcwMTNDMzZGMDAzNjRDNTFFNDM4QkE2Q0IyOTJDRjhCOTZDRDIzODdBQzk2MzRFRDc5QTk2Qjc2OEI1MUY5MTM5M0QzNkY3OEQ2OUY3OTUwNkQ3RjBCOEJGOEJDMjAwMTQ0RDUwRkFCMDNEQzJFMDI2OEI5OTk5OUJBNEFERUYwOEQ1MjUwQTE0QTI3RDU1MEQwM0U0O3U9MTczNDIwNDYzODUyNDYyNzg1NDtoPTIxNTc0ZTc2MDQ1ZjcwMDBkYmY0NTVkM2Q2ZWMyM2Y1; ' + - 'yandexuid=1197179041732383499; ' + - 'yashr=4682342911732383504; ' + - 'yuidss=1197179041732383499; ' + - 'user_display=824' - }) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const result = (await parser({ content, date, channel })).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2023-11-26T01:35:00.000Z', - stop: '2023-11-26T02:10:00.000Z', - title: 'ПОДКАСТ.ЛАБ. Мелодии моей жизни', - category: 'досуг', - description: - 'Впереди вся ночь и есть о чем поговорить. Фильмы, музыка, любовь, звезды, еда, мода, анекдоты, спорт, деньги, настоящее, будущее - все это в творческом эксперименте.\nЛариса Гузеева читает любовные письма. Леонид Якубович рассказывает, кого не берут в пилоты. Арина Холина - какой секс способен довести до мужа или до развода. Валерий Сюткин на ходу сочиняет песню для Карины Кросс и Вали Карнавал. Дмитрий Дибров дарит новую жизнь любимой "Антропологии". Денис Казанский - все о футболе, хоккее и не только.\n"ПОДКАСТЫ. ЛАБ" - серия подкастов разной тематики, которые невозможно проспать. Интеллектуальные дискуссии после полуночи с самыми компетентными экспертами и актуальными спикерами.' - } - ]) -}) - -it('can handle empty guide', async () => { - const result = await parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./tv.yandex.ru.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('2023-11-26').startOf('d') +const channel = { + site_id: '16', + xmltv_id: 'ChannelOne.ru' +} +axios.get.mockImplementation(url => { + if (url === 'https://tv.yandex.ru/?date=2023-11-26&grid=all&period=all-day') { + return Promise.resolve({ + headers: {}, + data: fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + }) + } + if (url === 'https://tv.yandex.ru/api/120809?date=2023-11-26&grid=all&period=all-day') { + return Promise.resolve({ + headers: {}, + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/schedule.json'))) + }) + } + if ( + url === + 'https://tv.yandex.ru/api/120809/main/chunk?page=0&date=2023-11-26&period=all-day&offset=0&limit=11' + ) { + return Promise.resolve({ + headers: {}, + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/schedule0.json'))) + }) + } + if (url === 'https://tv.yandex.ru/api/120809/event?eventId=217749657&programCoId=') { + return Promise.resolve({ + headers: {}, + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) + }) + } +}) + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://tv.yandex.ru/?date=2023-11-26&grid=all&period=all-day') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + Cookie: + 'i=eIUfSP+/mzQWXcH+Cuz8o1vY+D2K8fhBd6Sj0xvbPZeO4l3cY+BvMp8fFIuM17l6UE1Z5+R2a18lP00ex9iYVJ+VT+c=; ' + + 'spravka=dD0xNzM0MjA0NjM4O2k9MTI1LjE2NC4xNDkuMjAwO0Q9QTVCQ0IyOTI5RDQxNkU5NkEyOTcwMTNDMzZGMDAzNjRDNTFFNDM4QkE2Q0IyOTJDRjhCOTZDRDIzODdBQzk2MzRFRDc5QTk2Qjc2OEI1MUY5MTM5M0QzNkY3OEQ2OUY3OTUwNkQ3RjBCOEJGOEJDMjAwMTQ0RDUwRkFCMDNEQzJFMDI2OEI5OTk5OUJBNEFERUYwOEQ1MjUwQTE0QTI3RDU1MEQwM0U0O3U9MTczNDIwNDYzODUyNDYyNzg1NDtoPTIxNTc0ZTc2MDQ1ZjcwMDBkYmY0NTVkM2Q2ZWMyM2Y1; ' + + 'yandexuid=1197179041732383499; ' + + 'yashr=4682342911732383504; ' + + 'yuidss=1197179041732383499; ' + + 'user_display=824' + }) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const result = (await parser({ content, date, channel })).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2023-11-26T01:35:00.000Z', + stop: '2023-11-26T02:10:00.000Z', + title: 'ПОДКАСТ.ЛАБ. Мелодии моей жизни', + category: 'досуг', + description: + 'Впереди вся ночь и есть о чем поговорить. Фильмы, музыка, любовь, звезды, еда, мода, анекдоты, спорт, деньги, настоящее, будущее - все это в творческом эксперименте.\nЛариса Гузеева читает любовные письма. Леонид Якубович рассказывает, кого не берут в пилоты. Арина Холина - какой секс способен довести до мужа или до развода. Валерий Сюткин на ходу сочиняет песню для Карины Кросс и Вали Карнавал. Дмитрий Дибров дарит новую жизнь любимой "Антропологии". Денис Казанский - все о футболе, хоккее и не только.\n"ПОДКАСТЫ. ЛАБ" - серия подкастов разной тематики, которые невозможно проспать. Интеллектуальные дискуссии после полуночи с самыми компетентными экспертами и актуальными спикерами.' + } + ]) +}) + +it('can handle empty guide', async () => { + const result = await parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__', 'no_content.html'), 'utf8') + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv24.co.uk/tv24.co.uk.config.js b/sites/tv24.co.uk/tv24.co.uk.config.js index 25458228..5c4f6d6d 100644 --- a/sites/tv24.co.uk/tv24.co.uk.config.js +++ b/sites/tv24.co.uk/tv24.co.uk.config.js @@ -1,91 +1,91 @@ -const dayjs = require('dayjs') -const axios = require('axios') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'tv24.co.uk', - days: 2, - url: function ({ channel, date }) { - return `https://tv24.co.uk/x/channel/${channel.site_id}/0/${date.format('YYYY-MM-DD')}` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - let html = await axios - .get('https://tv24.co.uk/x/settings/addremove', { - headers: { - Cookie: 'selectedPostcode=-; selectedProvider=1000193' - } - }) - .then(r => r.data) - .catch(console.log) - let $ = cheerio.load(html) - - let channels = [] - $('li') - .toArray() - .forEach(item => { - const link = $(item).find('img').attr('src') - if (!link || link.includes('ic_channel_default')) return - const [, filename] = link.match(/channels\/(.*)\./) - const site_id = filename.replace('-l', '') - const name = $(item).find('h3').text().trim() - - channels.push({ - lang: 'en', - site_id, - name - }) - }) - - return channels - } -} - -function parseTitle($item) { - return $item('h3').text() -} - -function parseDescription($item) { - return $item('p').text() -} - -function parseStart($item, date) { - const time = $item('.time').text() - - return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD h:mma') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.program').toArray() -} +const dayjs = require('dayjs') +const axios = require('axios') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'tv24.co.uk', + days: 2, + url: function ({ channel, date }) { + return `https://tv24.co.uk/x/channel/${channel.site_id}/0/${date.format('YYYY-MM-DD')}` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + let html = await axios + .get('https://tv24.co.uk/x/settings/addremove', { + headers: { + Cookie: 'selectedPostcode=-; selectedProvider=1000193' + } + }) + .then(r => r.data) + .catch(console.log) + let $ = cheerio.load(html) + + let channels = [] + $('li') + .toArray() + .forEach(item => { + const link = $(item).find('img').attr('src') + if (!link || link.includes('ic_channel_default')) return + const [, filename] = link.match(/channels\/(.*)\./) + const site_id = filename.replace('-l', '') + const name = $(item).find('h3').text().trim() + + channels.push({ + lang: 'en', + site_id, + name + }) + }) + + return channels + } +} + +function parseTitle($item) { + return $item('h3').text() +} + +function parseDescription($item) { + return $item('p').text() +} + +function parseStart($item, date) { + const time = $item('.time').text() + + return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD h:mma') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.program').toArray() +} diff --git a/sites/tv24.co.uk/tv24.co.uk.test.js b/sites/tv24.co.uk/tv24.co.uk.test.js index 60284eaa..72330a99 100644 --- a/sites/tv24.co.uk/tv24.co.uk.test.js +++ b/sites/tv24.co.uk/tv24.co.uk.test.js @@ -1,50 +1,50 @@ -const { parser, url } = require('./tv24.co.uk.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('2022-08-28', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'bbc-two', - xmltv_id: 'BBCTwo.uk' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://tv24.co.uk/x/channel/bbc-two/0/2022-08-28') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-08-28T05:05:00.000Z', - stop: '2022-08-28T06:05:00.000Z', - title: "Gardeners' World", - description: - 'Arit Anderson discovers a paradise garden in Cambridge which has become a focal point for the local community, and Frances Tophill shares the joy of collecting and saving heirloom vegetable seeds on a visit to Pembrokeshire.' - }) - - expect(results[22]).toMatchObject({ - start: '2022-08-29T05:30:00.000Z', - stop: '2022-08-29T06:00:00.000Z', - title: 'Animal Park', - description: - "One of the park's vultures has laid an egg. It is ten years since Longleat had a successfully reared vulture chick, so the keepers send Hamza to find out if the parents are incubating their egg." - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./tv24.co.uk.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('2022-08-28', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'bbc-two', + xmltv_id: 'BBCTwo.uk' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://tv24.co.uk/x/channel/bbc-two/0/2022-08-28') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-08-28T05:05:00.000Z', + stop: '2022-08-28T06:05:00.000Z', + title: "Gardeners' World", + description: + 'Arit Anderson discovers a paradise garden in Cambridge which has become a focal point for the local community, and Frances Tophill shares the joy of collecting and saving heirloom vegetable seeds on a visit to Pembrokeshire.' + }) + + expect(results[22]).toMatchObject({ + start: '2022-08-29T05:30:00.000Z', + stop: '2022-08-29T06:00:00.000Z', + title: 'Animal Park', + description: + "One of the park's vultures has laid an egg. It is ten years since Longleat had a successfully reared vulture chick, so the keepers send Hamza to find out if the parents are incubating their egg." + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv24.se/tv24.se.config.js b/sites/tv24.se/tv24.se.config.js index 76d92535..1b0c83d1 100644 --- a/sites/tv24.se/tv24.se.config.js +++ b/sites/tv24.se/tv24.se.config.js @@ -1,163 +1,163 @@ -const dayjs = require('dayjs') -const axios = require('axios') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'tv24.se', - days: 2, - url: function ({ channel, date }) { - return `https://tv24.se/x/channel/${channel.site_id}/0/${date.format('YYYY-MM-DD')}` - }, - parser: async function ({ content, date }) { - let programs = [] - const items = parseItems(content) - for (let item of items) { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - const details = await loadProgramDetails($item) - programs.push({ - title: parseTitle($item), - description: details.description, - actors: details.actors, - image: details.image, - category: details.category, - sub_title: details.sub_title, - season: details.season, - episode: details.episode, - start, - stop - }) - } - - return programs - }, - async channels() { - let html = await axios - .get('https://tv24.se/x/settings/addremove') - .then(r => r.data) - .catch(console.log) - let $ = cheerio.load(html) - const nums = $('li') - .toArray() - .map(item => $(item).data('channel')) - html = await axios - .get('https://tv24.se', { - headers: { - Cookie: `selectedChannels=${nums.join(',')}` - } - }) - .then(r => r.data) - .catch(console.log) - $ = cheerio.load(html) - const items = $('li.c').toArray() - - return items.map(item => { - const name = $(item).find('h3').text().trim() - const link = $(item).find('.channel').attr('href') - const [, site_id] = link.match(/\/kanal\/(.*)/) || [null, null] - - return { - lang: 'sv', - site_id, - name - } - }) - } -} - -async function loadProgramDetails($item) { - const programId = $item('a').attr('href') - const data = await axios - .get(`https://tv24.se/x${programId}/0/0`) - .then(r => r.data) - .catch(console.error) - if (!data) return Promise.resolve({}) - const $ = cheerio.load(data.contentBefore + data.contentAfter) - - return Promise.resolve({ - image: parseImage($), - actors: parseActors($), - description: parseDescription($), - category: parseCategory($), - sub_title: parseSubTitle($), - season: parseSeason($), - episode: parseEpisode($) - }) -} - -function parseImage($) { - const style = $('.image > .actual').attr('style') - const [, url] = style.match(/background-image: url\('([^']+)'\)/) - - return url -} - -function parseSeason($) { - const [, season] = $('.sub-title') - .text() - .trim() - .match(/Säsong (\d+)/) || [null, ''] - - return parseInt(season) -} - -function parseEpisode($) { - const [, episode] = $('.sub-title') - .text() - .trim() - .match(/Avsnitt (\d+)/) || [null, ''] - - return parseInt(episode) -} - -function parseSubTitle($) { - const [, subtitle] = $('.sub-title').text().trim().split(': ') - - return subtitle -} - -function parseCategory($) { - return $('.extras > dt:contains(Kategori)').next().text().trim().split(' / ') -} - -function parseActors($) { - return $('.cast > li') - .map((i, el) => { - return $(el).find('.name').text().trim() - }) - .get() -} - -function parseDescription($) { - return $('.info > p').text().trim() -} - -function parseTitle($item) { - return $item('h3').text() -} - -function parseStart($item, date) { - const time = $item('.time') - - return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.program').toArray() -} +const dayjs = require('dayjs') +const axios = require('axios') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'tv24.se', + days: 2, + url: function ({ channel, date }) { + return `https://tv24.se/x/channel/${channel.site_id}/0/${date.format('YYYY-MM-DD')}` + }, + parser: async function ({ content, date }) { + let programs = [] + const items = parseItems(content) + for (let item of items) { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + const details = await loadProgramDetails($item) + programs.push({ + title: parseTitle($item), + description: details.description, + actors: details.actors, + image: details.image, + category: details.category, + sub_title: details.sub_title, + season: details.season, + episode: details.episode, + start, + stop + }) + } + + return programs + }, + async channels() { + let html = await axios + .get('https://tv24.se/x/settings/addremove') + .then(r => r.data) + .catch(console.log) + let $ = cheerio.load(html) + const nums = $('li') + .toArray() + .map(item => $(item).data('channel')) + html = await axios + .get('https://tv24.se', { + headers: { + Cookie: `selectedChannels=${nums.join(',')}` + } + }) + .then(r => r.data) + .catch(console.log) + $ = cheerio.load(html) + const items = $('li.c').toArray() + + return items.map(item => { + const name = $(item).find('h3').text().trim() + const link = $(item).find('.channel').attr('href') + const [, site_id] = link.match(/\/kanal\/(.*)/) || [null, null] + + return { + lang: 'sv', + site_id, + name + } + }) + } +} + +async function loadProgramDetails($item) { + const programId = $item('a').attr('href') + const data = await axios + .get(`https://tv24.se/x${programId}/0/0`) + .then(r => r.data) + .catch(console.error) + if (!data) return Promise.resolve({}) + const $ = cheerio.load(data.contentBefore + data.contentAfter) + + return Promise.resolve({ + image: parseImage($), + actors: parseActors($), + description: parseDescription($), + category: parseCategory($), + sub_title: parseSubTitle($), + season: parseSeason($), + episode: parseEpisode($) + }) +} + +function parseImage($) { + const style = $('.image > .actual').attr('style') + const [, url] = style.match(/background-image: url\('([^']+)'\)/) + + return url +} + +function parseSeason($) { + const [, season] = $('.sub-title') + .text() + .trim() + .match(/Säsong (\d+)/) || [null, ''] + + return parseInt(season) +} + +function parseEpisode($) { + const [, episode] = $('.sub-title') + .text() + .trim() + .match(/Avsnitt (\d+)/) || [null, ''] + + return parseInt(episode) +} + +function parseSubTitle($) { + const [, subtitle] = $('.sub-title').text().trim().split(': ') + + return subtitle +} + +function parseCategory($) { + return $('.extras > dt:contains(Kategori)').next().text().trim().split(' / ') +} + +function parseActors($) { + return $('.cast > li') + .map((i, el) => { + return $(el).find('.name').text().trim() + }) + .get() +} + +function parseDescription($) { + return $('.info > p').text().trim() +} + +function parseTitle($item) { + return $item('h3').text() +} + +function parseStart($item, date) { + const time = $item('.time') + + return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.program').toArray() +} diff --git a/sites/tv24.se/tv24.se.test.js b/sites/tv24.se/tv24.se.test.js index bfeeb206..f7535e65 100644 --- a/sites/tv24.se/tv24.se.test.js +++ b/sites/tv24.se/tv24.se.test.js @@ -1,75 +1,75 @@ -const { parser, url } = require('./tv24.se.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('2022-08-26', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'svt1', - xmltv_id: 'SVT1.se' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://tv24.se/x/channel/svt1/0/2022-08-26') -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - - axios.get.mockImplementation(url => { - if (url === 'https://tv24.se/x/b/rh7f40-1hkm/0/0') { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) - }) - } else if (url === 'https://tv24.se/x/b/rh9dhc-1hkm/0/0') { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-08-26T04:00:00.000Z', - stop: '2022-08-26T07:10:00.000Z', - title: 'Morgonstudion', - image: 'https://jrsy.tmsimg.com/assets/p14436175_i_h9_ad.jpg', - description: - 'Dagens viktigaste nyheter och analyser med ständiga uppdateringar. Vi sänder direkt inrikes- och utrikesnyheter inklusive sport, kultur och nöje. Dessutom intervjuer med aktuella gäster. Nyhetssammanfattningar varje kvart med start kl 06.00.', - actors: ['Carolina Neurath', 'Karin Magnusson', 'Pelle Nilsson', 'Ted Wigren'] - }) - - expect(results[33]).toMatchObject({ - start: '2022-08-27T05:20:00.000Z', - stop: '2022-08-27T05:50:00.000Z', - title: 'Uppdrag granskning', - image: 'https://jrsy.tmsimg.com/assets/p22818697_e_h9_aa.jpg', - description: - 'När samtliga sex män frias för ännu en skjutning växer vreden inom polisen. Ökningen av skjutningar i Sverige ligger i topp i Europa - och nu är våldsspiralen på väg mot ett nattsvart rekord. Hur blev Sverige landet där mördare går fria?', - actors: ['Karin Mattisson', 'Ali Fegan'], - category: ['Dokumentär', 'Samhällsfrågor'], - season: 23, - episode: 5, - sub_title: 'Där mördare går fria' - }) -}) - -it('can handle empty guide', async () => { - const result = await parser({ content: '' }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./tv24.se.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('2022-08-26', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'svt1', + xmltv_id: 'SVT1.se' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://tv24.se/x/channel/svt1/0/2022-08-26') +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + + axios.get.mockImplementation(url => { + if (url === 'https://tv24.se/x/b/rh7f40-1hkm/0/0') { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) + }) + } else if (url === 'https://tv24.se/x/b/rh9dhc-1hkm/0/0') { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-08-26T04:00:00.000Z', + stop: '2022-08-26T07:10:00.000Z', + title: 'Morgonstudion', + image: 'https://jrsy.tmsimg.com/assets/p14436175_i_h9_ad.jpg', + description: + 'Dagens viktigaste nyheter och analyser med ständiga uppdateringar. Vi sänder direkt inrikes- och utrikesnyheter inklusive sport, kultur och nöje. Dessutom intervjuer med aktuella gäster. Nyhetssammanfattningar varje kvart med start kl 06.00.', + actors: ['Carolina Neurath', 'Karin Magnusson', 'Pelle Nilsson', 'Ted Wigren'] + }) + + expect(results[33]).toMatchObject({ + start: '2022-08-27T05:20:00.000Z', + stop: '2022-08-27T05:50:00.000Z', + title: 'Uppdrag granskning', + image: 'https://jrsy.tmsimg.com/assets/p22818697_e_h9_aa.jpg', + description: + 'När samtliga sex män frias för ännu en skjutning växer vreden inom polisen. Ökningen av skjutningar i Sverige ligger i topp i Europa - och nu är våldsspiralen på väg mot ett nattsvart rekord. Hur blev Sverige landet där mördare går fria?', + actors: ['Karin Mattisson', 'Ali Fegan'], + category: ['Dokumentär', 'Samhällsfrågor'], + season: 23, + episode: 5, + sub_title: 'Där mördare går fria' + }) +}) + +it('can handle empty guide', async () => { + const result = await parser({ content: '' }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv2go.t-2.net/__data__/content.json b/sites/tv2go.t-2.net/__data__/content.json new file mode 100644 index 00000000..d3bc3208 --- /dev/null +++ b/sites/tv2go.t-2.net/__data__/content.json @@ -0,0 +1,54 @@ +{ + "entries":[ + { + "channelId":1000259, + "startTimestamp":"1637283000000", + "endTimestamp":"1637284500000", + "name":"Dnevnik Slovencev v Italiji", + "nameSingleLine":"Dnevnik Slovencev v Italiji", + "description":"Informativni", + "images":[ + { + "url":"/static/media/img/epg/max_crop/EPG_IMG_2927405.jpg", + "width":1008, + "height":720, + "averageColor":[ + 143, + 147, + 161 + ] + } + ], + "show":{ + "id":51991133, + "title":"Dnevnik Slovencev v Italiji", + "originalTitle":"Dnevnik Slovencev v Italiji", + "shortDescription":"Dnevnik Slovencev v Italiji je informativna oddaja, v kateri novinarji poročajo predvsem o dnevnih dogodkih med Slovenci v Italiji.", + "longDescription":"Pomembno ogledalo vsakdana, v katerem opozarjajo na težave, s katerimi se soočajo, predstavljajo pa tudi pestro kulturno, športno in družbeno življenje slovenske narodne skupnosti. V oddajo so vključene tudi novice iz matične domovine.", + "type":{ + "id":10, + "name":"Show" + }, + "productionFrom":"1609502400000", + "countries":[ + { + "id":"SI", + "name":"Slovenija" + } + ], + "languages":[ + { + "languageId":2, + "name":"Slovenščina" + } + ], + "genres":[ + { + "id":1000002, + "name":"Informativni" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/sites/tv2go.t-2.net/jquery.md5.js b/sites/tv2go.t-2.net/jquery.md5.js index a8f351dd..43518dc6 100644 --- a/sites/tv2go.t-2.net/jquery.md5.js +++ b/sites/tv2go.t-2.net/jquery.md5.js @@ -1,264 +1,264 @@ -/* - * jQuery MD5 Plugin 1.2.1 - * https://github.com/blueimp/jQuery-MD5 - * - * Copyright 2010, Sebastian Tschan - * https://blueimp.net - * - * Licensed under the MIT license: - * http://creativecommons.org/licenses/MIT/ - * - * Based on - * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message - * Digest Algorithm, as defined in RFC 1321. - * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 - * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet - * Distributed under the BSD License - * See http://pajhome.org.uk/crypt/md5 for more info. - */ - -/* - * Add integers, wrapping at 2^32. This uses 16-bit operations internally - * to work around bugs in some JS interpreters. - */ -function safe_add(x, y) { - var lsw = (x & 0xffff) + (y & 0xffff), - msw = (x >> 16) + (y >> 16) + (lsw >> 16) - return (msw << 16) | (lsw & 0xffff) -} - -/* - * Bitwise rotate a 32-bit number to the left. - */ -function bit_rol(num, cnt) { - return (num << cnt) | (num >>> (32 - cnt)) -} - -/* - * These functions implement the four basic operations the algorithm uses. - */ -function md5_cmn(q, a, b, x, s, t) { - return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b) -} -function md5_ff(a, b, c, d, x, s, t) { - return md5_cmn((b & c) | (~b & d), a, b, x, s, t) -} -function md5_gg(a, b, c, d, x, s, t) { - return md5_cmn((b & d) | (c & ~d), a, b, x, s, t) -} -function md5_hh(a, b, c, d, x, s, t) { - return md5_cmn(b ^ c ^ d, a, b, x, s, t) -} -function md5_ii(a, b, c, d, x, s, t) { - return md5_cmn(c ^ (b | ~d), a, b, x, s, t) -} - -/* - * Calculate the MD5 of an array of little-endian words, and a bit length. - */ -function binl_md5(x, len) { - /* append padding */ - x[len >> 5] |= 0x80 << len % 32 - x[(((len + 64) >>> 9) << 4) + 14] = len - - var i, - olda, - oldb, - oldc, - oldd, - a = 1732584193, - b = -271733879, - c = -1732584194, - d = 271733878 - - for (i = 0; i < x.length; i += 16) { - olda = a - oldb = b - oldc = c - oldd = d - - a = md5_ff(a, b, c, d, x[i], 7, -680876936) - d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586) - c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819) - b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330) - a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897) - d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426) - c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341) - b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983) - a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416) - d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417) - c = md5_ff(c, d, a, b, x[i + 10], 17, -42063) - b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162) - a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682) - d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101) - c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290) - b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329) - - a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510) - d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632) - c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713) - b = md5_gg(b, c, d, a, x[i], 20, -373897302) - a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691) - d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083) - c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335) - b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848) - a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438) - d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690) - c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961) - b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501) - a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467) - d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784) - c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473) - b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734) - - a = md5_hh(a, b, c, d, x[i + 5], 4, -378558) - d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463) - c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562) - b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556) - a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060) - d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353) - c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632) - b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640) - a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174) - d = md5_hh(d, a, b, c, x[i], 11, -358537222) - c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979) - b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189) - a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487) - d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835) - c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520) - b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651) - - a = md5_ii(a, b, c, d, x[i], 6, -198630844) - d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415) - c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905) - b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055) - a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571) - d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606) - c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523) - b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799) - a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359) - d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744) - c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380) - b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649) - a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070) - d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379) - c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259) - b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551) - - a = safe_add(a, olda) - b = safe_add(b, oldb) - c = safe_add(c, oldc) - d = safe_add(d, oldd) - } - return [a, b, c, d] -} - -/* - * Convert an array of little-endian words to a string - */ -function binl2rstr(input) { - var i, - output = '' - for (i = 0; i < input.length * 32; i += 8) { - output += String.fromCharCode((input[i >> 5] >>> i % 32) & 0xff) - } - return output -} - -/* - * Convert a raw string to an array of little-endian words - * Characters >255 have their high-byte silently ignored. - */ -function rstr2binl(input) { - var i, - output = [] - output[(input.length >> 2) - 1] = undefined - for (i = 0; i < output.length; i += 1) { - output[i] = 0 - } - for (i = 0; i < input.length * 8; i += 8) { - output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << i % 32 - } - return output -} - -/* - * Calculate the MD5 of a raw string - */ -function rstr_md5(s) { - return binl2rstr(binl_md5(rstr2binl(s), s.length * 8)) -} - -/* - * Calculate the HMAC-MD5, of a key and some data (raw strings) - */ -function rstr_hmac_md5(key, data) { - var i, - bkey = rstr2binl(key), - ipad = [], - opad = [], - hash - ipad[15] = opad[15] = undefined - if (bkey.length > 16) { - bkey = binl_md5(bkey, key.length * 8) - } - for (i = 0; i < 16; i += 1) { - ipad[i] = bkey[i] ^ 0x36363636 - opad[i] = bkey[i] ^ 0x5c5c5c5c - } - hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8) - return binl2rstr(binl_md5(opad.concat(hash), 512 + 128)) -} - -/* - * Convert a raw string to a hex string - */ -function rstr2hex(input) { - var hex_tab = '0123456789abcdef', - output = '', - x, - i - for (i = 0; i < input.length; i += 1) { - x = input.charCodeAt(i) - output += hex_tab.charAt((x >>> 4) & 0x0f) + hex_tab.charAt(x & 0x0f) - } - return output -} - -/* - * Encode a string as utf-8 - */ -function str2rstr_utf8(input) { - return unescape(encodeURIComponent(input)) -} - -/* - * Take string arguments and return either raw or hex encoded strings - */ -function raw_md5(s) { - return rstr_md5(str2rstr_utf8(s)) -} -function hex_md5(s) { - return rstr2hex(raw_md5(s)) -} -function raw_hmac_md5(k, d) { - return rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)) -} -function hex_hmac_md5(k, d) { - return rstr2hex(raw_hmac_md5(k, d)) -} - -module.exports = function (string, key, raw) { - if (!key) { - if (!raw) { - return hex_md5(string) - } else { - return raw_md5(string) - } - } - if (!raw) { - return hex_hmac_md5(key, string) - } else { - return raw_hmac_md5(key, string) - } -} +/* + * jQuery MD5 Plugin 1.2.1 + * https://github.com/blueimp/jQuery-MD5 + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://creativecommons.org/licenses/MIT/ + * + * Based on + * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message + * Digest Algorithm, as defined in RFC 1321. + * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for more info. + */ + +/* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ +function safe_add(x, y) { + var lsw = (x & 0xffff) + (y & 0xffff), + msw = (x >> 16) + (y >> 16) + (lsw >> 16) + return (msw << 16) | (lsw & 0xffff) +} + +/* + * Bitwise rotate a 32-bit number to the left. + */ +function bit_rol(num, cnt) { + return (num << cnt) | (num >>> (32 - cnt)) +} + +/* + * These functions implement the four basic operations the algorithm uses. + */ +function md5_cmn(q, a, b, x, s, t) { + return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b) +} +function md5_ff(a, b, c, d, x, s, t) { + return md5_cmn((b & c) | (~b & d), a, b, x, s, t) +} +function md5_gg(a, b, c, d, x, s, t) { + return md5_cmn((b & d) | (c & ~d), a, b, x, s, t) +} +function md5_hh(a, b, c, d, x, s, t) { + return md5_cmn(b ^ c ^ d, a, b, x, s, t) +} +function md5_ii(a, b, c, d, x, s, t) { + return md5_cmn(c ^ (b | ~d), a, b, x, s, t) +} + +/* + * Calculate the MD5 of an array of little-endian words, and a bit length. + */ +function binl_md5(x, len) { + /* append padding */ + x[len >> 5] |= 0x80 << len % 32 + x[(((len + 64) >>> 9) << 4) + 14] = len + + var i, + olda, + oldb, + oldc, + oldd, + a = 1732584193, + b = -271733879, + c = -1732584194, + d = 271733878 + + for (i = 0; i < x.length; i += 16) { + olda = a + oldb = b + oldc = c + oldd = d + + a = md5_ff(a, b, c, d, x[i], 7, -680876936) + d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586) + c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819) + b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330) + a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897) + d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426) + c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341) + b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983) + a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416) + d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417) + c = md5_ff(c, d, a, b, x[i + 10], 17, -42063) + b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162) + a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682) + d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101) + c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290) + b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329) + + a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510) + d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632) + c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713) + b = md5_gg(b, c, d, a, x[i], 20, -373897302) + a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691) + d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083) + c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335) + b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848) + a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438) + d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690) + c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961) + b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501) + a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467) + d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784) + c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473) + b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734) + + a = md5_hh(a, b, c, d, x[i + 5], 4, -378558) + d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463) + c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562) + b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556) + a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060) + d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353) + c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632) + b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640) + a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174) + d = md5_hh(d, a, b, c, x[i], 11, -358537222) + c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979) + b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189) + a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487) + d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835) + c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520) + b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651) + + a = md5_ii(a, b, c, d, x[i], 6, -198630844) + d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415) + c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905) + b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055) + a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571) + d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606) + c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523) + b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799) + a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359) + d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744) + c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380) + b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649) + a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070) + d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379) + c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259) + b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551) + + a = safe_add(a, olda) + b = safe_add(b, oldb) + c = safe_add(c, oldc) + d = safe_add(d, oldd) + } + return [a, b, c, d] +} + +/* + * Convert an array of little-endian words to a string + */ +function binl2rstr(input) { + var i, + output = '' + for (i = 0; i < input.length * 32; i += 8) { + output += String.fromCharCode((input[i >> 5] >>> i % 32) & 0xff) + } + return output +} + +/* + * Convert a raw string to an array of little-endian words + * Characters >255 have their high-byte silently ignored. + */ +function rstr2binl(input) { + var i, + output = [] + output[(input.length >> 2) - 1] = undefined + for (i = 0; i < output.length; i += 1) { + output[i] = 0 + } + for (i = 0; i < input.length * 8; i += 8) { + output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << i % 32 + } + return output +} + +/* + * Calculate the MD5 of a raw string + */ +function rstr_md5(s) { + return binl2rstr(binl_md5(rstr2binl(s), s.length * 8)) +} + +/* + * Calculate the HMAC-MD5, of a key and some data (raw strings) + */ +function rstr_hmac_md5(key, data) { + var i, + bkey = rstr2binl(key), + ipad = [], + opad = [], + hash + ipad[15] = opad[15] = undefined + if (bkey.length > 16) { + bkey = binl_md5(bkey, key.length * 8) + } + for (i = 0; i < 16; i += 1) { + ipad[i] = bkey[i] ^ 0x36363636 + opad[i] = bkey[i] ^ 0x5c5c5c5c + } + hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8) + return binl2rstr(binl_md5(opad.concat(hash), 512 + 128)) +} + +/* + * Convert a raw string to a hex string + */ +function rstr2hex(input) { + var hex_tab = '0123456789abcdef', + output = '', + x, + i + for (i = 0; i < input.length; i += 1) { + x = input.charCodeAt(i) + output += hex_tab.charAt((x >>> 4) & 0x0f) + hex_tab.charAt(x & 0x0f) + } + return output +} + +/* + * Encode a string as utf-8 + */ +function str2rstr_utf8(input) { + return unescape(encodeURIComponent(input)) +} + +/* + * Take string arguments and return either raw or hex encoded strings + */ +function raw_md5(s) { + return rstr_md5(str2rstr_utf8(s)) +} +function hex_md5(s) { + return rstr2hex(raw_md5(s)) +} +function raw_hmac_md5(k, d) { + return rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)) +} +function hex_hmac_md5(k, d) { + return rstr2hex(raw_hmac_md5(k, d)) +} + +module.exports = function (string, key, raw) { + if (!key) { + if (!raw) { + return hex_md5(string) + } else { + return raw_md5(string) + } + } + if (!raw) { + return hex_hmac_md5(key, string) + } else { + return raw_hmac_md5(key, string) + } +} diff --git a/sites/tv2go.t-2.net/tv2go.t-2.net.channels.xml b/sites/tv2go.t-2.net/tv2go.t-2.net.channels.xml index 3fcab5e1..ac60fc39 100644 --- a/sites/tv2go.t-2.net/tv2go.t-2.net.channels.xml +++ b/sites/tv2go.t-2.net/tv2go.t-2.net.channels.xml @@ -1,338 +1,338 @@ - - - RTS Maribor - TV Veseljak Golica - Discovery Channel - Hayat Plus - AMC - History Channel - History Channel - Disney Channel - Folk Plus - Disney Junior - Alfa TV MAK - MTV 3 - Naša TV - Alfa TV BiH - INFO TV - Animal Planet - Nickelodeon - TV Nakupi - HBO - HBO 2 - HBO 3 - Cinemax - Cinemax 2 - RT Doc - Discovery Science - DTX - ID - Freedom - Filmbox Premium - E! - Pink Serije - Pink Koncert - Pink’n’Roll - Sonce TV - Prva World - Prva Max - Happy Reality - Happy Reality 2 - Prva Files - Prva Kick - Prva Life - Prva Plus - Adria - One Adria - Folx Slovenija - BBC News - Dom TV - Espreso TV - Duck TV - Non Stop - Hit TV - Bloomberg Adria - Arena Sport 1 Premium - Megafon TV - CineStar TV 2 - LH TV - English Club TV - Harmonika TV - GLAM - XXXTazy - Angels - BooB - Capable Hole - Devils Home - Foxy Dolls - MIxxx - Prva TV - BIR TV - KIC TV - Kitchen TV - Mediaset Italia - TgCom24 - R Kanal+ - Cartoon Network - RTV Shqiptar - Europe by Satellite - RTV Atlas - 3SAT - 24Kitchen Adria - 360 Tunebox - Agro TV - Al Jazeera Balkans - Alsat Macedoniae - AMC - Anixe - TV Arena Esport - Arena Fight - Arena Sport 1 - Arena Sport 2 - Arena Sport 3 - Arena Sport 4 - Arte - ATM Kranjska Gora - B92 - Baby TV - Balkan Erotic - Balkanika Music TV - TV Balkan Trip - BBC Earth - BBC First - BHT 1 - BK TV - BN TV 2 - BN Music - Cartoonito - Aktual TV - BRIO - Karousel - CBS Reality - CGTN - Channel One Russia - CineStar TV 1 - Cinestar Action & Thriller - Cinestar Comedy - Cinestar Fantasy - Cinestar Premiere - Cinestar Premiere 2 - Club MTV - CMC - Croatian Music Channel - CNN International - Crime & Investigation Channel - Das Erste (ARD) - Da Vinci - Diva - DM SAT Televizija - Docubox - Dom Kino - Dorcel TV XXX - Duna - Duna World - Dusk - Elta 2 - Elta TV - Epic Drama - ePosavje TV - Erox - Eroxxx - ETV - Euronews - Eurosport (NEM) - Eurosport - Eurosport 2 - Eurosport - EWTN Europe - Exodus - Extreme - Extreme Sports - Fashionbox - Fashion TV - Fastnfunbox - FTV - FenFolk TV - FEN TV - Fightbox - Filmbox Art House - Filmbox Extra - Filmbox Stars - STAR - STAR Crime - STAR Life - STAR Movies - FR2 - France 24 English - France 24 French - Funbox - Gametoon - Gea TV Plus - GOLD TV - TV Zlati zvoki - Happy TV - Hayat - Hayat Folk - Hema - HGTV - H2 - Hot Pleasure - Hot XXL - HRT 1 - HRT 2 - Hustler TV - Hustler TV - Jim Jam - Jugoton TV - Kabel 1 - Kanal 5 - Kanal A - Tring 7 - KINO - Klasik - Koroška TV - M1 - M2 - M5 - Mezzo - Mezzo Live - Milf TV - Minimax - Mreža TV - MTV 1 - MTV 2 - MTV 00s - MTV 80s - MTV 90s - MTV - MTV Hits - MTV Live - Muzika Pervogo - Narodna TV - National Geographic - National Geographic Wild - Net TV - Net XXL - NHK World - Nickelodeon - Nick JR - Nova 24 TV 2 - Nova 24 TV - NTV IC Kakanj - OBN - O Kanal - ORF1 - ORF2 - TV Oron - OTO - OTV - OTV Valentino - PETV - Pink Extra - Pink Film - Pink Folk - Pink Hits - Pink Music - Pink Plus - Pink Reality - Pink SI - Pink World - Pink Zabava - Planet Eva - Planet TV 2 - Planet TV - Play House - POP TV - Pro 7 - Prva - RAI 1 - RAI 2 - RAI 3 - RED xxx - RT - RTK Kosova - RTL 2 HR - RTL - RTL Televizija - RTL2 - RTL Kockica - RTL Living - Super RTL - RTRS - RTS 1 - RTS 2 - RTS - RTVi - Vikom TV - SAT1 - Sci Fi - Servus TV - Sexation - SIP TV - TV Sitel - Sky News - SPORT1 - Šport TV - Šport TV 2 - Šport TV 3 - Festival - Super One - T-2 Info - Telecafe - Telma TV - TLC - TL Novelas Europe - TNT Music - TOP TV - TV Toxic - Trace Sport Stars - Trace Urban - Travel Channel - Travelxp - Travelxp - Tring Action - Tring Shqip - Tring Tring - Tržič TV - TV 3 - TV 8 - 24 Vesti - Arena TV - TV AS - TV Celje - MNE - TV Duga Novi Sad - TVE - TV Galeja - TV IDEA - TV Jadran - KCN - KCN Music - KCN 3 - TV Koper - TV Maribor - TV Miklavž - TV Sarajevo - SLO 1 - SLO 2 - SLO 3 - TV Slon - Vijesti - Vaš kanal - Best TV - Viasat Explore - Viasat History - Viasat Nature - TV1000 - Vitel - Vivid Red - Vivid TV - Tring Vizion+ - VOX - Vremya - VTV Velenje - vŽivo.si - Z1 televizija - ZDF - Zdrava Televizija - Zdrava TV - + + + RTS Maribor + TV Veseljak Golica + Discovery Channel + Hayat Plus + AMC + History Channel + History Channel + Disney Channel + Folk Plus + Disney Junior + Alfa TV MAK + MTV 3 + Naša TV + Alfa TV BiH + INFO TV + Animal Planet + Nickelodeon + TV Nakupi + HBO + HBO 2 + HBO 3 + Cinemax + Cinemax 2 + RT Doc + Discovery Science + DTX + ID + Freedom + Filmbox Premium + E! + Pink Serije + Pink Koncert + Pink’n’Roll + Sonce TV + Prva World + Prva Max + Happy Reality + Happy Reality 2 + Prva Files + Prva Kick + Prva Life + Prva Plus + Adria + One Adria + Folx Slovenija + BBC News + Dom TV + Espreso TV + Duck TV + Non Stop + Hit TV + Bloomberg Adria + Arena Sport 1 Premium + Megafon TV + CineStar TV 2 + LH TV + English Club TV + Harmonika TV + GLAM + XXXTazy + Angels + BooB + Capable Hole + Devils Home + Foxy Dolls + MIxxx + Prva TV + BIR TV + KIC TV + Kitchen TV + Mediaset Italia + TgCom24 + R Kanal+ + Cartoon Network + RTV Shqiptar + Europe by Satellite + RTV Atlas + 3SAT + 24Kitchen Adria + 360 Tunebox + Agro TV + Al Jazeera Balkans + Alsat Macedoniae + AMC + Anixe + TV Arena Esport + Arena Fight + Arena Sport 1 + Arena Sport 2 + Arena Sport 3 + Arena Sport 4 + Arte + ATM Kranjska Gora + B92 + Baby TV + Balkan Erotic + Balkanika Music TV + TV Balkan Trip + BBC Earth + BBC First + BHT 1 + BK TV + BN TV 2 + BN Music + Cartoonito + Aktual TV + BRIO + Karousel + CBS Reality + CGTN + Channel One Russia + CineStar TV 1 + Cinestar Action & Thriller + Cinestar Comedy + Cinestar Fantasy + Cinestar Premiere + Cinestar Premiere 2 + Club MTV + CMC - Croatian Music Channel + CNN International + Crime & Investigation Channel + Das Erste (ARD) + Da Vinci + Diva + DM SAT Televizija + Docubox + Dom Kino + Dorcel TV XXX + Duna + Duna World + Dusk + Elta 2 + Elta TV + Epic Drama + ePosavje TV + Erox + Eroxxx + ETV + Euronews + Eurosport (NEM) + Eurosport + Eurosport 2 + Eurosport + EWTN Europe + Exodus + Extreme + Extreme Sports + Fashionbox + Fashion TV + Fastnfunbox + FTV + FenFolk TV + FEN TV + Fightbox + Filmbox Art House + Filmbox Extra + Filmbox Stars + STAR + STAR Crime + STAR Life + STAR Movies + FR2 + France 24 English + France 24 French + Funbox + Gametoon + Gea TV Plus + GOLD TV + TV Zlati zvoki + Happy TV + Hayat + Hayat Folk + Hema + HGTV + H2 + Hot Pleasure + Hot XXL + HRT 1 + HRT 2 + Hustler TV + Hustler TV + Jim Jam + Jugoton TV + Kabel 1 + Kanal 5 + Kanal A + Tring 7 + KINO + Klasik + Koroška TV + M1 + M2 + M5 + Mezzo + Mezzo Live + Milf TV + Minimax + Mreža TV + MTV 1 + MTV 2 + MTV 00s + MTV 80s + MTV 90s + MTV + MTV Hits + MTV Live + Muzika Pervogo + Narodna TV + National Geographic + National Geographic Wild + Net TV + Net XXL + NHK World + Nickelodeon + Nick JR + Nova 24 TV 2 + Nova 24 TV + NTV IC Kakanj + OBN + O Kanal + ORF1 + ORF2 + TV Oron + OTO + OTV + OTV Valentino + PETV + Pink Extra + Pink Film + Pink Folk + Pink Hits + Pink Music + Pink Plus + Pink Reality + Pink SI + Pink World + Pink Zabava + Planet Eva + Planet TV 2 + Planet TV + Play House + POP TV + Pro 7 + Prva + RAI 1 + RAI 2 + RAI 3 + RED xxx + RT + RTK Kosova + RTL 2 HR + RTL + RTL Televizija + RTL2 + RTL Kockica + RTL Living + Super RTL + RTRS + RTS 1 + RTS 2 + RTS + RTVi + Vikom TV + SAT1 + Sci Fi + Servus TV + Sexation + SIP TV + TV Sitel + Sky News + SPORT1 + Šport TV + Šport TV 2 + Šport TV 3 + Festival + Super One + T-2 Info + Telecafe + Telma TV + TLC + TL Novelas Europe + TNT Music + TOP TV + TV Toxic + Trace Sport Stars + Trace Urban + Travel Channel + Travelxp 4K + Travelxp + Tring Action + Tring Shqip + Tring Tring + Tržič TV + TV 3 + TV 8 + 24 Vesti + Arena TV + TV AS + TV Celje + MNE + TV Duga Novi Sad + TVE + TV Galeja + TV IDEA + TV Jadran + KCN + KCN Music + KCN 3 + TV Koper + TV Maribor + TV Miklavž + TV Sarajevo + SLO 1 + SLO 2 + SLO 3 + TV Slon + Vijesti + Vaš kanal + Best TV + Viasat Explore + Viasat History + Viasat Nature + TV1000 + Vitel + Vivid Red + Vivid TV + Tring Vizion+ + VOX + Vremya + VTV Velenje + vŽivo.si + Z1 televizija + ZDF + Zdrava Televizija + Zdrava TV + diff --git a/sites/tv2go.t-2.net/tv2go.t-2.net.config.js b/sites/tv2go.t-2.net/tv2go.t-2.net.config.js index 2fc449f3..bb5ea983 100644 --- a/sites/tv2go.t-2.net/tv2go.t-2.net.config.js +++ b/sites/tv2go.t-2.net/tv2go.t-2.net.config.js @@ -1,126 +1,126 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const md5 = require('./jquery.md5') - -const API = { - locale: 'sl-SI', - version: '9.4', - format: 'json', - uuid: '464830403846070', - token: '6dace810-55d5-11e3-949a-0800200c9a66' -} - -const config = { - site: 'tv2go.t-2.net', - days: 2, - url({ date, channel }) { - const data = config.request.data({ date, channel }) - const endpoint = 'client/tv/getEpg' - const hash = generateHash(data, endpoint) - - return `https://tv2go.t-2.net/Catherine/api/${API.version}/${API.format}/${API.uuid}/${hash}/${endpoint}` - }, - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - data({ date, channel }) { - const channelId = parseInt(channel.site_id) - - return { - locale: API.locale, - channelId: [channelId], - startTime: date.valueOf(), - endTime: date.add(1, 'd').valueOf(), - imageInfo: [{ height: 500, width: 1100 }], - includeBookmarks: false, - includeShow: true - } - } - }, - parser({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.name, - category: parseCategory(item), - description: parseDescription(item), - image: parseImage(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const data = { - locale: API.locale, - type: 'TV', - imageInfo: [{ type: 'DARK', height: 70, width: 98 }] - } - const endpoint = 'client/channels/list' - const hash = generateHash(data, endpoint) - const response = await axios - .post( - `https://tv2go.t-2.net/Catherine/api/${API.version}/${API.format}/${API.uuid}/${hash}/${endpoint}`, - data, - { - headers: { - 'Content-Type': 'application/json' - } - } - ) - .catch(console.log) - - return response.data.channels.map(item => { - return { - lang: 'sl', - site_id: item.id, - name: item.name - } - }) - } -} - -function parseStart(item) { - return dayjs(parseInt(item.startTimestamp)) -} - -function parseStop(item) { - return dayjs(parseInt(item.endTimestamp)) -} - -function parseImage(item) { - return item.images && item.images[0] ? `https://tv2go.t-2.net${item.images[0].url}` : null -} - -function parseCategory(item) { - return item.show && Array.isArray(item.show.genres) ? item.show.genres.map(c => c.name) : [] -} - -function parseDescription(item) { - return item.show ? item.show.shortDescription : null -} - -function parseItems(content) { - let data - try { - data = JSON.parse(content) - } catch { - return [] - } - if (!data || !Array.isArray(data.entries)) return [] - - return data.entries -} - -function generateHash(data, endpoint) { - const salt = `${API.token}${API.version}${API.format}${API.uuid}` - - return md5(salt + endpoint + JSON.stringify(data)) -} - -module.exports = config +const axios = require('axios') +const dayjs = require('dayjs') +const md5 = require('./jquery.md5') + +const API = { + locale: 'sl-SI', + version: '9.4', + format: 'json', + uuid: '464830403846070', + token: '6dace810-55d5-11e3-949a-0800200c9a66' +} + +const config = { + site: 'tv2go.t-2.net', + days: 2, + url({ date, channel }) { + const data = config.request.data({ date, channel }) + const endpoint = 'client/tv/getEpg' + const hash = generateHash(data, endpoint) + + return `https://tv2go.t-2.net/Catherine/api/${API.version}/${API.format}/${API.uuid}/${hash}/${endpoint}` + }, + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + data({ date, channel }) { + const channelId = parseInt(channel.site_id) + + return { + locale: API.locale, + channelId: [channelId], + startTime: date.valueOf(), + endTime: date.add(1, 'd').valueOf(), + imageInfo: [{ height: 500, width: 1100 }], + includeBookmarks: false, + includeShow: true + } + } + }, + parser({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.name, + category: parseCategory(item), + description: parseDescription(item), + image: parseImage(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const data = { + locale: API.locale, + type: 'TV', + imageInfo: [{ type: 'DARK', height: 70, width: 98 }] + } + const endpoint = 'client/channels/list' + const hash = generateHash(data, endpoint) + const response = await axios + .post( + `https://tv2go.t-2.net/Catherine/api/${API.version}/${API.format}/${API.uuid}/${hash}/${endpoint}`, + data, + { + headers: { + 'Content-Type': 'application/json' + } + } + ) + .catch(console.log) + + return response.data.channels.map(item => { + return { + lang: 'sl', + site_id: item.id, + name: item.name + } + }) + } +} + +function parseStart(item) { + return dayjs(parseInt(item.startTimestamp)) +} + +function parseStop(item) { + return dayjs(parseInt(item.endTimestamp)) +} + +function parseImage(item) { + return item.images && item.images[0] ? `https://tv2go.t-2.net${item.images[0].url}` : null +} + +function parseCategory(item) { + return item.show && Array.isArray(item.show.genres) ? item.show.genres.map(c => c.name) : [] +} + +function parseDescription(item) { + return item.show ? item.show.shortDescription : null +} + +function parseItems(content) { + let data + try { + data = JSON.parse(content) + } catch { + return [] + } + if (!data || !Array.isArray(data.entries)) return [] + + return data.entries +} + +function generateHash(data, endpoint) { + const salt = `${API.token}${API.version}${API.format}${API.uuid}` + + return md5(salt + endpoint + JSON.stringify(data)) +} + +module.exports = config diff --git a/sites/tv2go.t-2.net/tv2go.t-2.net.test.js b/sites/tv2go.t-2.net/tv2go.t-2.net.test.js index 23f82839..ea463897 100644 --- a/sites/tv2go.t-2.net/tv2go.t-2.net.test.js +++ b/sites/tv2go.t-2.net/tv2go.t-2.net.test.js @@ -1,67 +1,68 @@ -const { parser, url, request } = require('./tv2go.t-2.net.config.js') -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('2021-11-19', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1000259', - xmltv_id: 'TVSlovenija1.si' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://tv2go.t-2.net/Catherine/api/9.4/json/464830403846070/d79cf4dc84f2131689f426956b8d40de/client/tv/getEpg' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/json' - }) -}) - -it('can generate valid request data', () => { - expect(request.data({ date, channel })).toMatchObject({ - locale: 'sl-SI', - channelId: [1000259], - startTime: 1637280000000, - endTime: 1637366400000, - imageInfo: [{ height: 500, width: 1100 }], - includeBookmarks: false, - includeShow: true - }) -}) - -it('can parse response', () => { - const content = - '{"entries":[{"channelId":1000259,"startTimestamp":"1637283000000","endTimestamp":"1637284500000","name":"Dnevnik Slovencev v Italiji","nameSingleLine":"Dnevnik Slovencev v Italiji","description":"Informativni","images":[{"url":"/static/media/img/epg/max_crop/EPG_IMG_2927405.jpg","width":1008,"height":720,"averageColor":[143,147,161]}],"show":{"id":51991133,"title":"Dnevnik Slovencev v Italiji","originalTitle":"Dnevnik Slovencev v Italiji","shortDescription":"Dnevnik Slovencev v Italiji je informativna oddaja, v kateri novinarji poročajo predvsem o dnevnih dogodkih med Slovenci v Italiji.","longDescription":"Pomembno ogledalo vsakdana, v katerem opozarjajo na težave, s katerimi se soočajo, predstavljajo pa tudi pestro kulturno, športno in družbeno življenje slovenske narodne skupnosti. V oddajo so vključene tudi novice iz matične domovine.","type":{"id":10,"name":"Show"},"productionFrom":"1609502400000","countries":[{"id":"SI","name":"Slovenija"}],"languages":[{"languageId":2,"name":"Slovenščina"}],"genres":[{"id":1000002,"name":"Informativni"}]}}]}' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-19T00:50:00.000Z', - stop: '2021-11-19T01:15:00.000Z', - title: 'Dnevnik Slovencev v Italiji', - category: ['Informativni'], - description: - 'Dnevnik Slovencev v Italiji je informativna oddaja, v kateri novinarji poročajo predvsem o dnevnih dogodkih med Slovenci v Italiji.', - image: 'https://tv2go.t-2.net/static/media/img/epg/max_crop/EPG_IMG_2927405.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: 'Invalid API client identifier' - }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./tv2go.t-2.net.config.js') +const dayjs = require('dayjs') +const fs = require('fs') +const path = require('path') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-19', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1000259', + xmltv_id: 'TVSlovenija1.si' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://tv2go.t-2.net/Catherine/api/9.4/json/464830403846070/d79cf4dc84f2131689f426956b8d40de/client/tv/getEpg' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/json' + }) +}) + +it('can generate valid request data', () => { + expect(request.data({ date, channel })).toMatchObject({ + locale: 'sl-SI', + channelId: [1000259], + startTime: 1637280000000, + endTime: 1637366400000, + imageInfo: [{ height: 500, width: 1100 }], + includeBookmarks: false, + includeShow: true + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.json'), 'utf8') + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-19T00:50:00.000Z', + stop: '2021-11-19T01:15:00.000Z', + title: 'Dnevnik Slovencev v Italiji', + category: ['Informativni'], + description: + 'Dnevnik Slovencev v Italiji je informativna oddaja, v kateri novinarji poročajo predvsem o dnevnih dogodkih med Slovenci v Italiji.', + image: 'https://tv2go.t-2.net/static/media/img/epg/max_crop/EPG_IMG_2927405.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: 'Invalid API client identifier' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvarenasport.com/tvarenasport.com.config.js b/sites/tvarenasport.com/tvarenasport.com.config.js index c4585739..fd6d4a97 100644 --- a/sites/tvarenasport.com/tvarenasport.com.config.js +++ b/sites/tvarenasport.com/tvarenasport.com.config.js @@ -1,102 +1,102 @@ -const cheerio = require('cheerio') -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') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -const CHANNEL_LOGO_REGEX = /chanel-([\w-]+?)\.png/ -const TIMEZONE = 'Europe/Belgrade' - -module.exports = { - site: 'tvarenasport.com', - tz: TIMEZONE, - lang: 'sr', - days: 2, - request: { - cache: { - ttl: 24 * 60 * 60 * 1000 // 1 day - } - }, - url: 'https://www.tvarenasport.com/tv-scheme', - parser({ content, channel, date }) { - const programs = [] - const expectedDate = date.format('YYYY-MM-DD') - const $ = cheerio.load(content) - - $('.tv-scheme-chanel').each((_, el) => { - const $ch = $(el) - const logo = $ch.find('.tv-scheme-chanel-header img').attr('src') || '' - const m = logo.match(CHANNEL_LOGO_REGEX) - if (!m || m[1] !== channel.site_id) return - const dates = $ch.find('.tv-scheme-days a').map((i, d) => { - const t = $(d).find('span:nth-child(3)').text().trim() - return dayjs(`${t}${date.year()}`, 'DD.MM.YYYY') - }).get() - const startIdx = dates.findIndex(d => d.format('YYYY-MM-DD') === expectedDate) - if (startIdx === -1) return - const sliders = $ch.find('.tv-scheme-new-slider-item') - const slider = sliders.eq(startIdx) - if (!slider.length) return - let entries = parseSchedules($, slider, dates[startIdx]) - entries.forEach((e, i) => { - const nxt = entries[i + 1] - e.stop = nxt - ? nxt.start - : dayjs.tz(`${expectedDate} 23:59`, 'YYYY-MM-DD HH:mm', TIMEZONE) - }) - programs.push(...entries) - }) - return programs - }, - - async channels() { - const data = await axios.get(this.url).then(r => r.data).catch(console.error) - if (!data) return [] - const $ = cheerio.load(data) - return $('.tv-scheme-chanel-header img') - .map((_, img) => { - const src = $(img).attr('src') || '' - const m = src.match(CHANNEL_LOGO_REGEX) - if (!m) return null - const id = m[1] - const displayName = getDisplayName(id) - const xmltvId = displayName.replaceAll(' ', '').replace(/Serbia$/, '.rs') - const logourl = `https://www.${this.site}${src}` - return { site_id: id, lang: this.lang, xmltv_id: xmltvId, name: displayName, logo: logourl } - }) - .get() - } -} - -function getDisplayName(id) { - const template = name => `Arena Sport ${name} Serbia` - let m - if ((m = /^0*(\d+)$/.exec(id))) return template(m[1]) - if ((m = /^a+(\d+)p$/.exec(id))) return template(`${m[1]} Premium`) - const formattedId = id.replace(/^a-/, '').replace(/^./, c => c.toUpperCase()) - return template(formattedId) -} - -function parseSchedules($, $slider, date) { - return $slider - .find('.slider-content') - .map((_, el) => parseSchedule($(el), date)) - .get() -} - -function parseSchedule($s, date) { - const time = $s.find('.slider-content-top span').text().trim() - const start = dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', TIMEZONE) - const sport = $s.find('.slider-content-middle span').text().trim() - const titleText = $s.find('.slider-content-bottom p').text().trim() - const league = $s.find('.slider-content-bottom span') - .not('.live-title, .blob-text, .blob-border, .blob').first().text().trim() - const isLive = $s.find('.blob-text').text().trim().toLowerCase() === 'uživo' - const title = (isLive ? '(Uživo) ' : '') + (league ? `${league}: ${titleText}` : titleText) - return { title, category: sport, start } +const cheerio = require('cheerio') +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') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +const CHANNEL_LOGO_REGEX = /chanel-([\w-]+?)\.png/ +const TIMEZONE = 'Europe/Belgrade' + +module.exports = { + site: 'tvarenasport.com', + tz: TIMEZONE, + lang: 'sr', + days: 2, + request: { + cache: { + ttl: 24 * 60 * 60 * 1000 // 1 day + } + }, + url: 'https://www.tvarenasport.com/tv-scheme', + parser({ content, channel, date }) { + const programs = [] + const expectedDate = date.format('YYYY-MM-DD') + const $ = cheerio.load(content) + + $('.tv-scheme-chanel').each((_, el) => { + const $ch = $(el) + const logo = $ch.find('.tv-scheme-chanel-header img').attr('src') || '' + const m = logo.match(CHANNEL_LOGO_REGEX) + if (!m || m[1] !== channel.site_id) return + const dates = $ch.find('.tv-scheme-days a').map((i, d) => { + const t = $(d).find('span:nth-child(3)').text().trim() + return dayjs(`${t}${date.year()}`, 'DD.MM.YYYY') + }).get() + const startIdx = dates.findIndex(d => d.format('YYYY-MM-DD') === expectedDate) + if (startIdx === -1) return + const sliders = $ch.find('.tv-scheme-new-slider-item') + const slider = sliders.eq(startIdx) + if (!slider.length) return + let entries = parseSchedules($, slider, dates[startIdx]) + entries.forEach((e, i) => { + const nxt = entries[i + 1] + e.stop = nxt + ? nxt.start + : dayjs.tz(`${expectedDate} 23:59`, 'YYYY-MM-DD HH:mm', TIMEZONE) + }) + programs.push(...entries) + }) + return programs + }, + + async channels() { + const data = await axios.get(this.url).then(r => r.data).catch(console.error) + if (!data) return [] + const $ = cheerio.load(data) + return $('.tv-scheme-chanel-header img') + .map((_, img) => { + const src = $(img).attr('src') || '' + const m = src.match(CHANNEL_LOGO_REGEX) + if (!m) return null + const id = m[1] + const displayName = getDisplayName(id) + const xmltvId = displayName.replaceAll(' ', '').replace(/Serbia$/, '.rs') + const logourl = `https://www.${this.site}${src}` + return { site_id: id, lang: this.lang, xmltv_id: xmltvId, name: displayName, logo: logourl } + }) + .get() + } +} + +function getDisplayName(id) { + const template = name => `Arena Sport ${name} Serbia` + let m + if ((m = /^0*(\d+)$/.exec(id))) return template(m[1]) + if ((m = /^a+(\d+)p$/.exec(id))) return template(`${m[1]} Premium`) + const formattedId = id.replace(/^a-/, '').replace(/^./, c => c.toUpperCase()) + return template(formattedId) +} + +function parseSchedules($, $slider, date) { + return $slider + .find('.slider-content') + .map((_, el) => parseSchedule($(el), date)) + .get() +} + +function parseSchedule($s, date) { + const time = $s.find('.slider-content-top span').text().trim() + const start = dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', TIMEZONE) + const sport = $s.find('.slider-content-middle span').text().trim() + const titleText = $s.find('.slider-content-bottom p').text().trim() + const league = $s.find('.slider-content-bottom span') + .not('.live-title, .blob-text, .blob-border, .blob').first().text().trim() + const isLive = $s.find('.blob-text').text().trim().toLowerCase() === 'uživo' + const title = (isLive ? '(Uživo) ' : '') + (league ? `${league}: ${titleText}` : titleText) + return { title, category: sport, start } } \ No newline at end of file diff --git a/sites/tvarenasport.com/tvarenasport.com.test.js b/sites/tvarenasport.com/tvarenasport.com.test.js index 227be736..cd8794bd 100644 --- a/sites/tvarenasport.com/tvarenasport.com.test.js +++ b/sites/tvarenasport.com/tvarenasport.com.test.js @@ -1,50 +1,50 @@ -const { parser, url } = require('./tvarenasport.com.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-07-30', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'a1p', - xmltv_id: 'ArenaSportPremium1.rs' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.tvarenasport.com/tv-scheme') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.html')) - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result.length).toBe(13) - expect(result[4]).toMatchObject({ - start: '2025-07-30T08:00:00.000Z', - stop: '2025-07-30T09:00:00.000Z', - title: 'UEFA LIGA ŠAMPIONA: Liga Šampiona: Pregled sezone', - category: 'Fudbal' - }) - expect(result[6]).toMatchObject({ - start: '2025-07-30T11:00:00.000Z', - stop: '2025-07-30T13:00:00.000Z', - title: '(Uživo) PRIJATELJSKE UTAKMICE: K League - Newcastle Utd' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./tvarenasport.com.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-07-30', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'a1p', + xmltv_id: 'ArenaSportPremium1.rs' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.tvarenasport.com/tv-scheme') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.html')) + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result.length).toBe(13) + expect(result[4]).toMatchObject({ + start: '2025-07-30T08:00:00.000Z', + stop: '2025-07-30T09:00:00.000Z', + title: 'UEFA LIGA ŠAMPIONA: Liga Šampiona: Pregled sezone', + category: 'Fudbal' + }) + expect(result[6]).toMatchObject({ + start: '2025-07-30T11:00:00.000Z', + stop: '2025-07-30T13:00:00.000Z', + title: '(Uživo) PRIJATELJSKE UTAKMICE: K League - Newcastle Utd' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvarenasport.hr/tvarenasport.hr.config.js b/sites/tvarenasport.hr/tvarenasport.hr.config.js index c5ae1743..30941e44 100644 --- a/sites/tvarenasport.hr/tvarenasport.hr.config.js +++ b/sites/tvarenasport.hr/tvarenasport.hr.config.js @@ -1,132 +1,132 @@ -const cheerio = require('cheerio') -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') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'tvarenasport.hr', - tz: 'Europe/Budapest', - lang: 'hr', - url: 'https://tvarenaprogram.com/live/v2/hr', - days: 2, - request: { - cache: { - ttl: 24 * 60 * 60 * 1000 // 1 day - } - }, - parser({ content, channel, date }) { - const programs = [] - const expectedDate = date.format('YYYY-MM-DD') - if (content) { - const dates = [] - const $ = cheerio.load(content) - const parent = $( - `.tv-scheme-chanel-header img[src*="chanel-${channel.site_id}.png"]` - ).parents('div') - parent - .siblings('.tv-scheme-days') - .find('a') - .toArray() - .forEach(el => { - const a = $(el) - const dt = a.find('span:nth-child(3)').text() - dates.push(dayjs(dt + date.year(), 'DD.MM.YYYY')) - }) - parent - .siblings('.tv-scheme-new-slider-wrapper') - .find('.tv-scheme-new-slider-item') - .toArray() - .forEach((el, i) => { - programs.push(...parseSchedules($(el), dates[i], module.exports.tz)) - }) - programs.forEach((s, i) => { - if (i < programs.length - 2) { - s.stop = programs[i + 1].start - } else { - s.stop = s.start.startOf('d').add(1, 'd') - } - }) - } - - return programs.filter( - p => - p.start.format('YYYY-MM-DD') === expectedDate || - p.stop.format('YYYY-MM-DD') === expectedDate - ) - }, - async channels() { - const channels = [] - const data = await axios - .get(this.url) - .then(r => r.data) - .catch(console.error) - - if (data) { - // channel naming rule - const names = id => { - let match = id.match(/^\d+$/) - if (match) { - return `Arena Sport ${parseInt(id)}` - } - match = id.match(/^\d/) - if (match) { - return `Arena Sport ${id}` - } - match = id.match(/^a(\d+)(p)?/) - if (match) { - return `Arena ${parseInt(match[1])}${match[2] === 'p' ? ' Premium' : ''}` - } - return `Arena ${id}` - } - const $ = cheerio.load(data) - const items = $('.tv-scheme-chanel-header img').toArray() - for (const item of items) { - const [, id] = $(item) - .attr('src') - .match(/chanel-([a-z0-9]+)\.png/) || [null, null] - if (id) { - channels.push({ - lang: this.lang, - site_id: id, - name: names(id) - }) - } - } - } - - return channels - } -} - -function parseSchedules($s, date, tz) { - const schedules = [] - const $ = $s._make - $s.find('.slider-content') - .toArray() - .forEach(el => { - schedules.push(parseSchedule($(el), date, tz)) - }) - - return schedules -} - -function parseSchedule($s, date, tz) { - const time = $s.find('.slider-content-top span').text() - const start = dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', tz) - const category = $s.find('.slider-content-middle span').text() - const title = $s.find('.slider-content-bottom p').text() - const description = $s.find('.slider-content-bottom span:first').text() - - return { - title: description ? description : title, - description: description ? title : description, - category, - start - } -} +const cheerio = require('cheerio') +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') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'tvarenasport.hr', + tz: 'Europe/Budapest', + lang: 'hr', + url: 'https://tvarenaprogram.com/live/v2/hr', + days: 2, + request: { + cache: { + ttl: 24 * 60 * 60 * 1000 // 1 day + } + }, + parser({ content, channel, date }) { + const programs = [] + const expectedDate = date.format('YYYY-MM-DD') + if (content) { + const dates = [] + const $ = cheerio.load(content) + const parent = $( + `.tv-scheme-chanel-header img[src*="chanel-${channel.site_id}.png"]` + ).parents('div') + parent + .siblings('.tv-scheme-days') + .find('a') + .toArray() + .forEach(el => { + const a = $(el) + const dt = a.find('span:nth-child(3)').text() + dates.push(dayjs(dt + date.year(), 'DD.MM.YYYY')) + }) + parent + .siblings('.tv-scheme-new-slider-wrapper') + .find('.tv-scheme-new-slider-item') + .toArray() + .forEach((el, i) => { + programs.push(...parseSchedules($(el), dates[i], module.exports.tz)) + }) + programs.forEach((s, i) => { + if (i < programs.length - 2) { + s.stop = programs[i + 1].start + } else { + s.stop = s.start.startOf('d').add(1, 'd') + } + }) + } + + return programs.filter( + p => + p.start.format('YYYY-MM-DD') === expectedDate || + p.stop.format('YYYY-MM-DD') === expectedDate + ) + }, + async channels() { + const channels = [] + const data = await axios + .get(this.url) + .then(r => r.data) + .catch(console.error) + + if (data) { + // channel naming rule + const names = id => { + let match = id.match(/^\d+$/) + if (match) { + return `Arena Sport ${parseInt(id)}` + } + match = id.match(/^\d/) + if (match) { + return `Arena Sport ${id}` + } + match = id.match(/^a(\d+)(p)?/) + if (match) { + return `Arena ${parseInt(match[1])}${match[2] === 'p' ? ' Premium' : ''}` + } + return `Arena ${id}` + } + const $ = cheerio.load(data) + const items = $('.tv-scheme-chanel-header img').toArray() + for (const item of items) { + const [, id] = $(item) + .attr('src') + .match(/chanel-([a-z0-9]+)\.png/) || [null, null] + if (id) { + channels.push({ + lang: this.lang, + site_id: id, + name: names(id) + }) + } + } + } + + return channels + } +} + +function parseSchedules($s, date, tz) { + const schedules = [] + const $ = $s._make + $s.find('.slider-content') + .toArray() + .forEach(el => { + schedules.push(parseSchedule($(el), date, tz)) + }) + + return schedules +} + +function parseSchedule($s, date, tz) { + const time = $s.find('.slider-content-top span').text() + const start = dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', tz) + const category = $s.find('.slider-content-middle span').text() + const title = $s.find('.slider-content-bottom p').text() + const description = $s.find('.slider-content-bottom span:first').text() + + return { + title: description ? description : title, + description: description ? title : description, + category, + start + } +} diff --git a/sites/tvarenasport.hr/tvarenasport.hr.test.js b/sites/tvarenasport.hr/tvarenasport.hr.test.js index 9358952c..c7516c2d 100644 --- a/sites/tvarenasport.hr/tvarenasport.hr.test.js +++ b/sites/tvarenasport.hr/tvarenasport.hr.test.js @@ -1,53 +1,53 @@ -const { parser, url } = require('./tvarenasport.hr.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('2024-12-07', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '01', - xmltv_id: 'ArenaSport1.hr' -} - -it('can generate valid url', () => { - expect(url).toBe('https://tvarenaprogram.com/live/v2/hr') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.html')) - const result = parser({ channel, date, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result.length).toBe(15) - expect(result[0]).toMatchObject({ - start: '2024-12-07T00:00:00.000Z', - stop: '2024-12-07T00:30:00.000Z', - title: 'MAGAZIN', - description: 'NBA ACTION', - category: 'Košarka' - }) - expect(result[4]).toMatchObject({ - start: '2024-12-07T06:00:00.000Z', - stop: '2024-12-07T07:30:00.000Z', - title: 'EHF LIGA PRVAKA', - description: 'DINAMO BUKUREŠT - PSG', - category: 'Rukomet' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./tvarenasport.hr.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('2024-12-07', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '01', + xmltv_id: 'ArenaSport1.hr' +} + +it('can generate valid url', () => { + expect(url).toBe('https://tvarenaprogram.com/live/v2/hr') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.html')) + const result = parser({ channel, date, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result.length).toBe(15) + expect(result[0]).toMatchObject({ + start: '2024-12-07T00:00:00.000Z', + stop: '2024-12-07T00:30:00.000Z', + title: 'MAGAZIN', + description: 'NBA ACTION', + category: 'Košarka' + }) + expect(result[4]).toMatchObject({ + start: '2024-12-07T06:00:00.000Z', + stop: '2024-12-07T07:30:00.000Z', + title: 'EHF LIGA PRVAKA', + description: 'DINAMO BUKUREŠT - PSG', + category: 'Rukomet' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvcesoir.fr/__data__/no_content.html b/sites/tvcesoir.fr/__data__/no_content.html new file mode 100644 index 00000000..6fedfd4c --- /dev/null +++ b/sites/tvcesoir.fr/__data__/no_content.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sites/tvcesoir.fr/tvcesoir.fr.config.js b/sites/tvcesoir.fr/tvcesoir.fr.config.js index 306f819f..5a063677 100644 --- a/sites/tvcesoir.fr/tvcesoir.fr.config.js +++ b/sites/tvcesoir.fr/tvcesoir.fr.config.js @@ -1,99 +1,99 @@ -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: 'tvcesoir.fr', - days: 2, - url: function ({ date, channel }) { - return `https://www.tvcesoir.fr/programme-tv/programme/chaine/${ - channel.site_id - }.html?dt=${date.format('YYYY-MM-DD')}` - }, - parser: function ({ content, date, channel }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date, channel) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - const _ = require('lodash') - - const providers = ['-1', '-2', '-3', '-4', '-5'] - - const channels = [] - for (let provider of providers) { - const data = await axios - .post('https://www.tvcesoir.fr/guide/schedule', null, { - params: { - provider, - region: 'France', - TVperiod: 'Night', - date: dayjs().format('YYYY-MM-DD'), - st: 0, - u_time: 2155, - is_mobile: 1 - } - }) - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(data) - $('.channelname').each((i, el) => { - const name = $(el).find('center > a:eq(1)').text() - const url = $(el).find('center > a:eq(1)').attr('href') - const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/) - - channels.push({ - lang: 'fr', - name, - site_id: `${number}/${slug}` - }) - }) - } - - return _.uniqBy(channels, 'site_id') - } -} - -function parseStart($item, date) { - const timeString = $item('td:eq(0)').text().trim() - const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` - - return dayjs.tz(dateString, 'YYYY-MM-DD HH[h]mm', 'Europe/Rome') -} - -function parseTitle($item) { - return $item('td:eq(1)').text().trim() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('table.table > tbody > tr').toArray() -} +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 uniqBy = require('lodash.uniqby') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'tvcesoir.fr', + days: 2, + url: function ({ date, channel }) { + return `https://www.tvcesoir.fr/programme-tv/programme/chaine/${ + channel.site_id + }.html?dt=${date.format('YYYY-MM-DD')}` + }, + parser: function ({ content, date, channel }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date, channel) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const axios = require('axios') + + const providers = ['-1', '-2', '-3', '-4', '-5'] + + const channels = [] + for (let provider of providers) { + const data = await axios + .post('https://www.tvcesoir.fr/guide/schedule', null, { + params: { + provider, + region: 'France', + TVperiod: 'Night', + date: dayjs().format('YYYY-MM-DD'), + st: 0, + u_time: 2155, + is_mobile: 1 + } + }) + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(data) + $('.channelname').each((i, el) => { + const name = $(el).find('center > a:eq(1)').text() + const url = $(el).find('center > a:eq(1)').attr('href') + const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/) + + channels.push({ + lang: 'fr', + name, + site_id: `${number}/${slug}` + }) + }) + } + + return uniqBy(channels, x => x.site_id) + } +} + +function parseStart($item, date) { + const timeString = $item('td:eq(0)').text().trim() + const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` + + return dayjs.tz(dateString, 'YYYY-MM-DD HH[h]mm', 'Europe/Rome') +} + +function parseTitle($item) { + return $item('td:eq(1)').text().trim() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('table.table > tbody > tr').toArray() +} diff --git a/sites/tvcesoir.fr/tvcesoir.fr.test.js b/sites/tvcesoir.fr/tvcesoir.fr.test.js index 73acc873..10610f10 100644 --- a/sites/tvcesoir.fr/tvcesoir.fr.test.js +++ b/sites/tvcesoir.fr/tvcesoir.fr.test.js @@ -1,50 +1,50 @@ -const { parser, url } = require('./tvcesoir.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('2023-11-24', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '847049/tf-1', - xmltv_id: 'TF1.fr' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.tvcesoir.fr/programme-tv/programme/chaine/847049/tf-1.html?dt=2023-11-24' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-11-24T01:00:00.000Z', - stop: '2023-11-24T01:10:00.000Z', - title: "Tirage de l'Euro Millions" - }) - - expect(results[26]).toMatchObject({ - start: '2023-11-24T22:45:00.000Z', - stop: '2023-11-24T23:15:00.000Z', - title: 'Juge Arthur' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./tvcesoir.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('2023-11-24', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '847049/tf-1', + xmltv_id: 'TF1.fr' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.tvcesoir.fr/programme-tv/programme/chaine/847049/tf-1.html?dt=2023-11-24' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-11-24T01:00:00.000Z', + stop: '2023-11-24T01:10:00.000Z', + title: "Tirage de l'Euro Millions" + }) + + expect(results[26]).toMatchObject({ + start: '2023-11-24T22:45:00.000Z', + stop: '2023-11-24T23:15:00.000Z', + title: 'Juge Arthur' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, './__data__/no_content.html'), 'utf8') + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvcubana.icrt.cu/__data__/content.json b/sites/tvcubana.icrt.cu/__data__/content.json new file mode 100644 index 00000000..b3398bc9 --- /dev/null +++ b/sites/tvcubana.icrt.cu/__data__/content.json @@ -0,0 +1,55 @@ +[ + { + "eventId":"6169c2300ad38b0a8d9e3760", + "title":"CARIBE NOTICIAS", + "description":"EMISI\\u00d3N DE CIERRE.", + "eventInitialDate":"2021-11-22T00:00:00", + "eventEndDate":"2021-11-22T00:00:00", + "idFromEprog":"5c096ea5bad1b202541503cf", + "extendedDescription":"", + "transmission":"Estreno", + "pid":"", + "space":"CARIBE NOTICIAS", + "eventStartTime":{ + "value":{ + "ticks":24000000000, + "days":0, + "hours":0, + "milliseconds":0, + "minutes":40, + "seconds":0, + "totalDays":0.027777777777777776, + "totalHours":0.6666666666666666, + "totalMilliseconds":2400000, + "totalMinutes":40, + "totalSeconds":2400 + }, + "hasValue":true + }, + "eventEndTime":{ + "value":{ + "ticks":30000000000, + "days":0, + "hours":0, + "milliseconds":0, + "minutes":50, + "seconds":0, + "totalDays":0.034722222222222224, + "totalHours":0.8333333333333334, + "totalMilliseconds":3000000, + "totalMinutes":50, + "totalSeconds":3000 + }, + "hasValue":true + }, + "eventDuration":"00:10:00", + "channelName":"Cubavisi\\u00f3n", + "eventInitialDateTime":"2021-11-22T00:40:00", + "eventEndDateTime":"2021-11-22T00:50:00", + "isEventWithNegativeDuration":false, + "isEventWithDurationOver24Hrs":false, + "isEventWithTextOverLength":false, + "created":"2021-11-22T10:32:27.476824", + "id":5309687 + } +] \ No newline at end of file diff --git a/sites/tvcubana.icrt.cu/__data__/no_content.html b/sites/tvcubana.icrt.cu/__data__/no_content.html new file mode 100644 index 00000000..03bc0ab4 --- /dev/null +++ b/sites/tvcubana.icrt.cu/__data__/no_content.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sites/tvcubana.icrt.cu/tvcubana.icrt.cu.config.js b/sites/tvcubana.icrt.cu/tvcubana.icrt.cu.config.js index 41edebca..94fd48f9 100644 --- a/sites/tvcubana.icrt.cu/tvcubana.icrt.cu.config.js +++ b/sites/tvcubana.icrt.cu/tvcubana.icrt.cu.config.js @@ -1,48 +1,48 @@ -const dayjs = require('dayjs') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(timezone) - -module.exports = { - site: 'tvcubana.icrt.cu', - days: 2, - url({ channel, date }) { - const daysOfWeek = ['domingo', 'lunes', 'martes', 'miercoles', 'jueves', 'viernes', 'sabado'] - - return `https://www.tvcubana.icrt.cu/cartv/${channel.site_id}/${daysOfWeek[date.day()]}.php` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.description, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseStart(item) { - return dayjs.tz(item.eventInitialDateTime, 'America/Havana') -} - -function parseStop(item) { - return dayjs.tz(item.eventEndDateTime, 'America/Havana') -} - -function parseItems(content) { - let data - try { - data = JSON.parse(content) - } catch { - return [] - } - if (!data || !Array.isArray(data)) return [] - - return data -} +const dayjs = require('dayjs') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(timezone) + +module.exports = { + site: 'tvcubana.icrt.cu', + days: 2, + url({ channel, date }) { + const daysOfWeek = ['domingo', 'lunes', 'martes', 'miercoles', 'jueves', 'viernes', 'sabado'] + + return `https://www.tvcubana.icrt.cu/cartv/${channel.site_id}/${daysOfWeek[date.day()]}.php` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.description, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseStart(item) { + return dayjs.tz(item.eventInitialDateTime, 'America/Havana') +} + +function parseStop(item) { + return dayjs.tz(item.eventEndDateTime, 'America/Havana') +} + +function parseItems(content) { + let data + try { + data = JSON.parse(content) + } catch { + return [] + } + if (!data || !Array.isArray(data)) return [] + + return data +} diff --git a/sites/tvcubana.icrt.cu/tvcubana.icrt.cu.test.js b/sites/tvcubana.icrt.cu/tvcubana.icrt.cu.test.js index b4854ae1..9e9f453c 100644 --- a/sites/tvcubana.icrt.cu/tvcubana.icrt.cu.test.js +++ b/sites/tvcubana.icrt.cu/tvcubana.icrt.cu.test.js @@ -1,50 +1,52 @@ -const { parser, url } = require('./tvcubana.icrt.cu.config.js') -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('2021-11-22', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'cv', - xmltv_id: 'CubavisionNacional.cu' -} -const content = - '[{"eventId":"6169c2300ad38b0a8d9e3760","title":"CARIBE NOTICIAS","description":"EMISI\\u00d3N DE CIERRE.","eventInitialDate":"2021-11-22T00:00:00","eventEndDate":"2021-11-22T00:00:00","idFromEprog":"5c096ea5bad1b202541503cf","extendedDescription":"","transmission":"Estreno","pid":"","space":"CARIBE NOTICIAS","eventStartTime":{"value":{"ticks":24000000000,"days":0,"hours":0,"milliseconds":0,"minutes":40,"seconds":0,"totalDays":0.027777777777777776,"totalHours":0.6666666666666666,"totalMilliseconds":2400000,"totalMinutes":40,"totalSeconds":2400},"hasValue":true},"eventEndTime":{"value":{"ticks":30000000000,"days":0,"hours":0,"milliseconds":0,"minutes":50,"seconds":0,"totalDays":0.034722222222222224,"totalHours":0.8333333333333334,"totalMilliseconds":3000000,"totalMinutes":50,"totalSeconds":3000},"hasValue":true},"eventDuration":"00:10:00","channelName":"Cubavisi\\u00f3n","eventInitialDateTime":"2021-11-22T00:40:00","eventEndDateTime":"2021-11-22T00:50:00","isEventWithNegativeDuration":false,"isEventWithDurationOver24Hrs":false,"isEventWithTextOverLength":false,"created":"2021-11-22T10:32:27.476824","id":5309687}]' - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://www.tvcubana.icrt.cu/cartv/cv/lunes.php') -}) - -it('can generate valid url for next day', () => { - expect(url({ channel, date: date.add(2, 'd') })).toBe( - 'https://www.tvcubana.icrt.cu/cartv/cv/miercoles.php' - ) -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(result).toMatchObject([ - { - start: '2021-11-22T05:40:00.000Z', - stop: '2021-11-22T05:50:00.000Z', - title: 'CARIBE NOTICIAS', - description: 'EMISIÓN DE CIERRE.' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./tvcubana.icrt.cu.config.js') +const dayjs = require('dayjs') +const fs = require('fs') +const path = require('path') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-22', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'cv', + xmltv_id: 'CubavisionNacional.cu' +} +let content = fs.readFileSync(path.resolve(__dirname, './__data__/content.json'), {encoding: 'utf8'}) +// in the specific case of this site, the unicode escape sequences are double-escaped +content = content.replace(/\\\\u([0-9a-fA-F]{4})/g, '\\u$1') + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://www.tvcubana.icrt.cu/cartv/cv/lunes.php') +}) + +it('can generate valid url for next day', () => { + expect(url({ channel, date: date.add(2, 'd') })).toBe( + 'https://www.tvcubana.icrt.cu/cartv/cv/miercoles.php' + ) +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(result).toMatchObject([ + { + start: '2021-11-22T05:40:00.000Z', + stop: '2021-11-22T05:50:00.000Z', + title: 'CARIBE NOTICIAS', + description: 'EMISIÓN DE CIERRE.' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, './__data__/no_content.html'), 'utf8') + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvgids.nl/tvgids.nl.config.js b/sites/tvgids.nl/tvgids.nl.config.js index 4df42207..ba3a12ee 100644 --- a/sites/tvgids.nl/tvgids.nl.config.js +++ b/sites/tvgids.nl/tvgids.nl.config.js @@ -1,87 +1,87 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const { DateTime } = require('luxon') - -module.exports = { - site: 'tvgids.nl', - days: 2, - url: function ({ date, channel }) { - const path = - DateTime.utc().day === DateTime.fromMillis(date.valueOf()).day - ? '' - : `${date.format('DD-MM-YYYY')}/` - - return `https://www.tvgids.nl/gids/${path}${channel.site_id}` - }, - parser: function ({ content, date }) { - date = date.subtract(1, 'd') - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ minutes: 30 }) - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - image: parseImage($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://www.tvgids.nl/gids/') - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(data) - - const channels = [] - $('.guide__channel-logo-container').each((i, el) => { - channels.push({ - site_id: $(el).find('a').attr('id'), - name: $(el).find('img').attr('title'), - lang: 'nl' - }) - }) - - return channels - } -} - -function parseTitle($item) { - return $item('.program__title').text().trim() -} - -function parseDescription($item) { - return $item('.program__text').text().trim() -} - -function parseImage($item) { - return $item('.program__thumbnail').data('src') -} - -function parseStart($item, date) { - const time = $item('.program__starttime').clone().children().remove().end().text().trim() - - return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { - zone: 'Europe/Amsterdam' - }).toUTC() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.guide__guide .program').toArray() -} +const cheerio = require('cheerio') +const axios = require('axios') +const { DateTime } = require('luxon') + +module.exports = { + site: 'tvgids.nl', + days: 2, + url: function ({ date, channel }) { + const path = + DateTime.utc().day === DateTime.fromMillis(date.valueOf()).day + ? '' + : `${date.format('DD-MM-YYYY')}/` + + return `https://www.tvgids.nl/gids/${path}${channel.site_id}` + }, + parser: function ({ content, date }) { + date = date.subtract(1, 'd') + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ minutes: 30 }) + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + image: parseImage($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://www.tvgids.nl/gids/') + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(data) + + const channels = [] + $('.guide__channel-logo-container').each((i, el) => { + channels.push({ + site_id: $(el).find('a').attr('id'), + name: $(el).find('img').attr('title'), + lang: 'nl' + }) + }) + + return channels + } +} + +function parseTitle($item) { + return $item('.program__title').text().trim() +} + +function parseDescription($item) { + return $item('.program__text').text().trim() +} + +function parseImage($item) { + return $item('.program__thumbnail').data('src') +} + +function parseStart($item, date) { + const time = $item('.program__starttime').clone().children().remove().end().text().trim() + + return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { + zone: 'Europe/Amsterdam' + }).toUTC() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.guide__guide .program').toArray() +} diff --git a/sites/tvgids.nl/tvgids.nl.test.js b/sites/tvgids.nl/tvgids.nl.test.js index 88da3f0a..bb322601 100644 --- a/sites/tvgids.nl/tvgids.nl.test.js +++ b/sites/tvgids.nl/tvgids.nl.test.js @@ -1,60 +1,60 @@ -const { parser, url } = require('./tvgids.nl.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-19', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'npo1', - xmltv_id: 'NPO1.nl' -} - -it('can generate valid url', () => { - jest.useFakeTimers().setSystemTime(new Date('2025-01-17')) - - expect(url({ date, channel })).toBe('https://www.tvgids.nl/gids/19-01-2025/npo1') -}) - -it('can generate valid url for today', () => { - const today = dayjs().startOf('d') - - expect(url({ date: today, channel })).toBe('https://www.tvgids.nl/gids/npo1') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2025-01-18T22:57:00.000Z', - stop: '2025-01-18T23:58:00.000Z', - title: 'Op1', - image: 'https://tvgidsassets.nl/v301/upload/o/carrousel/op1-451542641.jpg', - description: "Talkshow met wisselende presentatieduo's, live vanuit Amsterdam." - }) - - expect(results[61]).toMatchObject({ - start: '2025-01-20T01:18:00.000Z', - stop: '2025-01-20T01:48:00.000Z', - title: 'NOS Journaal', - image: 'https://tvgidsassets.nl/v301/upload/n/carrousel/nos-journaal-452818771.jpg', - description: - 'Met het laatste nieuws, gebeurtenissen van nationaal en internationaal belang en de weersverwachting voor vandaag.' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '', - date - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./tvgids.nl.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-19', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'npo1', + xmltv_id: 'NPO1.nl' +} + +it('can generate valid url', () => { + jest.useFakeTimers().setSystemTime(new Date('2025-01-17')) + + expect(url({ date, channel })).toBe('https://www.tvgids.nl/gids/19-01-2025/npo1') +}) + +it('can generate valid url for today', () => { + const today = dayjs().startOf('d') + + expect(url({ date: today, channel })).toBe('https://www.tvgids.nl/gids/npo1') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2025-01-18T22:57:00.000Z', + stop: '2025-01-18T23:58:00.000Z', + title: 'Op1', + image: 'https://tvgidsassets.nl/v301/upload/o/carrousel/op1-451542641.jpg', + description: "Talkshow met wisselende presentatieduo's, live vanuit Amsterdam." + }) + + expect(results[61]).toMatchObject({ + start: '2025-01-20T01:18:00.000Z', + stop: '2025-01-20T01:48:00.000Z', + title: 'NOS Journaal', + image: 'https://tvgidsassets.nl/v301/upload/n/carrousel/nos-journaal-452818771.jpg', + description: + 'Met het laatste nieuws, gebeurtenissen van nationaal en internationaal belang en de weersverwachting voor vandaag.' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '', + date + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvguide.com/tvguide.com.config.js b/sites/tvguide.com/tvguide.com.config.js index 6a92a514..52f3df50 100644 --- a/sites/tvguide.com/tvguide.com.config.js +++ b/sites/tvguide.com/tvguide.com.config.js @@ -1,152 +1,152 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const debug = require('debug')('site:tvguide.com') - -dayjs.extend(utc) -dayjs.extend(timezone) - -const providerId = '9100001138' -const maxDuration = 240 -const segments = 1440 / maxDuration -const headers = { - 'referer': 'https://www.tvguide.com/', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36', -} - -const east_channels = [ - '5StarMax', 'ABC Network Feed', 'ActionMax', 'A&E', 'AMC', 'Animal Planet', 'BBC America', - 'BET', 'BET Her', 'Bravo', 'Cartoon Network', 'CBS National', 'Cinemax', 'CMT', 'Comedy Central', - 'Discovery', 'Disney', 'Disney Junior', 'Disney XD', 'E!', 'Flix', 'Food Network', 'FOX', 'Freeform', - 'Fuse HD', 'FX', 'FXX', 'FYI', 'Game Show Network', 'Hallmark', 'Hallmark Mystery', 'HBO 2', - 'HBO Comedy', 'HBO', 'HBO Family', 'HBO Signature', 'HBO Zone', 'HGTV', 'History', 'IFC', - 'Investigation Discovery', 'ION', 'Lifetime', 'LMN', 'LOGO', 'MAGNOLIA Network', 'MGM+ Hits HD', - 'MoreMax', 'MovieMax', 'MTV2', 'MTV', 'National Geographic', 'National Geographic Wild', 'NBC National', - 'Nickelodeon', 'Nick Jr.', 'Nicktoons', 'OuterMax', 'OWN', 'Oxygen', 'Paramount Network', 'PBS HD', - 'Pop Network', 'SHOWTIME 2', 'Paramount+ with Showtime', 'SHOWTIME EXTREME', 'SHOWTIME FAMILY ZONE', - 'SHOWTIME NEXT', 'SHOWTIME SHOWCASE', 'SHOWTIME WOMEN', 'SHOxBET', 'Smithsonian', 'STARZ Cinema', - 'STARZ Comedy', 'STARZ', 'STARZ Edge', 'STARZ ENCORE Action', 'STARZ ENCORE Black', - 'STARZ ENCORE Classic', 'STARZ ENCORE', 'STARZ ENCORE Family', 'STARZ ENCORE Suspense', - 'STARZ ENCORE Westerns', 'STARZ InBlack', 'STARZ Kids & Family', 'Sundance TV', 'Syfy', 'tbs', - 'Turner Classic Movies', 'TeenNick', 'Telemundo', 'The Movie', 'The Movie Xtra', 'ThrillerMax', 'TLC', - 'TNT', 'Travel', 'truTV', 'TV Land', 'Universal Kids', 'USA', 'VH1', 'WE tv', 'Univision' -] - -module.exports = { - site: 'tvguide.com', - days: 2, - request: { - headers: function () { - return headers - }, - responseType: 'application/json', - decompress: true, - cache: { - ttl: 24 * 60 * 60 * 1000 // 1 day - } - }, - async url({ date, segment = 1 }) { - const params = [] - if (module.exports.apiKey === undefined) { - module.exports.apiKey = await module.exports.fetchApiKey() - debug('Got api key', module.exports.apiKey) - } - if (date) { - if (segment > 1) { - date = date.add((segment - 1) * maxDuration, 'm') - } - params.push(`start=${date.unix()}`, `duration=${maxDuration}`) - } - params.push(`apiKey=${module.exports.apiKey}`) - - return date ? - `https://backend.tvguide.com/tvschedules/tvguide/${providerId}/web?${params.join('&')}` : - `https://backend.tvguide.com/tvschedules/tvguide/serviceprovider/${providerId}/sources/web?${params.join('&')}` - }, - async parser({ content, date, channel, fetchSegments = true }) { - const programs = [] - const f = data => { - const result = [] - if (typeof data === 'string') { - data = JSON.parse(data) - } - if (data && Array.isArray(data?.data?.items)) { - data.data.items - .filter(i => i.channel.sourceId.toString() === channel.site_id) - .forEach(i => { - result.push(...i.programSchedules.map(p => { - return { i: p, url: p.programDetails } - })) - }) - } - - return result - } - const queues = f(content) - if (queues.length && fetchSegments) { - for (let segment = 2; segment <= segments; segment++) { - const segmentUrl = await module.exports.url({ date, segment }) - debug(`fetch segment ${segment}: ${segmentUrl}`) - try { - const res = await axios.get(segmentUrl, { headers }) - queues.push(...f(res.data)) - } catch (err) { - debug(`Failed to fetch segment ${segment}: ${err.message}`) - } - } - } - for (const queue of queues) { - try { - const res = await axios.get(queue.url, { headers }) - const item = res.data?.data?.item || queue.i - programs.push({ - title: item.title || queue.i.title, - sub_title: item.episodeNumber ? item.episodeTitle : null, - description: item.description, - season: item.seasonNumber, - episode: item.episodeNumber, - rating: item.rating ? { system: 'MPA', value: item.rating } : null, - categories: Array.isArray(item.genres) ? item.genres.map(g => g.name) : null, - start: dayjs.unix(item.startTime || queue.i.startTime), - stop: dayjs.unix(item.endTime || queue.i.endTime), - }) - } catch (err) { - debug(`Failed to fetch program details ${queue.url}: ${err.message}`) - } - } - return programs - }, - async channels() { - const channels = [] - try { - const data = await axios - .get(await this.url({}), { headers }) - .then(r => r.data) - data.data.items.forEach(item => { - const finalName = item.fullName.replace(/Channel|Schedule/g, '').trim() - const isEast = east_channels.some(name => name.toLowerCase().includes(finalName.toLowerCase())) - channels.push({ - lang: 'en', - site_id: item.sourceId, - xmltv_id: finalName.replaceAll(/[ '&]/g, '') + '.us' + (isEast ? '@East' : ''), - name: finalName - }) - }) - } catch (err) { - console.error('Failed to fetch channels:', err.message) - } - return channels - }, - async fetchApiKey() { - try { - const data = await axios - .get('https://www.tvguide.com/listings/') - .then(r => r.data) - return data ? data.match(/apiKey=([a-zA-Z0-9]+)&/)[1] : null - } catch (err) { - console.error('Failed to fetch API key:', err.message) - return null - } - } +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const debug = require('debug')('site:tvguide.com') + +dayjs.extend(utc) +dayjs.extend(timezone) + +const providerId = '9100001138' +const maxDuration = 240 +const segments = 1440 / maxDuration +const headers = { + 'referer': 'https://www.tvguide.com/', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36', +} + +const east_channels = [ + '5StarMax', 'ABC Network Feed', 'ActionMax', 'A&E', 'AMC', 'Animal Planet', 'BBC America', + 'BET', 'BET Her', 'Bravo', 'Cartoon Network', 'CBS National', 'Cinemax', 'CMT', 'Comedy Central', + 'Discovery', 'Disney', 'Disney Junior', 'Disney XD', 'E!', 'Flix', 'Food Network', 'FOX', 'Freeform', + 'Fuse HD', 'FX', 'FXX', 'FYI', 'Game Show Network', 'Hallmark', 'Hallmark Mystery', 'HBO 2', + 'HBO Comedy', 'HBO', 'HBO Family', 'HBO Signature', 'HBO Zone', 'HGTV', 'History', 'IFC', + 'Investigation Discovery', 'ION', 'Lifetime', 'LMN', 'LOGO', 'MAGNOLIA Network', 'MGM+ Hits HD', + 'MoreMax', 'MovieMax', 'MTV2', 'MTV', 'National Geographic', 'National Geographic Wild', 'NBC National', + 'Nickelodeon', 'Nick Jr.', 'Nicktoons', 'OuterMax', 'OWN', 'Oxygen', 'Paramount Network', 'PBS HD', + 'Pop Network', 'SHOWTIME 2', 'Paramount+ with Showtime', 'SHOWTIME EXTREME', 'SHOWTIME FAMILY ZONE', + 'SHOWTIME NEXT', 'SHOWTIME SHOWCASE', 'SHOWTIME WOMEN', 'SHOxBET', 'Smithsonian', 'STARZ Cinema', + 'STARZ Comedy', 'STARZ', 'STARZ Edge', 'STARZ ENCORE Action', 'STARZ ENCORE Black', + 'STARZ ENCORE Classic', 'STARZ ENCORE', 'STARZ ENCORE Family', 'STARZ ENCORE Suspense', + 'STARZ ENCORE Westerns', 'STARZ InBlack', 'STARZ Kids & Family', 'Sundance TV', 'Syfy', 'tbs', + 'Turner Classic Movies', 'TeenNick', 'Telemundo', 'The Movie', 'The Movie Xtra', 'ThrillerMax', 'TLC', + 'TNT', 'Travel', 'truTV', 'TV Land', 'Universal Kids', 'USA', 'VH1', 'WE tv', 'Univision' +] + +module.exports = { + site: 'tvguide.com', + days: 2, + request: { + headers: function () { + return headers + }, + responseType: 'application/json', + decompress: true, + cache: { + ttl: 24 * 60 * 60 * 1000 // 1 day + } + }, + async url({ date, segment = 1 }) { + const params = [] + if (module.exports.apiKey === undefined) { + module.exports.apiKey = await module.exports.fetchApiKey() + debug('Got api key', module.exports.apiKey) + } + if (date) { + if (segment > 1) { + date = date.add((segment - 1) * maxDuration, 'm') + } + params.push(`start=${date.unix()}`, `duration=${maxDuration}`) + } + params.push(`apiKey=${module.exports.apiKey}`) + + return date ? + `https://backend.tvguide.com/tvschedules/tvguide/${providerId}/web?${params.join('&')}` : + `https://backend.tvguide.com/tvschedules/tvguide/serviceprovider/${providerId}/sources/web?${params.join('&')}` + }, + async parser({ content, date, channel, fetchSegments = true }) { + const programs = [] + const f = data => { + const result = [] + if (typeof data === 'string') { + data = JSON.parse(data) + } + if (data && Array.isArray(data?.data?.items)) { + data.data.items + .filter(i => i.channel.sourceId.toString() === channel.site_id) + .forEach(i => { + result.push(...i.programSchedules.map(p => { + return { i: p, url: p.programDetails } + })) + }) + } + + return result + } + const queues = f(content) + if (queues.length && fetchSegments) { + for (let segment = 2; segment <= segments; segment++) { + const segmentUrl = await module.exports.url({ date, segment }) + debug(`fetch segment ${segment}: ${segmentUrl}`) + try { + const res = await axios.get(segmentUrl, { headers }) + queues.push(...f(res.data)) + } catch (err) { + debug(`Failed to fetch segment ${segment}: ${err.message}`) + } + } + } + for (const queue of queues) { + try { + const res = await axios.get(queue.url, { headers }) + const item = res.data?.data?.item || queue.i + programs.push({ + title: item.title || queue.i.title, + sub_title: item.episodeNumber ? item.episodeTitle : null, + description: item.description, + season: item.seasonNumber, + episode: item.episodeNumber, + rating: item.rating ? { system: 'MPA', value: item.rating } : null, + categories: Array.isArray(item.genres) ? item.genres.map(g => g.name) : null, + start: dayjs.unix(item.startTime || queue.i.startTime), + stop: dayjs.unix(item.endTime || queue.i.endTime), + }) + } catch (err) { + debug(`Failed to fetch program details ${queue.url}: ${err.message}`) + } + } + return programs + }, + async channels() { + const channels = [] + try { + const data = await axios + .get(await this.url({}), { headers }) + .then(r => r.data) + data.data.items.forEach(item => { + const finalName = item.fullName.replace(/Channel|Schedule/g, '').trim() + const isEast = east_channels.some(name => name.toLowerCase().includes(finalName.toLowerCase())) + channels.push({ + lang: 'en', + site_id: item.sourceId, + xmltv_id: finalName.replaceAll(/[ '&]/g, '') + '.us' + (isEast ? '@East' : ''), + name: finalName + }) + }) + } catch (err) { + console.error('Failed to fetch channels:', err.message) + } + return channels + }, + async fetchApiKey() { + try { + const data = await axios + .get('https://www.tvguide.com/listings/') + .then(r => r.data) + return data ? data.match(/apiKey=([a-zA-Z0-9]+)&/)[1] : null + } catch (err) { + console.error('Failed to fetch API key:', err.message) + return null + } + } } \ No newline at end of file diff --git a/sites/tvguide.com/tvguide.com.test.js b/sites/tvguide.com/tvguide.com.test.js index 50bd879c..a5869781 100644 --- a/sites/tvguide.com/tvguide.com.test.js +++ b/sites/tvguide.com/tvguide.com.test.js @@ -1,78 +1,78 @@ -const { parser, url } = require('./tvguide.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-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '9200004683', - xmltv_id: 'NatGeoWild.us' -} - -it('can generate valid url', async () => { - axios.get.mockImplementation(url => { - if (url === 'https://www.tvguide.com/listings/') { - return Promise.resolve({ - data: 'html_apiKey=DI9elXhZ3bU6ujsA2gXEKOANyncXGUGc&...' - }) - } - throw new Error(`Unexpected URL: ${url}`) - }) - - const result = await url({ date }) - expect(result).toBe( - 'https://backend.tvguide.com/tvschedules/tvguide/9100001138/web?start=1753747200&duration=240&apiKey=DI9elXhZ3bU6ujsA2gXEKOANyncXGUGc' - ) -}) - -it('can parse response', async () => { - const content = JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf-8')) - - axios.get.mockImplementation(url => { - if ( - url === - 'https://backend.tvguide.com/tvschedules/tvguide/programdetails/9000058285/web' - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, date, channel, fetchSegments: false }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2025-07-29T00:00:00.000Z', - stop: '2025-07-29T01:00:00.000Z', - title: 'Secrets of the Zoo: North Carolina', - sub_title: 'Chimp Off the Old Block', - description: - 'Chimps living at the North Carolina Zoo, a zoo located in the center of North Carolina that serves as the world\'s largest natural habitat zoo, as well as one of two state-supported zoos, are cared for', - categories: ['Reality'], - season: 1, - episode: 1, - }) -}) - -it('can handle empty guide', async () => { - const results = await parser({ - date, - channel, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json')) - }) - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./tvguide.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-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '9200004683', + xmltv_id: 'NatGeoWild.us' +} + +it('can generate valid url', async () => { + axios.get.mockImplementation(url => { + if (url === 'https://www.tvguide.com/listings/') { + return Promise.resolve({ + data: 'html_apiKey=DI9elXhZ3bU6ujsA2gXEKOANyncXGUGc&...' + }) + } + throw new Error(`Unexpected URL: ${url}`) + }) + + const result = await url({ date }) + expect(result).toBe( + 'https://backend.tvguide.com/tvschedules/tvguide/9100001138/web?start=1753747200&duration=240&apiKey=DI9elXhZ3bU6ujsA2gXEKOANyncXGUGc' + ) +}) + +it('can parse response', async () => { + const content = JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf-8')) + + axios.get.mockImplementation(url => { + if ( + url === + 'https://backend.tvguide.com/tvschedules/tvguide/programdetails/9000058285/web' + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, date, channel, fetchSegments: false }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2025-07-29T00:00:00.000Z', + stop: '2025-07-29T01:00:00.000Z', + title: 'Secrets of the Zoo: North Carolina', + sub_title: 'Chimp Off the Old Block', + description: + 'Chimps living at the North Carolina Zoo, a zoo located in the center of North Carolina that serves as the world\'s largest natural habitat zoo, as well as one of two state-supported zoos, are cared for', + categories: ['Reality'], + season: 1, + episode: 1, + }) +}) + +it('can handle empty guide', async () => { + const results = await parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json')) + }) + expect(results).toMatchObject([]) +}) diff --git a/sites/tvguide.myjcom.jp/__data__/content.json b/sites/tvguide.myjcom.jp/__data__/content.json new file mode 100644 index 00000000..1bd1a96e --- /dev/null +++ b/sites/tvguide.myjcom.jp/__data__/content.json @@ -0,0 +1,34 @@ +{ + "120_200_4_20220114":[ + { + "@search.score":1, + "cid":"120_7305523", + "serviceCode":"200_4", + "channelName":"スターチャンネル1", + "digitalNo":195, + "eventId":"181", + "title":"[5.1]フードロア:タマリンド", + "commentary":"HBO(R)アジア製作。日本の齊藤工などアジアの監督が、各国の食をテーマに描いたアンソロジーシリーズ。(全8話)(19年 シンガポール 56分)", + "attr":[ + "5.1", + "hd", + "cp1" + ], + "sortGenre":"31", + "hasImage":1, + "imgPath":"/monomedia/si/2022/20220114/7305523/image/7743d17b655b8d2274ca58b74f2f095c.jpg", + "isRecommended":null, + "programStart":20220114050000, + "programEnd":20220114060000, + "programDate":20220114, + "programId":568519, + "start_time":"00", + "duration":60, + "top":300, + "end_time":"20220114060000", + "channel_type":"120", + "is_end":false, + "show_remoterec":true + } + ] +} \ No newline at end of file diff --git a/sites/tvguide.myjcom.jp/__data__/no_content.json b/sites/tvguide.myjcom.jp/__data__/no_content.json new file mode 100644 index 00000000..ce916b67 --- /dev/null +++ b/sites/tvguide.myjcom.jp/__data__/no_content.json @@ -0,0 +1 @@ +{"120_200_3_20220114":[]} \ No newline at end of file diff --git a/sites/tvguide.myjcom.jp/tvguide.myjcom.jp.config.js b/sites/tvguide.myjcom.jp/tvguide.myjcom.jp.config.js index 9651de74..ab07d573 100644 --- a/sites/tvguide.myjcom.jp/tvguide.myjcom.jp.config.js +++ b/sites/tvguide.myjcom.jp/tvguide.myjcom.jp.config.js @@ -1,114 +1,114 @@ -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') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'tvguide.myjcom.jp', - days: 2, - lang: 'ja', - url: function ({ date, channel }) { - const id = `${channel.site_id}_${date.format('YYYYMMDD')}` - - return `https://tvguide.myjcom.jp/api/getEpgInfo/?channels=${id}` - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel, date) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.commentary, - category: parseCategory(item), - image: parseImage(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const requests = [ - axios.get( - 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=2&area=108&channelGenre&course&chart&is_adult=true' - ), - axios.get( - 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=3&area=108&channelGenre&course&chart&is_adult=true' - ), - axios.get( - 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=5&area=108&channelGenre&course&chart&is_adult=true' - ), - axios.get( - 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=120&area=108&channelGenre&course&chart&is_adult=true' - ), - axios.get( - 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=200&area=108&channelGenre&course&chart&is_adult=true' - ) - ] - - let items = [] - await Promise.all(requests) - .then(responses => { - for (const r of responses) { - items = items.concat(r.data.header) - } - }) - .catch(console.log) - - return items.map(item => { - return { - lang: 'ja', - site_id: `${item.channel_type}_${item.channel_id}_${item.network_id}`, - name: item.channel_name - } - }) - } -} - -function parseImage(item) { - return item.imgPath ? `https://tvguide.myjcom.jp${item.imgPath}` : null -} - -function parseCategory(item) { - if (!item.sortGenre) return null - - const id = item.sortGenre[0] - const genres = { - 0: 'ニュース/報道', - 1: 'スポーツ', - 2: '情報/ワイドショー', - 3: 'ドラマ', - 4: '音楽', - 5: 'バラエティ', - 6: '映画', - 7: 'アニメ/特撮', - 8: 'ドキュメンタリー/教養', - 9: '劇場/公演', - 10: '趣味/教育', - 11: '福祉', - 12: 'その他' - } - - return genres[id] -} - -function parseStart(item) { - return dayjs.tz(item.programStart.toString(), 'YYYYMMDDHHmmss', 'Asia/Tokyo') -} - -function parseStop(item) { - return dayjs.tz(item.programEnd.toString(), 'YYYYMMDDHHmmss', 'Asia/Tokyo') -} - -function parseItems(content, channel, date) { - const id = `${channel.site_id}_${date.format('YYYYMMDD')}` - const parsed = JSON.parse(content) - - return parsed[id] || [] -} +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') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'tvguide.myjcom.jp', + days: 2, + lang: 'ja', + url: function ({ date, channel }) { + const id = `${channel.site_id}_${date.format('YYYYMMDD')}` + + return `https://tvguide.myjcom.jp/api/getEpgInfo/?channels=${id}` + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel, date) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.commentary, + category: parseCategory(item), + image: parseImage(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const requests = [ + axios.get( + 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=2&area=108&channelGenre&course&chart&is_adult=true' + ), + axios.get( + 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=3&area=108&channelGenre&course&chart&is_adult=true' + ), + axios.get( + 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=5&area=108&channelGenre&course&chart&is_adult=true' + ), + axios.get( + 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=120&area=108&channelGenre&course&chart&is_adult=true' + ), + axios.get( + 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=200&area=108&channelGenre&course&chart&is_adult=true' + ) + ] + + let items = [] + await Promise.all(requests) + .then(responses => { + for (const r of responses) { + items = items.concat(r.data.header) + } + }) + .catch(console.log) + + return items.map(item => { + return { + lang: 'ja', + site_id: `${item.channel_type}_${item.channel_id}_${item.network_id}`, + name: item.channel_name + } + }) + } +} + +function parseImage(item) { + return item.imgPath ? `https://tvguide.myjcom.jp${item.imgPath}` : null +} + +function parseCategory(item) { + if (!item.sortGenre) return null + + const id = item.sortGenre[0] + const genres = { + 0: 'ニュース/報道', + 1: 'スポーツ', + 2: '情報/ワイドショー', + 3: 'ドラマ', + 4: '音楽', + 5: 'バラエティ', + 6: '映画', + 7: 'アニメ/特撮', + 8: 'ドキュメンタリー/教養', + 9: '劇場/公演', + 10: '趣味/教育', + 11: '福祉', + 12: 'その他' + } + + return genres[id] +} + +function parseStart(item) { + return dayjs.tz(item.programStart.toString(), 'YYYYMMDDHHmmss', 'Asia/Tokyo') +} + +function parseStop(item) { + return dayjs.tz(item.programEnd.toString(), 'YYYYMMDDHHmmss', 'Asia/Tokyo') +} + +function parseItems(content, channel, date) { + const id = `${channel.site_id}_${date.format('YYYYMMDD')}` + const parsed = JSON.parse(content) + + return parsed[id] || [] +} diff --git a/sites/tvguide.myjcom.jp/tvguide.myjcom.jp.test.js b/sites/tvguide.myjcom.jp/tvguide.myjcom.jp.test.js index 9506861a..51510b31 100644 --- a/sites/tvguide.myjcom.jp/tvguide.myjcom.jp.test.js +++ b/sites/tvguide.myjcom.jp/tvguide.myjcom.jp.test.js @@ -1,50 +1,51 @@ -const { parser, url } = require('./tvguide.myjcom.jp.config.js') -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('2022-01-14', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '120_200_4', - name: 'Star Channel 1', - xmltv_id: 'StarChannel1.jp' -} -const content = - '{"120_200_4_20220114":[{"@search.score":1,"cid":"120_7305523","serviceCode":"200_4","channelName":"スターチャンネル1","digitalNo":195,"eventId":"181","title":"[5.1]フードロア:タマリンド","commentary":"HBO(R)アジア製作。日本の齊藤工などアジアの監督が、各国の食をテーマに描いたアンソロジーシリーズ。(全8話)(19年 シンガポール 56分)","attr":["5.1","hd","cp1"],"sortGenre":"31","hasImage":1,"imgPath":"/monomedia/si/2022/20220114/7305523/image/7743d17b655b8d2274ca58b74f2f095c.jpg","isRecommended":null,"programStart":20220114050000,"programEnd":20220114060000,"programDate":20220114,"programId":568519,"start_time":"00","duration":60,"top":300,"end_time":"20220114060000","channel_type":"120","is_end":false,"show_remoterec":true}]}' - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe('https://tvguide.myjcom.jp/api/getEpgInfo/?channels=120_200_4_20220114') -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-01-13T20:00:00.000Z', - stop: '2022-01-13T21:00:00.000Z', - title: '[5.1]フードロア:タマリンド', - description: - 'HBO(R)アジア製作。日本の齊藤工などアジアの監督が、各国の食をテーマに描いたアンソロジーシリーズ。(全8話)(19年 シンガポール 56分)', - image: - 'https://tvguide.myjcom.jp/monomedia/si/2022/20220114/7305523/image/7743d17b655b8d2274ca58b74f2f095c.jpg', - category: 'ドラマ' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"120_200_3_20220114":[]}' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./tvguide.myjcom.jp.config.js') +const dayjs = require('dayjs') +const fs = require('fs') +const path = require('path') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-01-14', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '120_200_4', + name: 'Star Channel 1', + xmltv_id: 'StarChannel1.jp' +} +const content = fs.readFileSync(path.resolve(__dirname, './__data__/content.json'), 'utf8') + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe('https://tvguide.myjcom.jp/api/getEpgInfo/?channels=120_200_4_20220114') +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-01-13T20:00:00.000Z', + stop: '2022-01-13T21:00:00.000Z', + title: '[5.1]フードロア:タマリンド', + description: + 'HBO(R)アジア製作。日本の齊藤工などアジアの監督が、各国の食をテーマに描いたアンソロジーシリーズ。(全8話)(19年 シンガポール 56分)', + image: + 'https://tvguide.myjcom.jp/monomedia/si/2022/20220114/7305523/image/7743d17b655b8d2274ca58b74f2f095c.jpg', + category: 'ドラマ' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, './__data__/no_content.json'), 'utf8') + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvhebdo.com/tvhebdo.com.config.js b/sites/tvhebdo.com/tvhebdo.com.config.js index be4cf8b4..29f94a44 100644 --- a/sites/tvhebdo.com/tvhebdo.com.config.js +++ b/sites/tvhebdo.com/tvhebdo.com.config.js @@ -1,97 +1,97 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const { DateTime } = require('luxon') - -module.exports = { - site: 'tvhebdo.com', - days: 2, - url: function ({ channel, date }) { - return `https://www.tvhebdo.com/horaire-tele/${channel.site_id}/date/${date.format( - 'YYYY-MM-DD' - )}` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - } - prev.stop = start - } - let stop = start.plus({ minutes: 30 }) - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const _ = require('lodash') - - let items = [] - const offsets = [ - 0, 20, 40, 60, 80, 100, 120, 140, 160, 180, 200, 220, 240, 260, 280, 300, 320, 340, 360 - ] - for (let offset of offsets) { - const url = `https://www.tvhebdo.com/horaire/gr/offset/${offset}/gr_id/0/date/2022-05-11/time/12:00:00` - console.log(url) - const html = await axios - .get(url, { - headers: { - Cookie: - 'distributeur=8004264; __utmz=222163677.1652094266.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); _gcl_au=1.1.656635701.1652094273; tvh=3c2kaml9u14m83v91bg4dqgaf3; __utmc=222163677; IR_gbd=tvhebdo.com; IR_MPI=cf76b363-cf87-11ec-93f5-13daf79f8f76%7C1652367602625; __utma=222163677.2064368965.1652094266.1652281202.1652281479.3; __utmt=1; IR_MPS=1652284935955%7C1652284314367; _uetsid=0d8e2e60d13b11ec850db551304ae9e7; _uetvid=80456fa0b26e11ec9bf94951ce79b5f8; __utmb=222163677.19.9.1652284953979; __atuvc=30%7C19; __atuvs=627bdb98682bc242006' - } - }) - .then(r => r.data) - .catch(console.error) - const $ = cheerio.load(html) - const rows = $('table.gr_row').toArray() - items = items.concat(rows) - } - - let channels = [] - items.forEach(item => { - const $item = cheerio.load(item) - const name = $item('.gr_row_head > div > a.gr_row_head_logo.link_to_station > img').attr( - 'alt' - ) - const url = $item('.gr_row_head > div > div.gr_row_head_poste > a').attr('href') - const [, site_id] = url.match(/horaire-tele\/(.*)/) || [null, null] - channels.push({ - lang: 'fr', - site_id, - name - }) - }) - - return _.uniqBy(channels, 'site_id') - } -} - -function parseTitle($item) { - return $item('.titre').first().text().trim() -} - -function parseStart($item, date) { - const time = $item('.heure').text() - - return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { - zone: 'America/Toronto' - }).toUTC() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $( - '#main_container > div.liste_container > table > tbody > tr[class^=liste_row_style_]' - ).toArray() -} +const cheerio = require('cheerio') +const axios = require('axios') +const { DateTime } = require('luxon') +const uniqBy = require('lodash.uniqby') + +module.exports = { + site: 'tvhebdo.com', + days: 2, + url: function ({ channel, date }) { + return `https://www.tvhebdo.com/horaire-tele/${channel.site_id}/date/${date.format( + 'YYYY-MM-DD' + )}` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + } + prev.stop = start + } + let stop = start.plus({ minutes: 30 }) + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + + let items = [] + const offsets = [ + 0, 20, 40, 60, 80, 100, 120, 140, 160, 180, 200, 220, 240, 260, 280, 300, 320, 340, 360 + ] + for (let offset of offsets) { + const url = `https://www.tvhebdo.com/horaire/gr/offset/${offset}/gr_id/0/date/2022-05-11/time/12:00:00` + console.log(url) + const html = await axios + .get(url, { + headers: { + Cookie: + 'distributeur=8004264; __utmz=222163677.1652094266.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); _gcl_au=1.1.656635701.1652094273; tvh=3c2kaml9u14m83v91bg4dqgaf3; __utmc=222163677; IR_gbd=tvhebdo.com; IR_MPI=cf76b363-cf87-11ec-93f5-13daf79f8f76%7C1652367602625; __utma=222163677.2064368965.1652094266.1652281202.1652281479.3; __utmt=1; IR_MPS=1652284935955%7C1652284314367; _uetsid=0d8e2e60d13b11ec850db551304ae9e7; _uetvid=80456fa0b26e11ec9bf94951ce79b5f8; __utmb=222163677.19.9.1652284953979; __atuvc=30%7C19; __atuvs=627bdb98682bc242006' + } + }) + .then(r => r.data) + .catch(console.error) + const $ = cheerio.load(html) + const rows = $('table.gr_row').toArray() + items = items.concat(rows) + } + + let channels = [] + items.forEach(item => { + const $item = cheerio.load(item) + const name = $item('.gr_row_head > div > a.gr_row_head_logo.link_to_station > img').attr( + 'alt' + ) + const url = $item('.gr_row_head > div > div.gr_row_head_poste > a').attr('href') + const [, site_id] = url.match(/horaire-tele\/(.*)/) || [null, null] + channels.push({ + lang: 'fr', + site_id, + name + }) + }) + + return uniqBy(channels, x => x.site_id) + } +} + +function parseTitle($item) { + return $item('.titre').first().text().trim() +} + +function parseStart($item, date) { + const time = $item('.heure').text() + + return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { + zone: 'America/Toronto' + }).toUTC() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $( + '#main_container > div.liste_container > table > tbody > tr[class^=liste_row_style_]' + ).toArray() +} diff --git a/sites/tvhebdo.com/tvhebdo.com.test.js b/sites/tvhebdo.com/tvhebdo.com.test.js index 550ec8e0..eccf9992 100644 --- a/sites/tvhebdo.com/tvhebdo.com.test.js +++ b/sites/tvhebdo.com/tvhebdo.com.test.js @@ -1,53 +1,53 @@ -const { parser, url } = require('./tvhebdo.com.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('2022-05-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'src/CBFT', - xmltv_id: 'CBFT.ca' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.tvhebdo.com/horaire-tele/src/CBFT/date/2022-05-11' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve('sites/tvhebdo.com/__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-05-11T15:30:00.000Z', - stop: '2022-05-11T16:00:00.000Z', - title: '5 chefs dans ma cuisine' - }) - - expect(results[16]).toMatchObject({ - start: '2022-05-12T04:09:00.000Z', - stop: '2022-05-12T05:19:00.000Z', - title: 'Outlander: Le chardon et le tartan' - }) - - expect(results[36]).toMatchObject({ - start: '2022-05-12T15:00:00.000Z', - stop: '2022-05-12T15:30:00.000Z', - title: 'Ricardo' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve('sites/tvhebdo.com/__data__/no_content.html')) - const result = parser({ content, date }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./tvhebdo.com.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('2022-05-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'src/CBFT', + xmltv_id: 'CBFT.ca' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.tvhebdo.com/horaire-tele/src/CBFT/date/2022-05-11' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve('sites/tvhebdo.com/__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-05-11T15:30:00.000Z', + stop: '2022-05-11T16:00:00.000Z', + title: '5 chefs dans ma cuisine' + }) + + expect(results[16]).toMatchObject({ + start: '2022-05-12T04:09:00.000Z', + stop: '2022-05-12T05:19:00.000Z', + title: 'Outlander: Le chardon et le tartan' + }) + + expect(results[36]).toMatchObject({ + start: '2022-05-12T15:00:00.000Z', + stop: '2022-05-12T15:30:00.000Z', + title: 'Ricardo' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve('sites/tvhebdo.com/__data__/no_content.html')) + const result = parser({ content, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvheute.at/__data__/content.html b/sites/tvheute.at/__data__/content.html new file mode 100644 index 00000000..8eb1678e --- /dev/null +++ b/sites/tvheute.at/__data__/content.html @@ -0,0 +1,64 @@ +
      + +
      +

      Das ORF1 Programm mit allen Sendungen live im TV von tv.orf.at. Sie haben eine Sendung verpasst. In der ORF TVthek finden Sie viele Sendungen on demand zum Abruf als online Video und live stream.

      +
      +
      +
      +
      +
      +

      ORF1 heute

      + SKYsp2 ORF2 +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      SenderZeitZeitTitelStartTitel
      ORF1 Kids + +
      +
      + Monchhichi (Wh.) ANIMATIONSSERIE Der Streiche-Wettbewerb +
      + +
      +
      Roger hat sich Ärger mit Dr. Bellows eingehandelt, der ihn für einen Monat strafversetzen möchte. Einmal mehr hadert Roger mit dem Schicksal, dass er keinen eigenen Flaschengeist besitzt, der ihm aus der Patsche helfen kann. Jeannie schlägt vor, ihm Cousine Marilla zu schicken. Doch Tony ist strikt dagegen. Als ein Zaubererpärchen im exotischen Bühnenoutfit für die Zeit von Rogers Abwesenheit sein Apartment in Untermiete bezieht, glaubt Roger, Jeannie habe ihm ihre Verwandte doch noch gesandt.
      + +
      +
      +
      ORF1 + +
      +
      ZIB 18 NACHRICHTEN +
      +
      +
      \ No newline at end of file diff --git a/sites/tvheute.at/__data__/no_content.html b/sites/tvheute.at/__data__/no_content.html new file mode 100644 index 00000000..2d5f853f --- /dev/null +++ b/sites/tvheute.at/__data__/no_content.html @@ -0,0 +1,8 @@ + + + Object moved + + +

      Object moved to here.

      + + \ No newline at end of file diff --git a/sites/tvheute.at/tvheute.at.config.js b/sites/tvheute.at/tvheute.at.config.js index d73706c8..fcc5e37b 100644 --- a/sites/tvheute.at/tvheute.at.config.js +++ b/sites/tvheute.at/tvheute.at.config.js @@ -1,96 +1,96 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') - -module.exports = { - site: 'tvheute.at', - days: 2, - url({ channel, date }) { - return `https://tvheute.at/part/channel-shows/partial/${channel.site_id}/${date.format( - 'DD-MM-YYYY' - )}` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: parseTitle(item), - description: parseDescription(item), - image: parseImage(item), - category: parseCategory(item), - start: parseStart(item).toJSON(), - stop: parseStop(item).toJSON() - }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - const html = await axios - .get('https://tvheute.at/part/channel-selection') - .then(r => r.data) - .catch(console.log) - - let channels = [] - - const $ = cheerio.load(html) - $('.sortable-list > li').each((i, el) => { - const name = $(el).find('label').text() - const site_id = $(el).find('input').attr('value') - - channels.push({ - lang: 'de', - site_id, - name - }) - }) - - return channels - } -} - -function parseTitle(item) { - const $ = cheerio.load(item) - - return $('.title-col strong').text() -} - -function parseDescription(item) { - const $ = cheerio.load(item) - - return $('.title-col .description').text() -} - -function parseCategory(item) { - const $ = cheerio.load(item) - - return $('.station-col > .type').text() -} - -function parseImage(item) { - const $ = cheerio.load(item) - const imgSrc = $('.title-col .image img').data('src-desktop') - - return imgSrc ? `https://tvheute.at${imgSrc}` : null -} - -function parseStart(item) { - const $ = cheerio.load(item) - const time = $('.end-col > .duration-wrapper').data('start') - - return dayjs(time) -} - -function parseStop(item) { - const $ = cheerio.load(item) - const time = $('.end-col > .duration-wrapper').data('stop') - - return dayjs(time) -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#showListContainer > table > tbody > tr').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') + +module.exports = { + site: 'tvheute.at', + days: 2, + url({ channel, date }) { + return `https://tvheute.at/part/channel-shows/partial/${channel.site_id}/${date.format( + 'DD-MM-YYYY' + )}` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: parseTitle(item), + description: parseDescription(item), + image: parseImage(item), + category: parseCategory(item), + start: parseStart(item).toJSON(), + stop: parseStop(item).toJSON() + }) + }) + + return programs + }, + async channels() { + const axios = require('axios') + const html = await axios + .get('https://tvheute.at/part/channel-selection') + .then(r => r.data) + .catch(console.log) + + let channels = [] + + const $ = cheerio.load(html) + $('.sortable-list > li').each((i, el) => { + const name = $(el).find('label').text() + const site_id = $(el).find('input').attr('value') + + channels.push({ + lang: 'de', + site_id, + name + }) + }) + + return channels + } +} + +function parseTitle(item) { + const $ = cheerio.load(item) + + return $('.title-col strong').text() +} + +function parseDescription(item) { + const $ = cheerio.load(item) + + return $('.title-col .description').text() +} + +function parseCategory(item) { + const $ = cheerio.load(item) + + return $('.station-col > .type').text() +} + +function parseImage(item) { + const $ = cheerio.load(item) + const imgSrc = $('.title-col .image img').data('src-desktop') + + return imgSrc ? `https://tvheute.at${imgSrc}` : null +} + +function parseStart(item) { + const $ = cheerio.load(item) + const time = $('.end-col > .duration-wrapper').data('start') + + return dayjs(time) +} + +function parseStop(item) { + const $ = cheerio.load(item) + const time = $('.end-col > .duration-wrapper').data('stop') + + return dayjs(time) +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('#showListContainer > table > tbody > tr').toArray() +} diff --git a/sites/tvheute.at/tvheute.at.test.js b/sites/tvheute.at/tvheute.at.test.js index cb52784c..bc39c18f 100644 --- a/sites/tvheute.at/tvheute.at.test.js +++ b/sites/tvheute.at/tvheute.at.test.js @@ -1,48 +1,45 @@ -const { parser, url } = require('./tvheute.at.config.js') -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('2021-11-08', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'orf1', xmltv_id: 'ORF1.at' } -const content = ` -

      Das ORF1 Programm mit allen Sendungen live im TV von tv.orf.at. Sie haben eine Sendung verpasst. In der ORF TVthek finden Sie viele Sendungen on demand zum Abruf als online Video und live stream.

      ORF1 heute

      SKYsp2 ORF2
      Sender Zeit Zeit Titel Start Titel
      ORF1 Kids
      Monchhichi (Wh.) ANIMATIONSSERIE Der Streiche-Wettbewerb
      Roger hat sich Ärger mit Dr. Bellows eingehandelt, der ihn für einen Monat strafversetzen möchte. Einmal mehr hadert Roger mit dem Schicksal, dass er keinen eigenen Flaschengeist besitzt, der ihm aus der Patsche helfen kann. Jeannie schlägt vor, ihm Cousine Marilla zu schicken. Doch Tony ist strikt dagegen. Als ein Zaubererpärchen im exotischen Bühnenoutfit für die Zeit von Rogers Abwesenheit sein Apartment in Untermiete bezieht, glaubt Roger, Jeannie habe ihm ihre Verwandte doch noch gesandt.
      ORF1
      ZIB 18 NACHRICHTEN
      -` - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://tvheute.at/part/channel-shows/partial/orf1/08-11-2021' - ) -}) - -it('can parse response', () => { - expect(parser({ date, channel, content })).toMatchObject([ - { - start: '2021-11-08T05:00:00.000Z', - stop: '2021-11-08T05:10:00.000Z', - title: 'Monchhichi (Wh.)', - category: 'Kids', - description: - 'Roger hat sich Ärger mit Dr. Bellows eingehandelt, der ihn für einen Monat strafversetzen möchte. Einmal mehr hadert Roger mit dem Schicksal, dass er keinen eigenen Flaschengeist besitzt, der ihm aus der Patsche helfen kann. Jeannie schlägt vor, ihm Cousine Marilla zu schicken. Doch Tony ist strikt dagegen. Als ein Zaubererpärchen im exotischen Bühnenoutfit für die Zeit von Rogers Abwesenheit sein Apartment in Untermiete bezieht, glaubt Roger, Jeannie habe ihm ihre Verwandte doch noch gesandt.', - image: 'https://tvheute.at/images/orf1/monchhichi_kids--1895216560-00.jpg' - }, - { - start: '2021-11-08T17:00:00.000Z', - stop: '2021-11-08T17:10:00.000Z', - title: 'ZIB 18' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: `Object moved -

      Object moved to here.

      -` - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./tvheute.at.config.js') +const dayjs = require('dayjs') +const path = require('path') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const { readFileSync } = require('fs') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-08', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'orf1', xmltv_id: 'ORF1.at' } + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://tvheute.at/part/channel-shows/partial/orf1/08-11-2021' + ) +}) + +it('can parse response', () => { + expect(parser({ date, channel, content: readFileSync(path.resolve(__dirname, './__data__/content.html'), 'utf8') })).toMatchObject([ + { + start: '2021-11-08T05:00:00.000Z', + stop: '2021-11-08T05:10:00.000Z', + title: 'Monchhichi (Wh.)', + category: 'Kids', + description: + 'Roger hat sich Ärger mit Dr. Bellows eingehandelt, der ihn für einen Monat strafversetzen möchte. Einmal mehr hadert Roger mit dem Schicksal, dass er keinen eigenen Flaschengeist besitzt, der ihm aus der Patsche helfen kann. Jeannie schlägt vor, ihm Cousine Marilla zu schicken. Doch Tony ist strikt dagegen. Als ein Zaubererpärchen im exotischen Bühnenoutfit für die Zeit von Rogers Abwesenheit sein Apartment in Untermiete bezieht, glaubt Roger, Jeannie habe ihm ihre Verwandte doch noch gesandt.', + image: 'https://tvheute.at/images/orf1/monchhichi_kids--1895216560-00.jpg' + }, + { + start: '2021-11-08T17:00:00.000Z', + stop: '2021-11-08T17:10:00.000Z', + title: 'ZIB 18' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: readFileSync(path.resolve(__dirname, './__data__/no_content.html'), 'utf8') + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvi.iol.pt/tvi.iol.pt.config.js b/sites/tvi.iol.pt/tvi.iol.pt.config.js index fce6b7dc..49ad4631 100644 --- a/sites/tvi.iol.pt/tvi.iol.pt.config.js +++ b/sites/tvi.iol.pt/tvi.iol.pt.config.js @@ -1,76 +1,76 @@ -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: 'tvi.iol.pt', - url({ channel, date }) { - return `https://tvi.iol.pt/emissao/dia/${channel.site_id}?data=${date.format('YYYY-MM-DD')}` - }, - parser({ content, date }) { - let programs = [] - - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - - const stop = start.add(30, 'm') - - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('.guiatv-programa > h2').text().trim() -} - -function parseDescription($item) { - return $item('.guiatv-programa > .texto, .guiatv-programa > .texto2').text().trim() || null -} - -function parseIcon($item) { - const backgroundImage = $item('.picture16x9').css('background-image') - if (!backgroundImage) return null - const [, imageUrl] = backgroundImage.match(/url\((.*)\)/) || [null, null] - if (!imageUrl) return null - - return imageUrl -} - -function parseStart($item, date) { - const timezone = 'Europe/Madrid' - const time = $item('.hora').text().trim() - - return dayjs.tz(`${date.tz(timezone).format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', timezone) -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.guiatv-linha').toArray() -} +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: 'tvi.iol.pt', + url({ channel, date }) { + return `https://tvi.iol.pt/emissao/dia/${channel.site_id}?data=${date.format('YYYY-MM-DD')}` + }, + parser({ content, date }) { + let programs = [] + + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + + const stop = start.add(30, 'm') + + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('.guiatv-programa > h2').text().trim() +} + +function parseDescription($item) { + return $item('.guiatv-programa > .texto, .guiatv-programa > .texto2').text().trim() || null +} + +function parseIcon($item) { + const backgroundImage = $item('.picture16x9').css('background-image') + if (!backgroundImage) return null + const [, imageUrl] = backgroundImage.match(/url\((.*)\)/) || [null, null] + if (!imageUrl) return null + + return imageUrl +} + +function parseStart($item, date) { + const timezone = 'Europe/Madrid' + const time = $item('.hora').text().trim() + + return dayjs.tz(`${date.tz(timezone).format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', timezone) +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.guiatv-linha').toArray() +} diff --git a/sites/tvi.iol.pt/tvi.iol.pt.test.js b/sites/tvi.iol.pt/tvi.iol.pt.test.js index 4ca367a4..2a873211 100644 --- a/sites/tvi.iol.pt/tvi.iol.pt.test.js +++ b/sites/tvi.iol.pt/tvi.iol.pt.test.js @@ -1,66 +1,66 @@ -const { parser, url } = require('./tvi.iol.pt.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-26', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'tvi', xmltv_id: 'TVI.pt' } - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://tvi.iol.pt/emissao/dia/tvi?data=2025-01-26') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - - return p - }) - - expect(results.length).toBe(16) - expect(results[0]).toMatchObject({ - title: 'As aventuras do Gato das Botas', - description: null, - icon: 'https://img.iol.pt/image/id/66d6fb1ad34e94b82904c3ce/300.jpg', - start: '2025-01-26T05:15:00.000Z', - stop: '2025-01-26T05:45:00.000Z' - }) - expect(results[5]).toMatchObject({ - title: 'Missa', - description: 'Gondomar', - icon: 'https://img.iol.pt/image/id/6218de030cf21a10a4218ba3/300.jpg', - start: '2025-01-26T09:00:00.000Z', - stop: '2025-01-26T10:00:00.000Z' - }) - expect(results[7]).toMatchObject({ - title: 'Por um Triz', - description: 'Um segundo pode mudar tudo.', - icon: 'https://img.iol.pt/image/id/6777dcffd34e94b829094756/300.jpg', - start: '2025-01-26T11:00:00.000Z', - stop: '2025-01-26T11:58:00.000Z' - }) - expect(results[15]).toMatchObject({ - title: 'As aventuras do Gato das Botas', - description: null, - icon: 'https://img.iol.pt/image/id/66d6fb1ad34e94b82904c3ce/300.jpg', - start: '2025-01-27T04:50:00.000Z', - stop: '2025-01-27T05:20:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./tvi.iol.pt.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-26', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'tvi', xmltv_id: 'TVI.pt' } + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://tvi.iol.pt/emissao/dia/tvi?data=2025-01-26') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + + return p + }) + + expect(results.length).toBe(16) + expect(results[0]).toMatchObject({ + title: 'As aventuras do Gato das Botas', + description: null, + icon: 'https://img.iol.pt/image/id/66d6fb1ad34e94b82904c3ce/300.jpg', + start: '2025-01-26T05:15:00.000Z', + stop: '2025-01-26T05:45:00.000Z' + }) + expect(results[5]).toMatchObject({ + title: 'Missa', + description: 'Gondomar', + icon: 'https://img.iol.pt/image/id/6218de030cf21a10a4218ba3/300.jpg', + start: '2025-01-26T09:00:00.000Z', + stop: '2025-01-26T10:00:00.000Z' + }) + expect(results[7]).toMatchObject({ + title: 'Por um Triz', + description: 'Um segundo pode mudar tudo.', + icon: 'https://img.iol.pt/image/id/6777dcffd34e94b829094756/300.jpg', + start: '2025-01-26T11:00:00.000Z', + stop: '2025-01-26T11:58:00.000Z' + }) + expect(results[15]).toMatchObject({ + title: 'As aventuras do Gato das Botas', + description: null, + icon: 'https://img.iol.pt/image/id/66d6fb1ad34e94b82904c3ce/300.jpg', + start: '2025-01-27T04:50:00.000Z', + stop: '2025-01-27T05:20:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/tvim.tv/__data__/content.json b/sites/tvim.tv/__data__/content.json new file mode 100644 index 00000000..a18c491b --- /dev/null +++ b/sites/tvim.tv/__data__/content.json @@ -0,0 +1 @@ +{"response":"ok","data":{"thumb":"https://mobile-api.tvim.tv/images/chan_logos/70x25/T7.png","thumb_rel":"https://mobile-api.tvim.tv/images/chan_logos/70x25/T7.png","thumb_large_rel":"https://mobile-api.tvim.tv/images/chan_logos/120x60/T7.png","thumb_http":"http://mobile-api.tvim.tv/images/chan_logos/70x25/T7.png","thumb_large":"http://mobile-api.tvim.tv/images/chan_logos/120x60/T7.png","server_time":1635100951,"catchup_length":2,"_id":"T73","ind":2,"genre":"national","name":"T7","epg_id":"T7","chan":"T7","prog":[{"id":"T7-1635026400","title":"Programi i T7","from":1635026400,"end":1635040800,"starting":"00:00","from_utc":1635026400,"end_utc":1635040800,"desc":"Programi i T7","genre":"test","chan":"T7","epg_id":"T7","eng":""}]}} \ No newline at end of file diff --git a/sites/tvim.tv/__data__/no_content.json b/sites/tvim.tv/__data__/no_content.json new file mode 100644 index 00000000..7033b039 --- /dev/null +++ b/sites/tvim.tv/__data__/no_content.json @@ -0,0 +1 @@ +{"response":"ok","data":{"server_time":1635100927}} \ No newline at end of file diff --git a/sites/tvim.tv/tvim.tv.config.js b/sites/tvim.tv/tvim.tv.config.js index f67ac2ea..48b760ad 100644 --- a/sites/tvim.tv/tvim.tv.config.js +++ b/sites/tvim.tv/tvim.tv.config.js @@ -1,61 +1,61 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'tvim.tv', - days: 2, - url: function ({ date, channel }) { - return `https://www.tvim.tv/script/program_epg?date=${date.format('DD.MM.YYYY')}&prog=${ - channel.site_id - }&server_time=true` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const start = parseStart(item) - const stop = parseStop(item) - - programs.push({ - title: item.title, - description: item.desc, - category: item.genre, - start: start.toString(), - stop: stop.toString() - }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - const data = await axios - .get('https://www.tvim.tv/script/epg/category_channels?category=all&filter=playable') - .then(r => r.data) - .catch(console.log) - - let channels = [] - data.data.forEach(item => { - channels.push({ - lang: 'sq', - site_id: item.epg_id, - name: item.name - }) - }) - - return channels - } -} - -function parseStart(item) { - return dayjs.unix(item.from_utc) -} - -function parseStop(item) { - return dayjs.unix(item.end_utc) -} - -function parseItems(content) { - const parsed = JSON.parse(content) - - return parsed.data.prog || [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'tvim.tv', + days: 2, + url: function ({ date, channel }) { + return `https://www.tvim.tv/script/program_epg?date=${date.format('DD.MM.YYYY')}&prog=${ + channel.site_id + }&server_time=true` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const start = parseStart(item) + const stop = parseStop(item) + + programs.push({ + title: item.title, + description: item.desc, + category: item.genre, + start: start.toString(), + stop: stop.toString() + }) + }) + + return programs + }, + async channels() { + const axios = require('axios') + const data = await axios + .get('https://www.tvim.tv/script/epg/category_channels?category=all&filter=playable') + .then(r => r.data) + .catch(console.log) + + let channels = [] + data.data.forEach(item => { + channels.push({ + lang: 'sq', + site_id: item.epg_id, + name: item.name + }) + }) + + return channels + } +} + +function parseStart(item) { + return dayjs.unix(item.from_utc) +} + +function parseStop(item) { + return dayjs.unix(item.end_utc) +} + +function parseItems(content) { + const parsed = JSON.parse(content) + + return parsed.data.prog || [] +} diff --git a/sites/tvim.tv/tvim.tv.test.js b/sites/tvim.tv/tvim.tv.test.js index 9232cf38..b819b777 100644 --- a/sites/tvim.tv/tvim.tv.test.js +++ b/sites/tvim.tv/tvim.tv.test.js @@ -1,40 +1,41 @@ -const { parser, url } = require('./tvim.tv.config.js') -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('2021-10-24', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'T7', xmltv_id: 'T7.rs' } -const content = - '{"response":"ok","data":{"thumb":"https://mobile-api.tvim.tv/images/chan_logos/70x25/T7.png","thumb_rel":"https://mobile-api.tvim.tv/images/chan_logos/70x25/T7.png","thumb_large_rel":"https://mobile-api.tvim.tv/images/chan_logos/120x60/T7.png","thumb_http":"http://mobile-api.tvim.tv/images/chan_logos/70x25/T7.png","thumb_large":"http://mobile-api.tvim.tv/images/chan_logos/120x60/T7.png","server_time":1635100951,"catchup_length":2,"_id":"T73","ind":2,"genre":"national","name":"T7","epg_id":"T7","chan":"T7","prog":[{"id":"T7-1635026400","title":"Programi i T7","from":1635026400,"end":1635040800,"starting":"00:00","from_utc":1635026400,"end_utc":1635040800,"desc":"Programi i T7","genre":"test","chan":"T7","epg_id":"T7","eng":""}]}}' - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe( - 'https://www.tvim.tv/script/program_epg?date=24.10.2021&prog=T7&server_time=true' - ) -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: 'Sat, 23 Oct 2021 22:00:00 GMT', - stop: 'Sun, 24 Oct 2021 02:00:00 GMT', - title: 'Programi i T7', - description: 'Programi i T7', - category: 'test' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"response":"ok","data":{"server_time":1635100927}}' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./tvim.tv.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('2021-10-24', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'T7', xmltv_id: 'T7.rs' } +const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe( + 'https://www.tvim.tv/script/program_epg?date=24.10.2021&prog=T7&server_time=true' + ) +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: 'Sat, 23 Oct 2021 22:00:00 GMT', + stop: 'Sun, 24 Oct 2021 02:00:00 GMT', + title: 'Programi i T7', + description: 'Programi i T7', + category: 'test' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvinsider.com/tvinsider.com.config.js b/sites/tvinsider.com/tvinsider.com.config.js index 9162c3bd..9e43cb04 100644 --- a/sites/tvinsider.com/tvinsider.com.config.js +++ b/sites/tvinsider.com/tvinsider.com.config.js @@ -1,127 +1,127 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const { DateTime } = require('luxon') - -module.exports = { - site: 'tvinsider.com', - days: 2, - url({ channel }) { - return `https://www.tvinsider.com/network/${channel.site_id}/schedule/` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - const episodeInfo = parseEP($item); - let start = parseStart($item, date) - if (!start) return - if (prev) { - prev.stop = start - } - const stop = start.plus({ minute: 30 }) - - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - category: parseCategory($item), - date: parseDate($item), - ...episodeInfo, - subTitles: parseSubtitle($item), - previouslyShown: parsePreviously($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const html = await axios - .get('https://www.tvinsider.com/network/5-star-max/') - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(html) - const items = $('body > main > section > select > option').toArray() - - const channels = [] - items.forEach(item => { - const name = $(item).text().trim() - const path = $(item).attr('value') - if (!path) return - const [, , site_id] = path.split('/') || [null, null, null] - if (!site_id) return - - channels.push({ - lang: 'en', - site_id, - name - }) - }) - - return channels - } -} - -function parseTitle($item) { - return $item('h3').text().trim() -} -function parseEP($item){ - const text = $item('h6').text().trim(); - const match = text.match(/Season\s+(\d+)\s*•\s*Episode\s+(\d+)/i); - - if (!match) return {}; // Return an empty object if no match, so properties are undefined later - - const season = parseInt(match[1], 10); - const episode = parseInt(match[2], 10); - - return { season, episode }; // Return an object with season and episode -} - -function parseSubtitle($item) { - return $item('h5').text().trim() -} - -function parsePreviously($item){ - const h3Text = $item('h3').text().trim(); - const isNewShow = /New$/.test(h3Text); - - if (isNewShow) { - return null; - } else { - return {}; - } -} - -function parseDescription($item) { - return $item('p').text().trim() -} - -function parseCategory($item) { - const [category] = $item('h4').text().trim().split(' • ') - - return category -} - -function parseDate($item) { - const [, date] = $item('h4').text().trim().split(' • ') - - return date -} - -function parseStart($item, date) { - let time = $item('time').text().trim() - time = `${date.format('YYYY-MM-DD')} ${time}` - - return DateTime.fromFormat(time, 'yyyy-MM-dd t', { zone: 'America/New_York' }).toUTC() -} - -function parseItems(content, date) { - const $ = cheerio.load(content) - - return $(`#${date.format('MM-DD-YYYY')}`) - .next() - .find('a') - .toArray() -} +const cheerio = require('cheerio') +const axios = require('axios') +const { DateTime } = require('luxon') + +module.exports = { + site: 'tvinsider.com', + days: 2, + url({ channel }) { + return `https://www.tvinsider.com/network/${channel.site_id}/schedule/` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + const episodeInfo = parseEP($item) + let start = parseStart($item, date) + if (!start) return + if (prev) { + prev.stop = start + } + const stop = start.plus({ minute: 30 }) + + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + category: parseCategory($item), + date: parseDate($item), + ...episodeInfo, + subTitles: parseSubtitle($item), + previouslyShown: parsePreviously($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const html = await axios + .get('https://www.tvinsider.com/network/5-star-max/') + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(html) + const items = $('body > main > section > select > option').toArray() + + const channels = [] + items.forEach(item => { + const name = $(item).text().trim() + const path = $(item).attr('value') + if (!path) return + const [, , site_id] = path.split('/') || [null, null, null] + if (!site_id) return + + channels.push({ + lang: 'en', + site_id, + name + }) + }) + + return channels + } +} + +function parseTitle($item) { + return $item('h3').text().trim() +} +function parseEP($item){ + const text = $item('h6').text().trim() + const match = text.match(/Season\s+(\d+)\s*•\s*Episode\s+(\d+)/i) + + if (!match) return {} // Return an empty object if no match, so properties are undefined later + + const season = parseInt(match[1], 10) + const episode = parseInt(match[2], 10) + + return { season, episode } // Return an object with season and episode +} + +function parseSubtitle($item) { + return $item('h5').text().trim() +} + +function parsePreviously($item){ + const h3Text = $item('h3').text().trim() + const isNewShow = /New$/.test(h3Text) + + if (isNewShow) { + return null + } else { + return {} + } +} + +function parseDescription($item) { + return $item('p').text().trim() +} + +function parseCategory($item) { + const [category] = $item('h4').text().trim().split(' • ') + + return category +} + +function parseDate($item) { + const [, date] = $item('h4').text().trim().split(' • ') + + return date +} + +function parseStart($item, date) { + let time = $item('time').text().trim() + time = `${date.format('YYYY-MM-DD')} ${time}` + + return DateTime.fromFormat(time, 'yyyy-MM-dd t', { zone: 'America/New_York' }).toUTC() +} + +function parseItems(content, date) { + const $ = cheerio.load(content) + + return $(`#${date.format('MM-DD-YYYY')}`) + .next() + .find('a') + .toArray() +} diff --git a/sites/tvinsider.com/tvinsider.com.test.js b/sites/tvinsider.com/tvinsider.com.test.js index ab6db777..ffddcf6f 100644 --- a/sites/tvinsider.com/tvinsider.com.test.js +++ b/sites/tvinsider.com/tvinsider.com.test.js @@ -1,56 +1,56 @@ -const { parser, url } = require('./tvinsider.com.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: 'movieplex', - xmltv_id: 'MoviePlexEast.us' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://www.tvinsider.com/network/movieplex/schedule/') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(15) - expect(results[0]).toMatchObject({ - start: '2025-01-18T05:45:00.000Z', - stop: '2025-01-18T07:12:00.000Z', - title: 'Wild Oats', - category: 'Feature Film', - date: '2016', - description: - 'Two best friends travel to the Canary Islands after one mistakenly receives a large amount of money.' - }) - expect(results[14]).toMatchObject({ - start: '2025-01-19T04:42:00.000Z', - stop: '2025-01-19T05:12:00.000Z', - title: 'Trading Mom', - category: 'Feature Film', - date: '1994', - description: - 'Kids (Anna Chlumsky, Aaron Michael Metchik) under magic spell shop for another mother.' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./tvinsider.com.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: 'movieplex', + xmltv_id: 'MoviePlexEast.us' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://www.tvinsider.com/network/movieplex/schedule/') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(15) + expect(results[0]).toMatchObject({ + start: '2025-01-18T05:45:00.000Z', + stop: '2025-01-18T07:12:00.000Z', + title: 'Wild Oats', + category: 'Feature Film', + date: '2016', + description: + 'Two best friends travel to the Canary Islands after one mistakenly receives a large amount of money.' + }) + expect(results[14]).toMatchObject({ + start: '2025-01-19T04:42:00.000Z', + stop: '2025-01-19T05:12:00.000Z', + title: 'Trading Mom', + category: 'Feature Film', + date: '1994', + description: + 'Kids (Anna Chlumsky, Aaron Michael Metchik) under magic spell shop for another mother.' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/tvireland.ie/tvireland.ie.config.js b/sites/tvireland.ie/tvireland.ie.config.js index c28eb8c8..10f6274b 100644 --- a/sites/tvireland.ie/tvireland.ie.config.js +++ b/sites/tvireland.ie/tvireland.ie.config.js @@ -1,99 +1,99 @@ -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: 'tvireland.ie', - days: 2, - url: function ({ date, channel }) { - return `https://www.tvireland.ie/tv/listings/channel/${channel.site_id}?dt=${date.format( - 'YYYY-MM-DD' - )}` - }, - parser: function ({ content, date, channel }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date, channel) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - const _ = require('lodash') - - const providers = ['-9000019', '-8000019', '-1000019', '-2000019', '-7000019'] - - const channels = [] - for (let provider of providers) { - const data = await axios - .post('https://www.tvireland.ie/tv/schedule', null, { - params: { - provider, - region: 'Ireland', - TVperiod: 'Night', - date: dayjs().format('YYYY-MM-DD'), - st: 0, - u_time: 2027, - is_mobile: 1 - } - }) - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(data) - $('.channelname').each((i, el) => { - const name = $(el).find('center > a:eq(1)').text() - const url = $(el).find('center > a:eq(1)').attr('href') - const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/) - - channels.push({ - lang: 'en', - name, - site_id: `${number}/${slug}` - }) - }) - } - - return _.uniqBy(channels, 'site_id') - } -} - -function parseStart($item, date) { - const timeString = $item('td:eq(0)').text().trim() - const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` - - return dayjs.tz(dateString, 'YYYY-MM-DD H:mm a', 'Europe/Dublin') -} - -function parseTitle($item) { - return $item('td:eq(1)').text().trim() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('table.table > tbody > tr').toArray() -} +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 uniqBy = require('lodash.uniqby') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'tvireland.ie', + days: 2, + url: function ({ date, channel }) { + return `https://www.tvireland.ie/tv/listings/channel/${channel.site_id}?dt=${date.format( + 'YYYY-MM-DD' + )}` + }, + parser: function ({ content, date, channel }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date, channel) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const axios = require('axios') + + const providers = ['-9000019', '-8000019', '-1000019', '-2000019', '-7000019'] + + const channels = [] + for (let provider of providers) { + const data = await axios + .post('https://www.tvireland.ie/tv/schedule', null, { + params: { + provider, + region: 'Ireland', + TVperiod: 'Night', + date: dayjs().format('YYYY-MM-DD'), + st: 0, + u_time: 2027, + is_mobile: 1 + } + }) + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(data) + $('.channelname').each((i, el) => { + const name = $(el).find('center > a:eq(1)').text() + const url = $(el).find('center > a:eq(1)').attr('href') + const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/) + + channels.push({ + lang: 'en', + name, + site_id: `${number}/${slug}` + }) + }) + } + + return uniqBy(channels, x => x.site_id) + } +} + +function parseStart($item, date) { + const timeString = $item('td:eq(0)').text().trim() + const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` + + return dayjs.tz(dateString, 'YYYY-MM-DD H:mm a', 'Europe/Dublin') +} + +function parseTitle($item) { + return $item('td:eq(1)').text().trim() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('table.table > tbody > tr').toArray() +} diff --git a/sites/tvireland.ie/tvireland.ie.test.js b/sites/tvireland.ie/tvireland.ie.test.js index 7088c129..d65eaf8e 100644 --- a/sites/tvireland.ie/tvireland.ie.test.js +++ b/sites/tvireland.ie/tvireland.ie.test.js @@ -1,50 +1,50 @@ -const { parser, url } = require('./tvireland.ie.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('2023-11-25', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2378/virgin-media-more', - xmltv_id: 'VirginMediaMore.ie' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.tvireland.ie/tv/listings/channel/2378/virgin-media-more?dt=2023-11-25' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-11-25T00:20:00.000Z', - stop: '2023-11-25T01:10:00.000Z', - title: 'Best of Rugby World Cup' - }) - - expect(results[13]).toMatchObject({ - start: '2023-11-25T23:30:00.000Z', - stop: '2023-11-26T00:00:00.000Z', - title: 'Stories from the Street' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./tvireland.ie.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('2023-11-25', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2378/virgin-media-more', + xmltv_id: 'VirginMediaMore.ie' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.tvireland.ie/tv/listings/channel/2378/virgin-media-more?dt=2023-11-25' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-11-25T00:20:00.000Z', + stop: '2023-11-25T01:10:00.000Z', + title: 'Best of Rugby World Cup' + }) + + expect(results[13]).toMatchObject({ + start: '2023-11-25T23:30:00.000Z', + stop: '2023-11-26T00:00:00.000Z', + title: 'Stories from the Street' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvkaista.org/tvkaista.org.config.js b/sites/tvkaista.org/tvkaista.org.config.js index 6647f0bc..4767d2ed 100644 --- a/sites/tvkaista.org/tvkaista.org.config.js +++ b/sites/tvkaista.org/tvkaista.org.config.js @@ -1,169 +1,169 @@ -const doFetch = require('@ntlab/sfetch') -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) - -const tz = 'Europe/Helsinki' - -module.exports = { - site: 'tvkaista.org', - days: 2, - url({ channel, date }) { - return `https://www.tvkaista.org/${channel.site_id}/${date.format('YYYY-MM-DD')}` - }, - parser({ content, date }) { - let programs = [] - const items = parseItems(content) - - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - - let start = parseStart($item, date) - let stop = parseStop($item, start) - - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } else if (stop.isBefore(start)) { - stop = stop.add(1, 'd') - date = date.add(1, 'd') - } - } else { - if (start.hour() > 18) { - start = start.subtract(1, 'd') - date = date.subtract(1, 'd') - } - } - - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - season: parseSeason($item), - episode: parseEpisode($item), - categories: parseCategories($item), - rating: parseRating($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - let channels = [] - - const queue = ['https://www.tvkaista.org/', 'https://www.tvkaista.org/maksukanavat/'] - await doFetch(queue, (url, res) => { - const $ = cheerio.load(res) - $('body > main > div > div.row > div').each((i, el) => { - const link = $(el).find('div > div > div > div.col-auto > a') - const img = link.find('img.channel-logo') - const name = link.text().trim() || img.attr('alt') - const [, site_id] = link.attr('href').split('/') - - channels.push({ - lang: 'fi', - name, - site_id - }) - }) - }) - - return channels - } -} - -function parseRating($item) { - let rating = $item( - 'div.d-flex.flex-row.bd-highlight > div.bd-highlight.flex-fill > span:nth-child(3) > img' - ).attr('alt') - - return rating - ? { - system: 'VET', - value: rating.replace(/\(|\)/g, '') - } - : null -} - -function parseCategories($item) { - return $item('div.collapse > .badge') - .map((i, el) => $item(el).text().trim()) - .get() -} - -function parseSeason($item) { - const string = $item( - 'div.d-flex.flex-row.bd-highlight > div.bd-highlight.flex-fill > span:nth-child(2)' - ) - .text() - .trim() - if (!string) return null - - let [, season] = string.match(/S(\d{2})/) || [null, null] - - return season ? parseInt(season) : null -} - -function parseEpisode($item) { - const string = $item( - 'div.d-flex.flex-row.bd-highlight > div.bd-highlight.flex-fill > span:nth-child(2)' - ) - .text() - .trim() - if (!string) return null - - let [, episode] = string.match(/E(\d{2})/) || [null, null] - - return episode ? parseInt(episode) : null -} - -function parseStart($item, date) { - const [time] = $item('div.d-flex.flex-row.bd-highlight > div.bd-highlight.me-2') - .text() - .trim() - .split('-') - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', tz) -} - -function parseStop($item, date) { - const [, time] = $item('div.d-flex.flex-row.bd-highlight > div.bd-highlight.me-2') - .text() - .trim() - .split('-') - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', tz) -} - -function parseTitle($item) { - return $item('div.d-flex.flex-row.bd-highlight > div.bd-highlight.flex-fill > span:nth-child(1)') - .text() - .trim() -} - -function parseDescription($item) { - return ( - $item('div.collapse > p') - .text() - .replace(/\n/g, '') - .replace(/\s\s+/g, ' ') - // eslint-disable-next-line no-irregular-whitespace - .replace(/ /g, ' ') - .trim() - ) -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('ul.list-group > li').toArray() -} +const doFetch = require('@ntlab/sfetch') +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) + +const tz = 'Europe/Helsinki' + +module.exports = { + site: 'tvkaista.org', + days: 2, + url({ channel, date }) { + return `https://www.tvkaista.org/${channel.site_id}/${date.format('YYYY-MM-DD')}` + }, + parser({ content, date }) { + let programs = [] + const items = parseItems(content) + + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + + let start = parseStart($item, date) + let stop = parseStop($item, start) + + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } else if (stop.isBefore(start)) { + stop = stop.add(1, 'd') + date = date.add(1, 'd') + } + } else { + if (start.hour() > 18) { + start = start.subtract(1, 'd') + date = date.subtract(1, 'd') + } + } + + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + season: parseSeason($item), + episode: parseEpisode($item), + categories: parseCategories($item), + rating: parseRating($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + let channels = [] + + const queue = ['https://www.tvkaista.org/', 'https://www.tvkaista.org/maksukanavat/'] + await doFetch(queue, (url, res) => { + const $ = cheerio.load(res) + $('body > main > div > div.row > div').each((i, el) => { + const link = $(el).find('div > div > div > div.col-auto > a') + const img = link.find('img.channel-logo') + const name = link.text().trim() || img.attr('alt') + const [, site_id] = link.attr('href').split('/') + + channels.push({ + lang: 'fi', + name, + site_id + }) + }) + }) + + return channels + } +} + +function parseRating($item) { + let rating = $item( + 'div.d-flex.flex-row.bd-highlight > div.bd-highlight.flex-fill > span:nth-child(3) > img' + ).attr('alt') + + return rating + ? { + system: 'VET', + value: rating.replace(/\(|\)/g, '') + } + : null +} + +function parseCategories($item) { + return $item('div.collapse > .badge') + .map((i, el) => $item(el).text().trim()) + .get() +} + +function parseSeason($item) { + const string = $item( + 'div.d-flex.flex-row.bd-highlight > div.bd-highlight.flex-fill > span:nth-child(2)' + ) + .text() + .trim() + if (!string) return null + + let [, season] = string.match(/S(\d{2})/) || [null, null] + + return season ? parseInt(season) : null +} + +function parseEpisode($item) { + const string = $item( + 'div.d-flex.flex-row.bd-highlight > div.bd-highlight.flex-fill > span:nth-child(2)' + ) + .text() + .trim() + if (!string) return null + + let [, episode] = string.match(/E(\d{2})/) || [null, null] + + return episode ? parseInt(episode) : null +} + +function parseStart($item, date) { + const [time] = $item('div.d-flex.flex-row.bd-highlight > div.bd-highlight.me-2') + .text() + .trim() + .split('-') + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', tz) +} + +function parseStop($item, date) { + const [, time] = $item('div.d-flex.flex-row.bd-highlight > div.bd-highlight.me-2') + .text() + .trim() + .split('-') + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', tz) +} + +function parseTitle($item) { + return $item('div.d-flex.flex-row.bd-highlight > div.bd-highlight.flex-fill > span:nth-child(1)') + .text() + .trim() +} + +function parseDescription($item) { + return ( + $item('div.collapse > p') + .text() + .replace(/\n/g, '') + .replace(/\s\s+/g, ' ') + // eslint-disable-next-line no-irregular-whitespace + .replace(/ /g, ' ') + .trim() + ) +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('ul.list-group > li').toArray() +} diff --git a/sites/tvkaista.org/tvkaista.org.test.js b/sites/tvkaista.org/tvkaista.org.test.js index 2e829f17..a8cdacf5 100644 --- a/sites/tvkaista.org/tvkaista.org.test.js +++ b/sites/tvkaista.org/tvkaista.org.test.js @@ -1,93 +1,93 @@ -const { parser, url } = require('./tvkaista.org.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) - -let date = dayjs.utc('2025-03-01', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'yle-tv1' } - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://www.tvkaista.org/yle-tv1/2025-03-01') -}) - -it('can parse response for today', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_1.html')) - - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - - return p - }) - - expect(results.length).toBe(45) - expect(results[0]).toMatchObject({ - title: 'Alice & Jack', - description: - 'Kausi 1, 2/6. Säröjä. Jack on onnellisesti naimisissa, ja on pienen tyttären isä. Yllättävä puhelu Alicelta suistaa Jackin elämän kuitenkin pois raiteiltaan. Tunteiden myllerryksessä Jack suostuu tapaamaan Alicen salassa vaimoltaa', - season: 1, - episode: 2, - rating: { - system: 'VET', - value: '12' - }, - categories: ['Sarja'], - start: '2025-02-28T21:20:00.000Z', - stop: '2025-02-28T22:04:00.000Z' - }) -}) - -it('can parse response for next day', () => { - date = dayjs.utc('2025-03-03', 'YYYY-MM-DD').startOf('d') - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_2.html')) - - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - - return p - }) - - expect(results.length).toBe(39) - expect(results[0]).toMatchObject({ - title: 'Sodan silpoma elämä', - description: - 'Oleh Stahanov haavoittui vakavasti Itä-Ukrainan rintamalla. Miten elämä rakennetaan uudelleen, kun toipuminen vaatii selviytymistä niin fyysisistä vammoista kuin henkisestä taakastakin? Ohjaus: Viivi Berghem (Suomi 2024)', - start: '2025-03-02T21:05:00.000Z', - stop: '2025-03-02T22:02:00.000Z' - }) - expect(results[5]).toMatchObject({ - title: 'La Promesa - Salaisuuksien kartano', - description: - 'Kausi 1, 3/122. Päätöksen vaikeus. Jimena pääsee lennolle Manuelin kanssa tämän tunnustettua ensin lentokilpailuun osallistumisensa. Johtaako lento näiden kahden lähentymiseen? Onko mysteerikokin henkilöllisy', - season: 1, - episode: 3, - categories: ['Sarja'], - rating: { - system: 'VET', - value: '12' - }, - start: '2025-03-03T08:00:00.000Z', - stop: '2025-03-03T08:52:00.000Z' - }) - expect(results[38]).toMatchObject({ - title: 'Unelma työstä', - description: - 'Noin miljoona suomalaista on joko työttömänä tai työskentelee osa- tai määräaikaisessa työsuhteessa. Dokumentissa tarinansa kertoo entinen työministeri, loppuun palanut oikeustieteen tohtori, akateeminen pätkätyöläinen ja nuori teatte', - start: '2025-03-03T21:15:00.000Z', - stop: '2025-03-03T22:11:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - const results = parser({ content, date }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./tvkaista.org.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) + +let date = dayjs.utc('2025-03-01', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'yle-tv1' } + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://www.tvkaista.org/yle-tv1/2025-03-01') +}) + +it('can parse response for today', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_1.html')) + + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + + return p + }) + + expect(results.length).toBe(45) + expect(results[0]).toMatchObject({ + title: 'Alice & Jack', + description: + 'Kausi 1, 2/6. Säröjä. Jack on onnellisesti naimisissa, ja on pienen tyttären isä. Yllättävä puhelu Alicelta suistaa Jackin elämän kuitenkin pois raiteiltaan. Tunteiden myllerryksessä Jack suostuu tapaamaan Alicen salassa vaimoltaa', + season: 1, + episode: 2, + rating: { + system: 'VET', + value: '12' + }, + categories: ['Sarja'], + start: '2025-02-28T21:20:00.000Z', + stop: '2025-02-28T22:04:00.000Z' + }) +}) + +it('can parse response for next day', () => { + date = dayjs.utc('2025-03-03', 'YYYY-MM-DD').startOf('d') + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_2.html')) + + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + + return p + }) + + expect(results.length).toBe(39) + expect(results[0]).toMatchObject({ + title: 'Sodan silpoma elämä', + description: + 'Oleh Stahanov haavoittui vakavasti Itä-Ukrainan rintamalla. Miten elämä rakennetaan uudelleen, kun toipuminen vaatii selviytymistä niin fyysisistä vammoista kuin henkisestä taakastakin? Ohjaus: Viivi Berghem (Suomi 2024)', + start: '2025-03-02T21:05:00.000Z', + stop: '2025-03-02T22:02:00.000Z' + }) + expect(results[5]).toMatchObject({ + title: 'La Promesa - Salaisuuksien kartano', + description: + 'Kausi 1, 3/122. Päätöksen vaikeus. Jimena pääsee lennolle Manuelin kanssa tämän tunnustettua ensin lentokilpailuun osallistumisensa. Johtaako lento näiden kahden lähentymiseen? Onko mysteerikokin henkilöllisy', + season: 1, + episode: 3, + categories: ['Sarja'], + rating: { + system: 'VET', + value: '12' + }, + start: '2025-03-03T08:00:00.000Z', + stop: '2025-03-03T08:52:00.000Z' + }) + expect(results[38]).toMatchObject({ + title: 'Unelma työstä', + description: + 'Noin miljoona suomalaista on joko työttömänä tai työskentelee osa- tai määräaikaisessa työsuhteessa. Dokumentissa tarinansa kertoo entinen työministeri, loppuun palanut oikeustieteen tohtori, akateeminen pätkätyöläinen ja nuori teatte', + start: '2025-03-03T21:15:00.000Z', + stop: '2025-03-03T22:11:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + const results = parser({ content, date }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/tvmi.mt/tvmi.mt.config.js b/sites/tvmi.mt/tvmi.mt.config.js index e2692031..a62b3f5a 100644 --- a/sites/tvmi.mt/tvmi.mt.config.js +++ b/sites/tvmi.mt/tvmi.mt.config.js @@ -1,77 +1,77 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'tvmi.mt', - days: 2, - url: function ({ date, channel }) { - return `https://tvmi.mt/schedule/${channel.site_id}/${date.format('YYYY-MM-DD')}` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - image: parseImage($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('div > div:nth-child(2) > div:nth-child(2),a > div:nth-child(2) > div:nth-child(2)') - .text() - .trim() -} - -function parseDescription($item) { - return $item('div > div:nth-child(2) > div:nth-child(3),a > div:nth-child(2) > div:nth-child(3)') - .text() - .trim() -} - -function parseImage($item) { - const bg = $item('div > div:nth-child(1) > div > div,a > div:nth-child(1) > div').data('bg') - - return bg ? `https:${bg}` : null -} - -function parseStart($item, date) { - const timeString = $item( - 'div > div:nth-child(2) > div:nth-child(1),a > div:nth-child(2) > div:nth-child(1)' - ) - .text() - .trim() - const [, HH, mm] = timeString.match(/^(\d{2}):(\d{2})/) || [null, null, null] - if (!HH || !mm) return null - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Europe/Malta') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('body > main > div.mt-8 > div').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'tvmi.mt', + days: 2, + url: function ({ date, channel }) { + return `https://tvmi.mt/schedule/${channel.site_id}/${date.format('YYYY-MM-DD')}` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + image: parseImage($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('div > div:nth-child(2) > div:nth-child(2),a > div:nth-child(2) > div:nth-child(2)') + .text() + .trim() +} + +function parseDescription($item) { + return $item('div > div:nth-child(2) > div:nth-child(3),a > div:nth-child(2) > div:nth-child(3)') + .text() + .trim() +} + +function parseImage($item) { + const bg = $item('div > div:nth-child(1) > div > div,a > div:nth-child(1) > div').data('bg') + + return bg ? `https:${bg}` : null +} + +function parseStart($item, date) { + const timeString = $item( + 'div > div:nth-child(2) > div:nth-child(1),a > div:nth-child(2) > div:nth-child(1)' + ) + .text() + .trim() + const [, HH, mm] = timeString.match(/^(\d{2}):(\d{2})/) || [null, null, null] + if (!HH || !mm) return null + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Europe/Malta') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('body > main > div.mt-8 > div').toArray() +} diff --git a/sites/tvmi.mt/tvmi.mt.test.js b/sites/tvmi.mt/tvmi.mt.test.js index 7ce6f899..f4cc4be2 100644 --- a/sites/tvmi.mt/tvmi.mt.test.js +++ b/sites/tvmi.mt/tvmi.mt.test.js @@ -1,53 +1,53 @@ -const { parser, url } = require('./tvmi.mt.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('2022-10-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2', - xmltv_id: 'TVM.mt' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe('https://tvmi.mt/schedule/2/2022-10-29') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-29T03:30:00.000Z', - stop: '2022-10-29T04:00:00.000Z', - title: 'Bizzilla', - description: - 'Storja ta’ tliet familji, tnejn minnhom miżżewġin bejniethom, u familja oħra li għalkemm mhijiex, b’daqshekk ma jfissirx li mhijiex parti ntegrali fil-kompliċitá li ilha għaddejja bejniethom għal dawn l-aħħar tletin sena.', - image: - 'https://dist4.tvmi.mt/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjMyNjEwNywiYXVkIjoiMTg4LjI0Mi40OC45MyIsImV4cCI6MTY2NzAxNjM1OH0.N4de761te_pRvWwSUnF6httRAzdukup5syejwXTUv8g/vod/663927/image.jpg' - }) - - expect(results[1]).toMatchObject({ - start: '2022-10-29T04:00:00.000Z', - stop: '2022-10-29T04:30:00.000Z', - title: 'The Adventures of Puss in Boots', - image: - 'https://dist4.tvmi.mt/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjMyNjEwNywiYXVkIjoiMTg4LjI0Mi40OC45MyIsImV4cCI6MTY2NzAxNjM1OH0.N4de761te_pRvWwSUnF6httRAzdukup5syejwXTUv8g/vod/747336/image.jpg' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '', - channel - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./tvmi.mt.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('2022-10-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2', + xmltv_id: 'TVM.mt' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe('https://tvmi.mt/schedule/2/2022-10-29') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-29T03:30:00.000Z', + stop: '2022-10-29T04:00:00.000Z', + title: 'Bizzilla', + description: + 'Storja ta’ tliet familji, tnejn minnhom miżżewġin bejniethom, u familja oħra li għalkemm mhijiex, b’daqshekk ma jfissirx li mhijiex parti ntegrali fil-kompliċitá li ilha għaddejja bejniethom għal dawn l-aħħar tletin sena.', + image: + 'https://dist4.tvmi.mt/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjMyNjEwNywiYXVkIjoiMTg4LjI0Mi40OC45MyIsImV4cCI6MTY2NzAxNjM1OH0.N4de761te_pRvWwSUnF6httRAzdukup5syejwXTUv8g/vod/663927/image.jpg' + }) + + expect(results[1]).toMatchObject({ + start: '2022-10-29T04:00:00.000Z', + stop: '2022-10-29T04:30:00.000Z', + title: 'The Adventures of Puss in Boots', + image: + 'https://dist4.tvmi.mt/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjMyNjEwNywiYXVkIjoiMTg4LjI0Mi40OC45MyIsImV4cCI6MTY2NzAxNjM1OH0.N4de761te_pRvWwSUnF6httRAzdukup5syejwXTUv8g/vod/747336/image.jpg' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '', + channel + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvmusor.hu/tvmusor.hu.config.js b/sites/tvmusor.hu/tvmusor.hu.config.js index 5b3ec2ca..a0f92bbc 100644 --- a/sites/tvmusor.hu/tvmusor.hu.config.js +++ b/sites/tvmusor.hu/tvmusor.hu.config.js @@ -1,81 +1,81 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const _ = require('lodash') - -module.exports = { - site: 'tvmusor.hu', - days: 2, - url: 'https://tvmusor.borsonline.hu/a/get-events/', - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }, - data({ channel, date }) { - const params = new URLSearchParams() - params.append( - 'data', - JSON.stringify({ - blocks: [`${channel.site_id}|${date.format('YYYY-MM-DD')}`] - }) - ) - - return params - } - }, - parser({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = dayjs(item.e) - let stop = dayjs(item.f) - if (prev) { - start = prev.stop - } - - programs.push({ - title: item.j, - category: item.h, - description: item.c, - image: parseImage(item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://tvmusor.borsonline.hu/most/') - .then(r => r.data) - .catch(console.log) - - const [, channelData] = data.match(/const CHANNEL_DATA = (.*);/) - const json = channelData.replace('},}', '}}').replace(/(\d+):/g, '"$1":') - const channels = JSON.parse(json) - - return Object.values(channels).map(item => { - return { - lang: 'hu', - site_id: item.id, - name: item.name - } - }) - } -} - -function parseImage(item) { - return item.z ? `https://tvmusor.borsonline.hu/images/events/408/${item.z}` : null -} - -function parseItems(content, channel, date) { - const data = JSON.parse(content) - if (!data || !data.data || !data.data.loadedBlocks) return [] - const blocks = data.data.loadedBlocks - const blockId = `${channel.site_id}_${date.format('YYYY-MM-DD')}` - if (!Array.isArray(blocks[blockId])) return [] - - return _.uniqBy(_.uniqBy(blocks[blockId], 'e'), 'b') -} +const axios = require('axios') +const dayjs = require('dayjs') +const uniqBy = require('lodash.uniqby') + +module.exports = { + site: 'tvmusor.hu', + days: 2, + url: 'https://tvmusor.borsonline.hu/a/get-events/', + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }, + data({ channel, date }) { + const params = new URLSearchParams() + params.append( + 'data', + JSON.stringify({ + blocks: [`${channel.site_id}|${date.format('YYYY-MM-DD')}`] + }) + ) + + return params + } + }, + parser({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel, date) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = dayjs(item.e) + let stop = dayjs(item.f) + if (prev) { + start = prev.stop + } + + programs.push({ + title: item.j, + category: item.h, + description: item.c, + image: parseImage(item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://tvmusor.borsonline.hu/most/') + .then(r => r.data) + .catch(console.log) + + const [, channelData] = data.match(/const CHANNEL_DATA = (.*);/) + const json = channelData.replace('},}', '}}').replace(/(\d+):/g, '"$1":') + const channels = JSON.parse(json) + + return Object.values(channels).map(item => { + return { + lang: 'hu', + site_id: item.id, + name: item.name + } + }) + } +} + +function parseImage(item) { + return item.z ? `https://tvmusor.borsonline.hu/images/events/408/${item.z}` : null +} + +function parseItems(content, channel, date) { + const data = JSON.parse(content) + if (!data || !data.data || !data.data.loadedBlocks) return [] + const blocks = data.data.loadedBlocks + const blockId = `${channel.site_id}_${date.format('YYYY-MM-DD')}` + if (!Array.isArray(blocks[blockId])) return [] + + return uniqBy(uniqBy(blocks[blockId], a => a.e), b => b.b) +} diff --git a/sites/tvmusor.hu/tvmusor.hu.test.js b/sites/tvmusor.hu/tvmusor.hu.test.js index 79ed7e7c..0336ff5c 100644 --- a/sites/tvmusor.hu/tvmusor.hu.test.js +++ b/sites/tvmusor.hu/tvmusor.hu.test.js @@ -1,69 +1,69 @@ -const { parser, url, request } = require('./tvmusor.hu.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('2022-11-19', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '290', - xmltv_id: 'M4Sport.hu' -} - -it('can generate valid url', () => { - expect(url).toBe('https://tvmusor.borsonline.hu/a/get-events/') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }) -}) - -it('can generate valid request data', () => { - const result = request.data({ channel, date }) - expect(result.get('data')).toBe('{"blocks":["290|2022-11-19"]}') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-18T23:30:00.000Z', - stop: '2022-11-19T00:55:00.000Z', - title: 'Rövidpályás Úszó Országos Bajnokság', - category: 'sportműsor', - description: 'Forma-1 magazin. Hírek, információk, érdekességek a Forma-1 világából.', - image: - 'https://tvmusor.borsonline.hu/images/events/408/f1e45193930943d9ee29769e0afa902aff0e4a90-better-call-saul.jpg' - }) - - expect(results[1]).toMatchObject({ - start: '2022-11-19T00:55:00.000Z', - stop: '2022-11-19T01:10:00.000Z', - title: 'Sportlövészet', - category: 'sportműsor' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"status":"error","reason":"invalid blocks"}' - }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./tvmusor.hu.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('2022-11-19', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '290', + xmltv_id: 'M4Sport.hu' +} + +it('can generate valid url', () => { + expect(url).toBe('https://tvmusor.borsonline.hu/a/get-events/') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }) +}) + +it('can generate valid request data', () => { + const result = request.data({ channel, date }) + expect(result.get('data')).toBe('{"blocks":["290|2022-11-19"]}') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-18T23:30:00.000Z', + stop: '2022-11-19T00:55:00.000Z', + title: 'Rövidpályás Úszó Országos Bajnokság', + category: 'sportműsor', + description: 'Forma-1 magazin. Hírek, információk, érdekességek a Forma-1 világából.', + image: + 'https://tvmusor.borsonline.hu/images/events/408/f1e45193930943d9ee29769e0afa902aff0e4a90-better-call-saul.jpg' + }) + + expect(results[1]).toMatchObject({ + start: '2022-11-19T00:55:00.000Z', + stop: '2022-11-19T01:10:00.000Z', + title: 'Sportlövészet', + category: 'sportműsor' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"status":"error","reason":"invalid blocks"}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvmustra.hu/tvmustra.hu.config.js b/sites/tvmustra.hu/tvmustra.hu.config.js index ca59ea39..0374d37e 100644 --- a/sites/tvmustra.hu/tvmustra.hu.config.js +++ b/sites/tvmustra.hu/tvmustra.hu.config.js @@ -1,78 +1,78 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const { DateTime } = require('luxon') - -module.exports = { - site: 'tvmustra.hu', - days: 2, - url({ channel, date }) { - return `https://www.tvmustra.hu/tvmusor/${channel.site_id}/${date.format('YYYY-MM-DD')}` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (!start) return - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ minute: 30 }) - - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const html = await axios - .get('https://www.tvmustra.hu/') - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(html) - const items = $('.channel-selector option').toArray() - - const channels = [] - items.forEach(item => { - const name = $(item).text().trim() - const site_id = $(item).attr('value').trim() - if (!site_id) return - - channels.push({ - lang: 'hu', - site_id, - name - }) - }) - - return channels - } -} - -function parseTitle($item) { - return $item('.musor_lista_cim, .musor_lista_cim2').text().trim() -} - -function parseStart($item, date) { - const time = $item('.musor_lista_idopont, .musor_lista_idopont2').text().trim() - - return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { - zone: 'Europe/Budapest' - }).toUTC() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#epg-container > div:nth-child(4) > div.col-6_sor3 > div.showtime').toArray() -} +const cheerio = require('cheerio') +const axios = require('axios') +const { DateTime } = require('luxon') + +module.exports = { + site: 'tvmustra.hu', + days: 2, + url({ channel, date }) { + return `https://www.tvmustra.hu/tvmusor/${channel.site_id}/${date.format('YYYY-MM-DD')}` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (!start) return + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ minute: 30 }) + + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const html = await axios + .get('https://www.tvmustra.hu/') + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(html) + const items = $('.channel-selector option').toArray() + + const channels = [] + items.forEach(item => { + const name = $(item).text().trim() + const site_id = $(item).attr('value').trim() + if (!site_id) return + + channels.push({ + lang: 'hu', + site_id, + name + }) + }) + + return channels + } +} + +function parseTitle($item) { + return $item('.musor_lista_cim, .musor_lista_cim2').text().trim() +} + +function parseStart($item, date) { + const time = $item('.musor_lista_idopont, .musor_lista_idopont2').text().trim() + + return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { + zone: 'Europe/Budapest' + }).toUTC() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('#epg-container > div:nth-child(4) > div.col-6_sor3 > div.showtime').toArray() +} diff --git a/sites/tvmustra.hu/tvmustra.hu.test.js b/sites/tvmustra.hu/tvmustra.hu.test.js index a1093629..ce7a059f 100644 --- a/sites/tvmustra.hu/tvmustra.hu.test.js +++ b/sites/tvmustra.hu/tvmustra.hu.test.js @@ -1,47 +1,47 @@ -const { parser, url } = require('./tvmustra.hu.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-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'M1HD', - xmltv_id: 'M1HD.hu' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://www.tvmustra.hu/tvmusor/M1HD/2025-01-17') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(98) - expect(results[0]).toMatchObject({ - start: '2025-01-17T05:00:00.000Z', - stop: '2025-01-17T05:30:00.000Z', - title: 'HÍRADÓ' - }) - expect(results[97]).toMatchObject({ - start: '2025-01-18T04:00:00.000Z', - stop: '2025-01-18T04:30:00.000Z', - title: 'Ma éjszaka' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - content: '' - }) - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./tvmustra.hu.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-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'M1HD', + xmltv_id: 'M1HD.hu' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://www.tvmustra.hu/tvmusor/M1HD/2025-01-17') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(98) + expect(results[0]).toMatchObject({ + start: '2025-01-17T05:00:00.000Z', + stop: '2025-01-17T05:30:00.000Z', + title: 'HÍRADÓ' + }) + expect(results[97]).toMatchObject({ + start: '2025-01-18T04:00:00.000Z', + stop: '2025-01-18T04:30:00.000Z', + title: 'Ma éjszaka' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + content: '' + }) + expect(results).toMatchObject([]) +}) diff --git a/sites/tvpassport.com/tvpassport.com.config.js b/sites/tvpassport.com/tvpassport.com.config.js index 95adc874..b15ec990 100644 --- a/sites/tvpassport.com/tvpassport.com.config.js +++ b/sites/tvpassport.com/tvpassport.com.config.js @@ -1,182 +1,182 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const doFetch = require('@ntlab/sfetch') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'tvpassport.com', - days: 3, - url({ channel, date }) { - return `https://www.tvpassport.com/tv-listings/stations/${channel.site_id}/${date.format( - 'YYYY-MM-DD' - )}` - }, - request: { - timeout: 30000, - headers: { - Cookie: 'cisession=e49ff13191d6875887193cae9e324b44ef85768d;' - } - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - for (let item of items) { - const $item = cheerio.load(item) - const start = parseStart($item) - const duration = parseDuration($item) - const stop = start.add(duration, 'm') - let title = parseTitle($item) - let subtitle = parseSubTitle($item) - if (title === 'Movie') { - title = subtitle - subtitle = null - } - - programs.push({ - title, - subtitle, - description: parseDescription($item), - image: parseImage($item), - category: parseCategory($item), - rating: parseRating($item), - actors: parseActors($item), - guest: parseGuest($item), - director: parseDirector($item), - year: parseYear($item), - start, - stop - }) - } - - return programs - }, - async channels() { - function wait(ms) { - return new Promise(resolve => { - setTimeout(resolve, ms) - }) - } - - const xml = await axios - .get('https://www.tvpassport.com/sitemap.stations.xml') - .then(r => r.data) - .catch(console.error) - - const $ = cheerio.load(xml) - - const elements = $('loc').toArray() - const queue = elements.map(el => $(el).text()) - const total = queue.length - - let i = 1 - const channels = [] - - await doFetch(queue, async (url, res) => { - if (!res) return - - const [, site_id] = url.match(/\/tv-listings\/stations\/(.*)$/) - - console.log(`[${i}/${total}]`, url) - - await wait(1000) - - const $channelPage = cheerio.load(res) - const title = $channelPage('meta[property="og:title"]').attr('content') - const name = title.replace('TV Schedule for ', '') - - channels.push({ - lang: 'en', - site_id, - name - }) - - i++ - }) - - return channels - } -} - -function parseDescription($item) { - return $item('*').data('description') -} - -function parseImage($item) { - const showpicture = $item('*').data('showpicture') - const url = new URL(showpicture, 'https://cdn.tvpassport.com/image/show/960x540/') - - return url.href -} - -function parseTitle($item) { - return $item('*').data('showname').toString() -} - -function parseSubTitle($item) { - return $item('*').data('episodetitle').toString() || null -} - -function parseYear($item) { - return $item('*').data('year').toString() || null -} - -function parseCategory($item) { - const showtype = $item('*').data('showtype') - - return showtype ? showtype.split(', ') : [] -} - -function parseActors($item) { - const cast = $item('*').data('cast') - - return cast ? cast.split(', ') : [] -} - -function parseDirector($item) { - const director = $item('*').data('director') - - return director ? director.split(', ') : [] -} - -function parseGuest($item) { - const guest = $item('*').data('guest') - - return guest ? guest.split(', ') : [] -} - -function parseRating($item) { - const rating = $item('*').data('rating') - - return rating - ? { - system: 'MPA', - value: rating.replace(/^TV/, 'TV-') - } - : null -} - -function parseStart($item) { - const time = $item('*').data('st') - - return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss', 'America/New_York') -} - -function parseDuration($item) { - const duration = $item('*').data('duration') - - return parseInt(duration) -} - -function parseItems(content) { - if (!content) return [] - const $ = cheerio.load(content) - - return $('.station-listings .list-group-item').toArray() -} +const axios = require('axios') +const dayjs = require('dayjs') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const doFetch = require('@ntlab/sfetch') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'tvpassport.com', + days: 3, + url({ channel, date }) { + return `https://www.tvpassport.com/tv-listings/stations/${channel.site_id}/${date.format( + 'YYYY-MM-DD' + )}` + }, + request: { + timeout: 30000, + headers: { + Cookie: 'cisession=e49ff13191d6875887193cae9e324b44ef85768d;' + } + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + for (let item of items) { + const $item = cheerio.load(item) + const start = parseStart($item) + const duration = parseDuration($item) + const stop = start.add(duration, 'm') + let title = parseTitle($item) + let subtitle = parseSubTitle($item) + if (title === 'Movie') { + title = subtitle + subtitle = null + } + + programs.push({ + title, + subtitle, + description: parseDescription($item), + image: parseImage($item), + category: parseCategory($item), + rating: parseRating($item), + actors: parseActors($item), + guest: parseGuest($item), + director: parseDirector($item), + year: parseYear($item), + start, + stop + }) + } + + return programs + }, + async channels() { + function wait(ms) { + return new Promise(resolve => { + setTimeout(resolve, ms) + }) + } + + const xml = await axios + .get('https://www.tvpassport.com/sitemap.stations.xml') + .then(r => r.data) + .catch(console.error) + + const $ = cheerio.load(xml) + + const elements = $('loc').toArray() + const queue = elements.map(el => $(el).text()) + const total = queue.length + + let i = 1 + const channels = [] + + await doFetch(queue, async (url, res) => { + if (!res) return + + const [, site_id] = url.match(/\/tv-listings\/stations\/(.*)$/) + + console.log(`[${i}/${total}]`, url) + + await wait(1000) + + const $channelPage = cheerio.load(res) + const title = $channelPage('meta[property="og:title"]').attr('content') + const name = title.replace('TV Schedule for ', '') + + channels.push({ + lang: 'en', + site_id, + name + }) + + i++ + }) + + return channels + } +} + +function parseDescription($item) { + return $item('*').data('description') +} + +function parseImage($item) { + const showpicture = $item('*').data('showpicture') + const url = new URL(showpicture, 'https://cdn.tvpassport.com/image/show/960x540/') + + return url.href +} + +function parseTitle($item) { + return $item('*').data('showname').toString() +} + +function parseSubTitle($item) { + return $item('*').data('episodetitle').toString() || null +} + +function parseYear($item) { + return $item('*').data('year').toString() || null +} + +function parseCategory($item) { + const showtype = $item('*').data('showtype') + + return showtype ? showtype.split(', ') : [] +} + +function parseActors($item) { + const cast = $item('*').data('cast') + + return cast ? cast.split(', ') : [] +} + +function parseDirector($item) { + const director = $item('*').data('director') + + return director ? director.split(', ') : [] +} + +function parseGuest($item) { + const guest = $item('*').data('guest') + + return guest ? guest.split(', ') : [] +} + +function parseRating($item) { + const rating = $item('*').data('rating') + + return rating + ? { + system: 'MPA', + value: rating.replace(/^TV/, 'TV-') + } + : null +} + +function parseStart($item) { + const time = $item('*').data('st') + + return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss', 'America/New_York') +} + +function parseDuration($item) { + const duration = $item('*').data('duration') + + return parseInt(duration) +} + +function parseItems(content) { + if (!content) return [] + const $ = cheerio.load(content) + + return $('.station-listings .list-group-item').toArray() +} diff --git a/sites/tvpassport.com/tvpassport.com.test.js b/sites/tvpassport.com/tvpassport.com.test.js index 2bff8707..60f79e8f 100644 --- a/sites/tvpassport.com/tvpassport.com.test.js +++ b/sites/tvpassport.com/tvpassport.com.test.js @@ -1,76 +1,76 @@ -const { parser, url, request } = require('./tvpassport.com.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('2022-10-04', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'youtoo-america-network/5463', - xmltv_id: 'YTATV.us' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.tvpassport.com/tv-listings/stations/youtoo-america-network/5463/2022-10-04' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - Cookie: 'cisession=e49ff13191d6875887193cae9e324b44ef85768d;' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - - let results = parser({ content }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-04T10:00:00.000Z', - stop: '2022-10-04T10:30:00.000Z', - title: 'Charlie Moore: No Offense', - subtitle: 'Under the Influencer', - category: ['Sports', 'Outdoors'], - image: 'https://cdn.tvpassport.com/image/show/960x540/69103.jpg', - rating: { - system: 'MPA', - value: 'TV-G' - }, - actors: ['John Reardon', 'Mayko Nguyen', 'Justin Kelly'], - director: ['Rob McElhenney'], - guest: ['Sean Penn'], - description: - 'Celebrity interviews while fishing in various locations throughout the United States.', - year: null - }) - - expect(results[1]).toMatchObject({ - start: '2022-10-04T10:30:00.000Z', - stop: '2022-10-04T11:00:00.000Z', - title: '1900', - year: null - }) - - expect(results[2]).toMatchObject({ - start: '2022-10-04T11:00:00.000Z', - stop: '2022-10-04T12:00:00.000Z', - title: 'The Mark of Zorro', - subtitle: null, - year: '1940' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ content: '' }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./tvpassport.com.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('2022-10-04', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'youtoo-america-network/5463', + xmltv_id: 'YTATV.us' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.tvpassport.com/tv-listings/stations/youtoo-america-network/5463/2022-10-04' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + Cookie: 'cisession=e49ff13191d6875887193cae9e324b44ef85768d;' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + + let results = parser({ content }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-04T10:00:00.000Z', + stop: '2022-10-04T10:30:00.000Z', + title: 'Charlie Moore: No Offense', + subtitle: 'Under the Influencer', + category: ['Sports', 'Outdoors'], + image: 'https://cdn.tvpassport.com/image/show/960x540/69103.jpg', + rating: { + system: 'MPA', + value: 'TV-G' + }, + actors: ['John Reardon', 'Mayko Nguyen', 'Justin Kelly'], + director: ['Rob McElhenney'], + guest: ['Sean Penn'], + description: + 'Celebrity interviews while fishing in various locations throughout the United States.', + year: null + }) + + expect(results[1]).toMatchObject({ + start: '2022-10-04T10:30:00.000Z', + stop: '2022-10-04T11:00:00.000Z', + title: '1900', + year: null + }) + + expect(results[2]).toMatchObject({ + start: '2022-10-04T11:00:00.000Z', + stop: '2022-10-04T12:00:00.000Z', + title: 'The Mark of Zorro', + subtitle: null, + year: '1940' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ content: '' }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvplus.com.tr/tvplus.com.tr.channels.xml b/sites/tvplus.com.tr/tvplus.com.tr.channels.xml index f0fcc625..f7370389 100644 --- a/sites/tvplus.com.tr/tvplus.com.tr.channels.xml +++ b/sites/tvplus.com.tr/tvplus.com.tr.channels.xml @@ -1,153 +1,153 @@ - - - EKOL TV - ŞÖMİNE PLUS - tabii spor - tabii TV - TV 2020 - TV+ Info - 24 - 360 - A2 - KIBRIS ADA TV - A HABER - AKİT TV - AL JAZEERA ARABIC - AL JAZEERA ENGLISH - A NEWS - A PARA - A SPOR - AS TV - ATV - BABYTV - BBC News - BENGÜTÜRK - BEYAZ TV - BİZİM EV TV - BLOOMBERG HT - Bloomberg - BluTV Play 1 - BluTV Play 2 - BRT 1 - BRT 2 - CARTOONITO - CARTOON NETWORK - ÇİFTÇİ TV - CNBC-E - CNN International - CNN TÜRK - DA VINCI - DISCOVERY CHANNEL - DISNEY JUNIOR - DİYANET TV - DMAX - DREAM TÜRK - DUCK TV - DEUTSCHE WELLE ENGLISH - EKOTÜRK - ENGLISH CLUB TV - EPIC DRAMA - EURONEWS - EUROSPORT 1 - EUROSPORT 2 - FB TV - FLASH HABER - FM TV - FRANCE 24 ARABIC - FRANCE 24 ENGLISH - FX - GZT TV - HABER GLOBAL - HABERTÜRK - HALK TV - HT SPOR - KADIRGA TV - KANAL 7 - KANAL 23 - KANAL 26 - KANAL 33 - KANAL D - KANAL V - KIBRIS GENC TV - KANAL T - KIBRIS TV - KONTV - KRT TV - LOVE NATURE - MELTEM TV - MİNİKA ÇOCUK - MİNİKA GO - MOONBUG KIDS TV - NATIONAL GEOGRAPHIC - NATIONAL GEOGRAPHIC WILD - NBA TV - NICKELODEON - Nick JR - NICKTOONS - NOW - NTV - NR1 DAMAR - NUMBER1 TURK - NUMBER1 TV - ON6 - POWER TURK - POWER TV - SEMERKAND - SHOW TV - SİNEMA TV 2 - SİNEMA TV 1001 - SİNEMA 1002 - SİNEMA AİLE 2 - SİNEMA AİLE - SİNEMA AKSİYON 2 - SİNEMA TV AKSİYON - SİNEMA KOMEDİ - SİNEMA TV - SİNEMA YERLİ 2 - SİNEMA YERLİ - SKY NEWS ARABIA - SÖZCÜ TV - SPACETOON - SPORTS TV - S SPORT 2 - S SPORT - STAR TV - TARIH TV - TARIM TV - TELE1 - TEVE2 - TGRT HABER - TİVİ6 - TLC - TMB TV - TRT1 - TRT 2 - TRT 3 - TRT ARABI - TRT AVAZ - TRT BELGESEL - TRT ÇOCUK - TRT DIYANET COCUK - TRT EBA - TRT HABER - TRT KURDİ - TRT MÜZİK - TRT SPOR - TRT SPOR YILDIZ - TRT TÜRK - TRT World - TURKHABER - TV 4 - TV5 - TV5 MONDE - TV8 - TV8,5 - TV100 - TVNET - TYT TÜRK - ÜLKE TV - ULUSAL KANAL - VAV TV - VIASAT EXPLORE - VIASAT HISTORY - + + + EKOL TV + ŞÖMİNE PLUS + tabii spor + tabii TV + TV 2020 + TV+ Info + 24 + 360 + A2 + KIBRIS ADA TV + A HABER + AKİT TV + AL JAZEERA ARABIC + AL JAZEERA ENGLISH + A NEWS + A PARA + A SPOR + AS TV + ATV + BABYTV + BBC News + BENGÜTÜRK + BEYAZ TV + BİZİM EV TV + BLOOMBERG HT + Bloomberg + BluTV Play 1 + BluTV Play 2 + BRT 1 + BRT 2 + CARTOONITO + CARTOON NETWORK + ÇİFTÇİ TV + CNBC-E + CNN International + CNN TÜRK + DA VINCI + DISCOVERY CHANNEL + DISNEY JUNIOR + DİYANET TV + DMAX + DREAM TÜRK + DUCK TV + DEUTSCHE WELLE ENGLISH + EKOTÜRK + ENGLISH CLUB TV + EPIC DRAMA + EURONEWS + EUROSPORT 1 + EUROSPORT 2 + FB TV + FLASH HABER + FM TV + FRANCE 24 ARABIC + FRANCE 24 ENGLISH + FX + GZT TV + HABER GLOBAL + HABERTÜRK + HALK TV + HT SPOR + KADIRGA TV + KANAL 7 + KANAL 23 + KANAL 26 + KANAL 33 + KANAL D + KANAL V + KIBRIS GENC TV + KANAL T + KIBRIS TV + KONTV + KRT TV + LOVE NATURE + MELTEM TV + MİNİKA ÇOCUK + MİNİKA GO + MOONBUG KIDS TV + NATIONAL GEOGRAPHIC + NATIONAL GEOGRAPHIC WILD + NBA TV + NICKELODEON + Nick JR + NICKTOONS + NOW + NTV + NR1 DAMAR + NUMBER1 TURK + NUMBER1 TV + ON6 + POWER TURK + POWER TV + SEMERKAND + SHOW TV + SİNEMA TV 2 + SİNEMA TV 1001 + SİNEMA 1002 + SİNEMA AİLE 2 + SİNEMA AİLE + SİNEMA AKSİYON 2 + SİNEMA TV AKSİYON + SİNEMA KOMEDİ + SİNEMA TV + SİNEMA YERLİ 2 + SİNEMA YERLİ + SKY NEWS ARABIA + SÖZCÜ TV + SPACETOON + SPORTS TV + S SPORT 2 + S SPORT + STAR TV + TARIH TV + TARIM TV + TELE1 + TEVE2 + TGRT HABER + TİVİ6 + TLC + TMB TV + TRT1 + TRT 2 + TRT 3 + TRT ARABI + TRT AVAZ + TRT BELGESEL + TRT ÇOCUK + TRT DIYANET COCUK + TRT EBA + TRT HABER + TRT KURDİ + TRT MÜZİK + TRT SPOR + TRT SPOR YILDIZ + TRT TÜRK + TRT World + TURKHABER + TV 4 + TV5 + TV5 MONDE + TV8 + TV8,5 + TV100 + TVNET + TYT TÜRK + ÜLKE TV + ULUSAL KANAL + VAV TV + VIASAT EXPLORE + VIASAT HISTORY + diff --git a/sites/tvplus.com.tr/tvplus.com.tr.config.js b/sites/tvplus.com.tr/tvplus.com.tr.config.js index a25681ae..07b6b4d4 100644 --- a/sites/tvplus.com.tr/tvplus.com.tr.config.js +++ b/sites/tvplus.com.tr/tvplus.com.tr.config.js @@ -1,102 +1,102 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const debug = require('debug')('site:tvplus.com.tr') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -const baseUrl = 'https://tvplus.com.tr/canli-tv/yayin-akisi' - -module.exports = { - site: 'tvplus.com.tr', - days: 2, - request: { - cache: { - ttl: 24 * 60 * 60 * 1000 // 1 day - } - }, - async url({ channel }) { - if (module.exports.buildId === undefined) { - module.exports.buildId = await module.exports.fetchBuildId() - debug('Got build id', module.exports.buildId) - } - const channelId = channel.site_id.replace('/', '--') - return `https://tvplus.com.tr/_next/data/${module.exports.buildId}/${channel.lang}/canli-tv/yayin-akisi/${channelId}.json?title=${channelId}` - }, - parser({ content, date }) { - const programs = [] - if (content) { - const data = JSON.parse(content) - if (Array.isArray(data?.pageProps?.allPlaybillList)) { - data.pageProps.allPlaybillList - .filter(i => i.length && i[0].starttime.startsWith(date.format('YYYY-MM-DD'))) - .forEach(i => { - for (const schedule of i) { - const [, season, episode] = schedule.seasonInfo?.match( - /(\d+)\. Sezon - (\d+)\. Bölüm/ - ) || [null, null, null] - programs.push({ - title: schedule.name, - description: schedule.introduce, - category: schedule.genres, - image: schedule.picture, - season: season ? parseInt(season) : null, - episode: episode ? parseInt(episode) : null, - start: dayjs.utc(schedule.starttime), - stop: dayjs.utc(schedule.endtime) - }) - } - }) - } - } - - return programs - }, - async channels() { - if (module.exports.buildId === undefined) { - module.exports.buildId = await module.exports.fetchBuildId() - debug('Got build id', module.exports.buildId) - } - const channels = [] - const data = await axios - .get(`https://tvplus.com.tr/_next/data/${module.exports.buildId}/canli-tv/yayin-akisi.json`) - .then(r => r.data) - .catch(console.error) - - const channels_json = data.pageProps.channelListSsr - - channels_json.forEach(channel => { - channels.push({ - lang: 'tr', - name: channel.name, - site_id: channel.name.normalize('NFD') // Decompose accented characters - .replace(/[\u0300-\u036f]/g, '') // Remove accent marks - .toLowerCase() - .replace(/\s+/g, '-') // Replace spaces with hyphens - .replace(/[^a-zA-Z0-9-]/g, '') // Remove special chars but keep hyphens - .replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens - + '/' + channel.id, - logo: channel.channelLogo - }) - }) - - return channels - }, - async fetchBuildId() { - const data = await axios - .get(baseUrl) - .then(r => r.data) - .catch(console.error) - - if (data) { - const $ = cheerio.load(data) - const nextData = JSON.parse($('#__NEXT_DATA__').text()) - return nextData?.buildId || null - } else { - return null - } - } -} +const cheerio = require('cheerio') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const debug = require('debug')('site:tvplus.com.tr') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +const baseUrl = 'https://tvplus.com.tr/canli-tv/yayin-akisi' + +module.exports = { + site: 'tvplus.com.tr', + days: 2, + request: { + cache: { + ttl: 24 * 60 * 60 * 1000 // 1 day + } + }, + async url({ channel }) { + if (module.exports.buildId === undefined) { + module.exports.buildId = await module.exports.fetchBuildId() + debug('Got build id', module.exports.buildId) + } + const channelId = channel.site_id.replace('/', '--') + return `https://tvplus.com.tr/_next/data/${module.exports.buildId}/${channel.lang}/canli-tv/yayin-akisi/${channelId}.json?title=${channelId}` + }, + parser({ content, date }) { + const programs = [] + if (content) { + const data = JSON.parse(content) + if (Array.isArray(data?.pageProps?.allPlaybillList)) { + data.pageProps.allPlaybillList + .filter(i => i.length && i[0].starttime.startsWith(date.format('YYYY-MM-DD'))) + .forEach(i => { + for (const schedule of i) { + const [, season, episode] = schedule.seasonInfo?.match( + /(\d+)\. Sezon - (\d+)\. Bölüm/ + ) || [null, null, null] + programs.push({ + title: schedule.name, + description: schedule.introduce, + category: schedule.genres, + image: schedule.picture, + season: season ? parseInt(season) : null, + episode: episode ? parseInt(episode) : null, + start: dayjs.utc(schedule.starttime), + stop: dayjs.utc(schedule.endtime) + }) + } + }) + } + } + + return programs + }, + async channels() { + if (module.exports.buildId === undefined) { + module.exports.buildId = await module.exports.fetchBuildId() + debug('Got build id', module.exports.buildId) + } + const channels = [] + const data = await axios + .get(`https://tvplus.com.tr/_next/data/${module.exports.buildId}/canli-tv/yayin-akisi.json`) + .then(r => r.data) + .catch(console.error) + + const channels_json = data.pageProps.channelListSsr + + channels_json.forEach(channel => { + channels.push({ + lang: 'tr', + name: channel.name, + site_id: channel.name.normalize('NFD') // Decompose accented characters + .replace(/[\u0300-\u036f]/g, '') // Remove accent marks + .toLowerCase() + .replace(/\s+/g, '-') // Replace spaces with hyphens + .replace(/[^a-zA-Z0-9-]/g, '') // Remove special chars but keep hyphens + .replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens + + '/' + channel.id, + logo: channel.channelLogo + }) + }) + + return channels + }, + async fetchBuildId() { + const data = await axios + .get(baseUrl) + .then(r => r.data) + .catch(console.error) + + if (data) { + const $ = cheerio.load(data) + const nextData = JSON.parse($('#__NEXT_DATA__').text()) + return nextData?.buildId || null + } else { + return null + } + } +} diff --git a/sites/tvplus.com.tr/tvplus.com.tr.test.js b/sites/tvplus.com.tr/tvplus.com.tr.test.js index d7586cda..b2950abe 100644 --- a/sites/tvplus.com.tr/tvplus.com.tr.test.js +++ b/sites/tvplus.com.tr/tvplus.com.tr.test.js @@ -1,77 +1,77 @@ -const { parser, url } = require('./tvplus.com.tr.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('2024-12-15', 'YYYY-MM-DD').startOf('d') -const channel = { - lang: 'tr', - site_id: 'nick-jr/4353', - xmltv_id: 'NickJr.tr' -} - -axios.get.mockImplementation(url => { - if (url === 'https://tvplus.com.tr/canli-tv/yayin-akisi') { - return Promise.resolve({ - data: fs.readFileSync(path.join(__dirname, '__data__', 'build.html')).toString() - }) - } -}) - -it('can generate valid url', async () => { - expect(await url({ channel })).toBe( - 'https://tvplus.com.tr/_next/data/kUzvz_bbQJNaShlFUkrR3/tr/canli-tv/yayin-akisi/nick-jr--4353.json?title=nick-jr--4353' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.json')) - const results = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(88) - expect(results[0]).toMatchObject({ - start: '2024-12-14T21:10:00.000Z', - stop: '2024-12-14T21:20:00.000Z', - title: 'Camgöz (2020)', - description: - "Max'in Camgöz adında yarı köpek balığı yarı köpek eşsiz bir evcil havyanı vardır. İlk başlarda Camgöz'ü saklamaya çalışsa da Sisli Pınarlar'da, en iyi arkadaşlar, meraklı komşular ve hatta Max'in ailesi bile yaramaz yeni arkadaşını fark edecektir.", - image: - 'https://gbzeottvsc01.tvplus.com.tr:33207/CPS/images/universal/film/program/202412/20241209/21/2126356250845eb88428_0_XL.jpg', - category: 'Çocuk', - season: 1, - episode: 116 - }) - expect(results[10]).toMatchObject({ - start: '2024-12-14T23:00:00.000Z', - stop: '2024-12-14T23:25:00.000Z', - title: 'Blaze ve Yol Canavarları', - description: - 'Blaze ve Yol Canavarları, dünyanın en büyük canavar kamyonu Blaze ve en iyi arkadaşı ve sürücüsü AJ adında bir çocuk hakkındaki interaktif bir anaokulu animasyon dizisidir.', - image: - 'https://gbzeottvsc01.tvplus.com.tr:33207/CPS/images/universal/film/program/202412/20241209/94/2126356271145eb88428_0_XL.jpg', - category: 'Çocuk', - season: 6, - episode: 617 - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./tvplus.com.tr.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('2024-12-15', 'YYYY-MM-DD').startOf('d') +const channel = { + lang: 'tr', + site_id: 'nick-jr/4353', + xmltv_id: 'NickJr.tr' +} + +axios.get.mockImplementation(url => { + if (url === 'https://tvplus.com.tr/canli-tv/yayin-akisi') { + return Promise.resolve({ + data: fs.readFileSync(path.join(__dirname, '__data__', 'build.html')).toString() + }) + } +}) + +it('can generate valid url', async () => { + expect(await url({ channel })).toBe( + 'https://tvplus.com.tr/_next/data/kUzvz_bbQJNaShlFUkrR3/tr/canli-tv/yayin-akisi/nick-jr--4353.json?title=nick-jr--4353' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.json')) + const results = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(88) + expect(results[0]).toMatchObject({ + start: '2024-12-14T21:10:00.000Z', + stop: '2024-12-14T21:20:00.000Z', + title: 'Camgöz (2020)', + description: + "Max'in Camgöz adında yarı köpek balığı yarı köpek eşsiz bir evcil havyanı vardır. İlk başlarda Camgöz'ü saklamaya çalışsa da Sisli Pınarlar'da, en iyi arkadaşlar, meraklı komşular ve hatta Max'in ailesi bile yaramaz yeni arkadaşını fark edecektir.", + image: + 'https://gbzeottvsc01.tvplus.com.tr:33207/CPS/images/universal/film/program/202412/20241209/21/2126356250845eb88428_0_XL.jpg', + category: 'Çocuk', + season: 1, + episode: 116 + }) + expect(results[10]).toMatchObject({ + start: '2024-12-14T23:00:00.000Z', + stop: '2024-12-14T23:25:00.000Z', + title: 'Blaze ve Yol Canavarları', + description: + 'Blaze ve Yol Canavarları, dünyanın en büyük canavar kamyonu Blaze ve en iyi arkadaşı ve sürücüsü AJ adında bir çocuk hakkındaki interaktif bir anaokulu animasyon dizisidir.', + image: + 'https://gbzeottvsc01.tvplus.com.tr:33207/CPS/images/universal/film/program/202412/20241209/94/2126356271145eb88428_0_XL.jpg', + category: 'Çocuk', + season: 6, + episode: 617 + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvprofil.com/tvprofil.com.channels.xml b/sites/tvprofil.com/tvprofil.com.channels.xml index 0cd516b1..40276310 100644 --- a/sites/tvprofil.com/tvprofil.com.channels.xml +++ b/sites/tvprofil.com/tvprofil.com.channels.xml @@ -1,9098 +1,9098 @@ - - - 2X2 - 4Fun Dance - 4Fun Kids - 4FunTV - 7/8 TV - 13 Ulica - 24 Kitchen - 24Kitchen BG - 360 TuneBox - A2TV TR - Active Family - Adventure HD - AGRO TV BG - a Haber - Ale Kino+ - Alfa BG - ALFA TVP - Al Jazeera - Al Jazeera Balkans - AMC - AMC PL - a News - A Para - ARTE FR - ATV1 - ATV2 - Auto Motor und Sport - AXN BG - AXN Black BG - AXN Black PL - AXN PL - AXN Spin PL - AXN White BG - AXN White PL - BabyTV - Balkanika TV - Barely Legal TV - BBC News Channel - BBC World News - BBN Türk - BG-DNES - BG Music Channel - BHTV - Bloomberg HT - Bloomberg TV - Bloomberg TV BG - BN music - Body in Balance - BOX TV BG - Brazzers TV (ex. Private Spice) - BSTV BG - bTV Action - bTV BG - bTV Cinema - bTV Comedy - bTV Story - Bulgaria on Air - Canal+ Domo - CANAL+ Kuchnia - Cartoonito CEE - Cartoonito UK - Cartoon Network - CBS Reality - Cinemania BG - Cinemax 2 BG - Cinemax BG - City TV BG - Club MTV International - CNBC Europe - CNN Europe - Comedy Central BG - Comedy Central Extra BG - Comedy Central PL - Crime+Investigation PL - Crime & Investigation Channel - Das Erste - Da Vinci Learning - Deluxe Music - Deutsche Welle English - Diema - Diema Family - Diema Sport 2 - Diema Sport 3 BG - Diema Sport - Disco Polo Music PL - Discovery Animal Planet - Discovery Channel - Discovery Historia - Discovery Science - Disney Channel BG - Disney Junior BG - Dizi Smart Max - Dizi Smart Premium - DMAX TR - DM SAT - Dorcel TV - Dorcel XXX - Dream Türk - DSTV - ducktv HD - ducktv SD - EKids - Ekotürk - Epic Drama (CEE) - Erox HD - Eroxxx HD - Eska Rock TV - Eska TV - Eska TV Extra - Eurochannel - EuroNews - EuroNews Bulgaria - Euronews FR - European League of Football Channel - Eurosport 1 BG - Eurosport 2 - EWTN PL - EXTASY TV - Extreme Sports Channel - FashionBox HD - Fashion TV - Fast&FunBox HD - FightBox HD - Fight Klub BG - Fight Klub HD - Filmax - FilmBox Arthouse - FilmBox BG - FilmBox Extra BG - FilmBox Family PL - FilmBox Stars BG - Film Cafe PL - Fix & Foxi - Fokus TV - Folx TV - France 2 - France 3 - France24 - France 24 French - Fuel TV - FunBox UHD - FX Comedy PL - FX PL - Gametoon HD - Haber Global - Habitat TV - HBO 2 BG - HBO 3 BG - HBO BG - History Channel 2 - Home & Garden Television - HUSTLER TV - Insight TV - Investigation Discovery - JimJam BG - Jukebox - K::CN 2 Music - Kanal 7 - KANAL 24 TR - Kanal D TR - Karadeniz TV - KiKA - Kino Nova - Kino Polska - Kino Polska Muzyka - krone.tv - Love Nature US - LUXE TV - Magic TV BG - MAX Sport 1 BG - MAX Sport 2 BG - MAX Sport 3 BG - Max Sport 4 BG - MCM Top - Melodie TV - Metro TV - Mezzo - Mezzo Live HD - MinikaGO TR - Minimini+ - Motorvision Plus - Motowizja - MovieSmart Classic - MovieSmart Türk - Movie Star - MTV 00s - MTV 80s - MTV 90s International - MTV Europe - MTV Hits International - MTV Live HD - MyZen TV - Nat Geo Wild BG - National Geographic BG - Nickelodeon Commercial - Nick Junior - Nick Junior BG - Nova News - Nova Sport BG - Nova TV BG - Novelas+ - Novelas Plus 1 - NOW - Nowa TV - Number One Türk - NUTA.TV HD - NUTA GOLD - oe24.TV - ORF2 - Passion XXX - Pickbox TV BG - Planeta Folk BG - Planeta TV BG - Planete+ PL - Playboy TV - Plovdivska Pravoslavna TV - Polonia1 - POLO TV - Polsat 2 - Polsat Cafe - Polsat Comedy Central Extra - Polsat Doku - Polsat Film - Polsat Games - Polsat Music - Polsat News 2 - Polsat News - Polsat Play - Polsat Rodzina - Polsat Seriale - Polsat Viasat Explore - Polsat Viasat History - Polsat Viasat Nature - Power TV PL - PRO 7 Österreich - ProSieben MAXX - Puls 4 - RAI DUE - RAI News 24 - RAI TRE - RAI UNO - Rai Yoyo - Reality Kings - Red Carpet TV PL - RedLight HD - RiC DE - Ring TV - RT Documentary - RTL DE - RTL Zwei - Russia Today - Sat.1 Gold - Sat.1 Österreich - Show TV - SinemaTV 2 - SinemaTV 1001 - SinemaTV 1002 - SinemaTV - SinemaTV Aile 2 - SinemaTV Aile - SinemaTV Aksiyon 2 - SinemaTV Aksiyon - SinemaTV Komedi 2 - SinemaTV Komedi - SinemaTV Yerli 2 - SinemaTV Yerli - Sixx AT - Skat TV - Sky News - SkyShowtime 1 Nordic - Sportdigital EDGE - STAR BG - STAR Channel - STAR Crime BG - STAR Life BG - Stars TV PL - Star TV TR - Stingray CMusic - Stingray Djazz - Stingray iConcerts - Stopklatka - Sundance TV PL - Super Polsat - Super RTL - SuperToons - TAY TV - TBN Polska HD - TELE 1 - Telewizja 13 - teve2 - teve2 TR - TGRT Belgesel - The Fishing and Hunting - The Fishing and Hunting RO - The History Channel - The Voice BG - Tivibu Spor - TLC - Travel Channel - Travel TV BG - Travelxp - TRT 1 - TRT 2 - TRT 4K - TRT Arapça - TRT Avaz - TRT Belgesel - TRT Çocuk - TRT EBA TV İlkokul - TRT EBA TV Lise - TRT EBA TV Ortaokul - TRT Haber - TRT Kurdî - TRT Müzik - TRT Spor - TRT World - TTV - TV1 BG - TV 4 - TV5 - TV5 Monde - TV5Monde Europe - TV 8.5 - TV 8 TR - TV100 TR - TVE Internacional - TVN 7 - TVN24 - TVN24 BiS - TV Net - TVN Style - TVN Turbo - TVP 2 - TVP 3 - TVP ABC - TVP HD - TVP Historia - TVP Info - TVP Kobieta - TVP Kultura - TVP Regionalna - TVP Rozrywka - TVP Seriale - TV Puls PL - TVR PL - TVS - TV Silesia - TVT PL - TV Trwam - Uçankuş TV - Viasat Explore CEE - Viasat History - Viasat Kino Balkan - Viasat Nature CEE - Vivacom Arena - VOX - VTK BG - Wness TV - wPolsce PL - Wydarzenia 24 - Xtreme TV - XXL - ZDF - БНТ 1 - БНТ 2 - БНТ 3 - БНТ 4 - Евроком - Наша ТВ - Первый - Родина - Россиᴙ 24 - Телевизия Стара Загора BG - ФЕН ТВ - Фен Фолк ТВ - 2X2 - 4Fun Dance - 4Fun Kids - 13 Ulica - 24 Kitchen - 24Kitchen PT - 360 TuneBox - A2TV TR - Active Family - Adria TV - Adult Channel - Adventure HD - Agro TV - a Haber - Ale Kino+ - ALFA TV - ALFA TVP - Al Jazeera - Al Jazeera Arabic English - Al Jazeera Balkans - Alternativna televizija Banja Luka - AMC - AMC PL - a News - Anixe HD Serie - ANIXE plus - A Para - Arena Esport - Arena Fight - Arena Sport 1 BiH - Arena Sport 1 HR - Arena Sport 1 Premium - Arena Sport 1 Premium BiH - Arena Sport 1 RS - Arena Sport 1x2 - Arena Sport 2 BiH - Arena Sport 2 Premium - Arena Sport 2 Premium BiH - Arena Sport 3 BiH - Arena Sport 3 Premium - Arena Sport 3 Premium BiH - Arena Sport 4 BiH - Arena Sport 4 RS - Arena Sport 5 BiH - Arena Sport 6 BiH - Arena Sport 6 RS - ATV2 - AXN - AXN Black PL - AXN PL - AXN Spin - AXN Spin PL - AXN White PL - B1 TV - B92 - BabyTV - Balkanika TV - Balkan trip - Balkan TV - BBC Earth - BBC News Channel - BBC World News - BBN Türk - BDC Televizija - Behar TV Sarajevo - Bir TV - BlicTV - Bloomberg Adria - Bloomberg HT - Bloomberg TV - BN 2 HD - BN music - Brainz TV - Bravo Music - Brazzers TV (ex. Private Spice) - CANAL+ Kuchnia - Cartoonito CEE - Cartoon Network - CBS Reality - CCTV 4 Europe - CGTN - CGTN Documentary - Cinema TV - Cinemax 2 - Cinemax - CineStar Action&Thriller - CineStar Premiere 1 - CineStar Premiere 2 - CineStar TV1 - CineStar TV2 - CineStar TV Comedy Family - CineStar TV Fantasy - City Play - City TV - Club MTV International - CNBC Europe - CNN Europe - Comedy Central PL - Crime+Investigation PL - Crime & Investigation Channel - Croatian Music Channel - Das Erste - Da Vinci Learning - Deutsche Welle English - Dexy TV - Digi 24 - Disco Polo Music PL - Discovery Animal Planet - Discovery Channel - Discovery Historia - Discovery Science - Disney Channel - Disney Channel DE - DIVA (ex. Universal) - DIZI - Dizi Smart Max - Dizi Smart Premium - DMAX DE - DMAX TR - DM SAT - Dobra TV - DOKU TV - Doma TV - Dorcel TV - Dorcel XXX - DOX TV - Dream Türk - E! Entertainment - Ekotürk - English Club TV - Epic Drama (CEE) - Erox HD - Eroxxx HD - Eska Rock TV - Eska TV Extra - Etno TV RO - Euro Cinema 1 - Euro Cinema 2 - Euro Cinema 3 - Euro Cinema 4 - EuroNews - Euronews FR - EuroNews Srbija - European League of Football Channel - Eurosport 1 DE - Eurosport 2 - Eurosport - EWTN - EWTN PL - Extreme Sports Channel - FACE TV - FashionBox HD - Fashion TV - Fast&FunBox HD - Favorit TV RO - FightBox HD - Fight Klub HD - Filmax - FilmBox Arthouse - FilmBox Extra RS - FilmBox Family PL - FilmBox Premium RS - FilmBox Stars RS - Film Cafe PL - Fokus TV - Folx TV - Food Network - FOX NEWS - France24 - France24 Arabic - France 24 French - FREEДОМ - FX Comedy PL - FX PL - GameHub HR - Gametoon HD - GP1 - Grand Televizija - Haber Global - Habitat TV - HappyTV - Hayat 2 - Hayat Folk Box - Hayat Love Box - Hayat Music Box - Hayatovci - Hayat Stil i život - Hayat TV - HBO2 - HBO3 - HBO - HEMA TV - Herceg TV - Historija TV - History Channel 2 - Home & Garden Television - HRT 1 - HRT 2 - HRT 3 - HRT 4 - HRT Int. - HSE - HUSTLER TV - Hype TV - IDJ World - Imperia TV - InformerTV - Insajder TV - Investigation Discovery - Izvorna TV - JimJam - Jugoton TV - K1 TV - K::CN 1 Kopernikus - K::CN 2 Music - K::CN 3 Svet Plus - Kabel Eins - Kanal1 - Kanal 3 Prnjavor - Kanal 6 - KANAL 24 TR - Karadeniz TV - Kazbuka - KiKA - Kino Polska - Kino Polska Muzyka - KinoTV - KitchenTV - Klape i Tambure TV - KLASIK - krone.tv - Kurir TV - Laudato TV - Lov i ribolov - M1 Family - M1 FILM - M1 Gold - Maria Vision - Melodie TV - MinikaGO TR - Minimax - Minimini+ - Motorvision Plus International - Motowizja - MovieSmart Classic - MovieSmart Türk - MTV 00s - MTV 80s - MTV 90s International - MTV Europe - MTV Hits International - MTV Igman - MTV Live HD - MY TV - N1 BA - N1 HR - N1 RS - N24 Doku - Narodna TV - Nat Geo Wild - Nat Geo Wild HD - National Geographic - National Geographic Channel HD - National Geographic RS - NBA TV - Neon TV - Newsmax Balkans - Nickelodeon Commercial - Nick Junior - Nick Music - Nicktoons - Nova BH - Nova Max - Nova S - Nova Series - Nova Sport Srbija - NOVA TV - Novelas+ - Novelas Plus 1 - NOW - Nowa TV - NTV 101 Sanski most - NTV IC Kakanj - Number One Türk - NUTA.TV HD - NUTA GOLD - OBN - oe24.TV - O Kanal - O Kanal Music - O Kanal Plus - One - Otvorena televizija - OTV Valentino - Pickbox TV RS - Pikaboo - Pink Action - Pink and Roll - Pink BH - Pink Classic - Pink Comedy - Pink Crime & Mystery - Pink Erotic 1 - Pink Erotic 2 - Pink Erotic 3 - Pink Erotic 4 - Pink Erotic 5 - Pink Erotic 6 - Pink Erotic 7 - Pink Erotic 8 - PINK Family - Pink Fashion - Pink Fight Network - PINK Film - Pink Folk 1 - Pink Folk 2 - Pink Ha Ha - Pink Hits 2 - Pink Hits - Pink Horror - Pink Kids - Pink Koncert - Pink Kuvar - Pink LOL - Pink M - Pink Movies - Pink Music 1 - Pink Pedia - Pink Premium - Pink Reality - Pink Romance - Pink SCI FI & Fantasy - Pink Serije - Pink Show - Pink Soap - Pink Style - Pink Super Kids - Pink Thriller - Pink Timeout - Pink Western - PINK World - Pink World Cinema - PINK Zabava - Planete+ PL - Playboy TV - Poljoprivredna TV - Polonia1 - POLO TV - Polsat 2 - Polsat Cafe - Polsat Comedy Central Extra - Polsat Doku - Polsat Film - Polsat Music - Polsat News 2 - Polsat News - Polsat Play - Polsat Rodzina - Polsat Seriale - Polsat Viasat Explore - Polsat Viasat History - Polsat Viasat Nature - Posavska Televizija - Power TV PL - Premier League TV - Private TV - PRO 7 Österreich - ProSieben - ProSieben MAXX - ProTV Tomislavgrad - Prva FILES - Prva KICK - Prva LIFE - Prva MAX - Prva Plus - Prva Srpska TV - Prva World - Puls 4 - QVC Deutschland - Radio Televizija BIH - Radio Televizija BN - Radio Televizija Federacije BIH - RAI DUE - RAI Education - RAI News 24 - RAI TRE - RAI UNO - Rai Yoyo - Reality Kings - Red Carpet TV PL - RedLight HD - RED tv - RT Documentary - RTL 2 - RTL - RTL DE - RTL Kockica - RTL Living - RTL Nitro - RTL Zwei - RTRS - RTRS PLUS - RTS 1 - RTS 2 - RTS Drama - RTSH 3 - RTSH 24 - RTSH Plus - RTSH Shkollë - RTS Klasika - RTS Kolo - RTS Muzika - RTS Nauka - RTS Poletarac - RTS SVET - RTS Trezor - RTS Život - RTV7 Tuzla - RTV BPK Goražde - RTV Herceg-Bosne - RTV HIT Brčko - RTV Lukavac - RT Vojvodina 1 - RTV PINK - RTV Slon Tuzla - RTV Slovenija 1 - RTV Slovenija 2 - RTV Slovenija 3 - RTV Tuzlanskog Kantona - RTV Unsko-sanskog kantona - RTV Visoko - RTV Vogošća - RTV Zenica - Russia Today - SAT.1 - Sat.1 Gold - Sat.1 Österreich - SciFi - Sevdah TV - Shoptel - Show TV - SinemaTV 2 - SinemaTV 1001 - SinemaTV 1002 - SinemaTV - SinemaTV Aile 2 - SinemaTV Aile - SinemaTV Aksiyon 2 - SinemaTV Aksiyon - SinemaTV Komedi 2 - SinemaTV Komedi - SinemaTV Yerli 2 - SinemaTV Yerli - Sixx AT - SK Fight - SK Golf - Sky News - Slobomir - Smart TV Tešanj - SOS Plus - Sport 1 DE - Sport Klub 1 Hrvatska - Sport Klub 3 - Sport Klub 4 - Sport Klub 5 - Sport Klub 6 - Sportska Televizija - STAR Channel - STAR Crime - STAR Life - STAR Movies - Stars TV PL - Star TV TR - Stingray iConcerts - Stopklatka - Sundance TV PL - Supermedia Televizija - Super Polsat - Super RTL - SuperSat TV - Superstar 2 - Superstar 3 - Superstar TV - Tanjug Tačno - Tatabrada - TAY TV - TBN Polska HD - TELE 1 - Tele 5 DE - Televizija 5 - Televizija 24 - Televizija Alfa - Televizija Crne Gore MNE - Televizija Dalmacija - Televizija Doktor - Telewizja 13 - teve2 - teve2 TR - TGRT Belgesel - The History Channel - Tivibu Spor - TLC - TLC DE - TNT Kids - TOGGO plus - Toxic Folk - Toxic Rap - Toxic TV - Trace Urban - Travel Channel - Tropik TV - TRT 1 - TRT 4K - TRT Arapça - TRT Avaz - TRT EBA TV İlkokul - TRT EBA TV Lise - TRT EBA TV Ortaokul - TRT World - TTV - TV1 Mreža - TV 4 - TV5 - TV 8.5 - TV 8 TR - TV100 TR - TV Arena Bijeljina - TV Duga + SAT - TV Istočno Sarajevo - TVN 7 - TV Net - TVN Style - TVN Turbo - TVP 2 - TVP 3 - TVP ABC - TVP HD - TVP Historia - TVP Info - TV PINK EXTRA - TV PINK PLUS - TVP Kobieta - TVP Kultura - TVP Rozrywka - TVP Seriale - TV Ras - TVR Cluj RO - TVR Craiova RO - TVR Iasi RO - TVR PL - TVR Tg-Mures RO - TVR Timisoara RO - TVS - TV Sarajevo - TV Silesia - TVT PL - TV Trwam - TV Vijesti - Uçankuş TV - Valentino Etno - Valentino Music HD - Vavoom - Vesti - Viasat Explore CEE - Viasat History - Viasat Kino Balkan - Viasat Nature CEE - Viasat True Crime - Vikom - VOX - VOX up - Welt - wPolsce PL - Wydarzenia 24 - Xtreme TV - XXL - Yaban TV - Zagrebačka Televizija - ZDF - ZDFinfo - ZDFneo - МРТ 1 - Россиᴙ 24 - Телевизија Храм - 1-2-3.tv - 1-2-3.tv - 2X2 - 2X2 - 2X2 - 3 Plus CH - 3 Plus CH - 3 Plus CH - 3SAT - 3SAT - 3SAT - 4Fun Dance - 4Fun Dance - 4Fun Dance - 4Fun Kids - 4Fun Kids - 4Fun Kids - 4FunTV - 4FunTV - 4FunTV - 4 Plus - 4 Plus - 4 Plus - 4 Seven - 5 Plus - 5 Plus - 5 Plus - 5Select - 5 Star - 6 plus - 6 plus - 6 plus - 6ter - 13th Street DE - 13th Street DE - 13 Ulica - 13 Ulica - 13 Ulica - 20 Mediaset - A2TV TR - A2TV TR - A2TV TR - Active Family - Active Family - Active Family - Adult Channel 2 - Adult Channel - Adventure HD - Adventure HD - Adventure HD - a Haber - a Haber - a Haber - Ale Kino+ - Ale Kino+ - Ale Kino+ - ALFA TVP - ALFA TVP - ALFA TVP - Al Jazeera - Al Jazeera - Al Jazeera - Al Jazeera Arabic Arabic - Al Jazeera Arabic English - Al Jazeera Balkans - Alternativna televizija Banja Luka - AMC PL - AMC PL - AMC PL - a News - a News - a News - Animal Planet DE - Animal Planet DE - Anixe HD Serie - Anixe HD Serie - Anixe HD Serie - ANIXE plus - ANIXE plus - ANIXE plus - Antena Europe - A Para - A Para - A Para - À Punt ES - ARD-alpha - ARD-alpha - ARD-alpha - ARTE DE - ARTE DE - ARTE DE - ARTE FR - ATV1 - ATV1 - ATV1 - ATV2 - ATV2 - ATV2 - ATV TR - ATV TR - auftanken.TV - auftanken.TV - auftanken.TV - Auto Motor und Sport - AXN Black DE - AXN Black DE - AXN Black PL - AXN Black PL - AXN Black PL - AXN PL - AXN PL - AXN PL - AXN Spin PL - AXN Spin PL - AXN Spin PL - AXN White DE - AXN White DE - AXN White PL - AXN White PL - AXN White PL - BabyTV - Baden TV - Balkanika TV - Bayerischen Fernsehen Nord - Bayerischen Fernsehen Nord - BBC1 - BBC2 - BBC3 - BBC4 - BBC News Channel - BBC Parliament - BBC World News - BBC World News - BBC World News - BBN Türk - BBN Türk - BBN Türk - beIN GURME - beIN GURME - beIN iZ - beIN iZ - beIN iZ - beIN Movies Premiere TR - Benfica TV - Bergblick - Bergblick - Bergblick - Beyaz TV - Beyaz TV - Beyaz TV - BFM Business - BFM TV - BFM TV - BFM TV - Bibel TV - Bibel TV - Bibel TV - Bloomberg HT - Bloomberg HT - Bloomberg HT - Bloomberg TV - Bloomberg TV - Bloomberg TV - Blue Hustler - Boing - Boing Plus - BonGusto - BonGusto - BR Fernsehen Süd - BR Fernsehen Süd - BR Fernsehen Süd - Canal 24H - Canal 24H - Canal+ Domo - Canal+ Domo - Canal+ France - Canal+ France - CANAL+ Kuchnia - CANAL+ Kuchnia - CANAL+ Kuchnia - Canale 5 - CARAC1 - CARAC4 - Cartoonito CEE - Cartoonito Germany - Cartoonito Germany - Cartoonito Italia - Cartoon Network DE - Cartoon Network DE - CBBC - Cbeebies - CCTV 4 Europe - CCTV 4 Europe - CGTN - CGTN - CGTN Documentary - CGTN Documentary - Challenge TV - Channel 4 - Channel 5 - Chérie 25 - Children's ITV - cielo - Cine34 - Class TV Moda - ClipMyHorse.TV Linear TV - Clubland TV - Club MTV International - CNBC Europe - CNBC Europe - CNBC Europe - CNews - CNews - CNN Europe - CNN Europe - CNN Europe - CNN Türk TV - CNN Türk TV - Comedy Central DE - Comedy Central DE - Comedy Central DE - Comedy Central PL - Comedy Central PL - Comedy Central PL - Crime+Investigation PL - Crime+Investigation PL - Crime+Investigation PL - Crime & Investigation Channel - Crime & Investigation DE - Crime & Investigation DE - Croatian Music Channel - Croatian Music Channel - CStar FR - Curiosity Channel - Curiosity Channel - Curiosity Channel - Das Bild TV - Das Bild TV - Das Bild TV - Das Erste - Das Erste - Das Erste - Das Health TV - Das Health TV - Daystar - DAZN 1 DE - DAZN 2 DE - Deluxe Music - Deluxe Music - Deluxe Music - Deutsches Musik Fernsehen - Deutsches Musik Fernsehen - Deutsches Musik Fernsehen - Deutsche Welle English - Deutsche Welle English - Deutsche Welle English - Deutsche Welle Espanol - DF1 - Disco Polo Music PL - Disco Polo Music PL - Disco Polo Music PL - Discovery Channel DE - Discovery Channel DE - Discovery Channel IT - Discovery Historia - Discovery Historia - Discovery Historia - Disney Channel DE - Disney Channel DE - Disney Channel DE - Dizi Smart Max - Dizi Smart Max - Dizi Smart Max - Dizi Smart Premium - Dizi Smart Premium - Dizi Smart Premium - DMAX DE - DMAX DE - DMAX DE - DMAX IT - DMAX TR - DMAX TR - DMAX TR - DMAX UK - DM SAT - DM SAT - DM SAT - Dorcel TV - Dorcel TV - Dorcel XXX - Dream Türk - Dream Türk - Dream Türk - ducktv HD - ducktv SD - Duna TV - Duna World - E4 - E! Entertainment - E! Entertainment - Ekotürk - Ekotürk - Ekotürk - Erox HD - Erox HD - Eroxxx HD - Eroxxx HD - Eroxxx HD - Eska Rock TV - Eska Rock TV - Eska Rock TV - Eska TV - Eska TV - Eska TV - Eska TV Extra - Eska TV Extra - Eska TV Extra - eSports 1 - Euro D - Euro D - EuroNews - EuroNews - EuroNews - Euronews FR - European League of Football Channel - European League of Football Channel - European League of Football Channel - Eurosport 1 DE - Eurosport 1 DE - Eurosport 1 DE - Eurosport 2 DE - Eurosport 2 DE - Eurosport 2 FR - Eurosport 2 IT - Eurosport - Eurosport FR - Eurosport IT - Euro Star - Euro Star - EWTN - EWTN PL - EWTN PL - EWTN PL - Extreme Sports Channel - Fashion TV - Fashion TV - Fashion TV - Fast&FunBox HD - Fight 24 - Fight Klub HD - Fight Klub HD - Fight Klub HD - Film4 - Filmax - Filmax - Filmax - FilmBox Family PL - FilmBox Family PL - FilmBox Family PL - Film Cafe PL - Film Cafe PL - Film Cafe PL - Fix & Foxi - Fix & Foxi - Fix & Foxi - Focus TV - Fokus TV - Fokus TV - Fokus TV - Folx TV - Folx TV - Folx TV - Food Network Italia - Food Network UK - France 2 - France 2 - France 3 - France 3 - France 4 - France 4 - France 5 - France 5 - France24 - France24 - France24 - France 24 French - France 24 French - France 24 French - France Info - Frisbee - FS1 AT - FunBox UHD - FX Comedy PL - FX Comedy PL - FX Comedy PL - FX PL - FX PL - FX PL - Game One - Geo Television - Geo Television - Giallo TV - Ginx Esports TV - Goldstar - Golf plus - Gulli - Gulli - Gute Laune TV - Gute Laune TV - Haber Global - Haber Global - Haber Global - Habertürk - Habertürk - Habitat TV - Habitat TV - Habitat TV - Halk TV - Hamburg 1 - Heimatkanal - Heimatkanal - HGTV DE - HGTV DE - HGTV DE - HGTV IT - History Channel DE - History Channel DE - Home & Garden Television - HR Fernsehen - HR Fernsehen - HR Fernsehen - HRT 1 - HRT 1 - HSE24 Extra - HSE24 Extra - HSE24 Extra - HSE24 Trend - HSE24 Trend - HSE24 Trend - HSE - HSE - HSE - Hustler HD - HUSTLER TV - HUSTLER TV - i24News FR - Insight TV - Insight TV - Insight TV - Italia 1 - Italia 2 - ITV1 - ITV2 - ITV3 - ITV4 - iTVN - iTVN - iTVN Extra - iTVN Extra - ITV Quiz - Jukebox - K2 - K-TV Katholisches Fernsehen - K-TV Katholisches Fernsehen - K-TV Katholisches Fernsehen - Kabel Eins - Kabel Eins - Kabel Eins - Kabel Eins Classics - Kabel Eins Classics - Kabel eins Doku - Kabel eins Doku - Kabel eins Doku - Kabel eins Österreich - Kanal 7 - Kanal 7 - Kanal 7 - Kanal 9 TV - KANAL 24 TR - KANAL 24 TR - KANAL 24 TR - Kanal D TR - Kanal D TR - Kanal D TR - Karadeniz TV - Karadeniz TV - Karadeniz TV - KiKA - KiKA - KiKA - Kino Polska - Kino Polska - Kino Polska Muzyka - Kino Polska Muzyka - Kino Polska Muzyka - Kinowelt - Kinowelt - KIT-TV - Klan Kosova - Klan Kosova - Klan TV HD - KLASIK - Kral Pop TV - krone.tv - krone.tv - krone.tv - Kurier TV - La7 - La7 - La7d - La Chaîne Info - La Cinque - La Télé - L'Equipe - Love Nature US - LUXE TV - LUXE TV - LUXE TV - M6 - M6 - Marco Polo TV - Marco Polo TV - MDR Fernsehen - MDR Fernsehen - MDR Fernsehen - MDR Sachsen-Anhalt - MDR Sachsen-Anhalt - MDR Sachsen - MDR Sachsen - MDR Thüringen - MDR Thüringen - Mediaset Extra - Mediaset Italia - Melodie TV - Melodie TV - Melodie TV - Metro TV - Metro TV - Mezzo - Mezzo Live HD - MGG TV - MinikaGO TR - MinikaGO TR - MinikaGO TR - Minimini+ - Minimini+ - Minimini+ - More4 - More Than Sports TV - More Than Sports TV - Motorvision Plus - Motorvision Plus - Motorvision Plus - Motorvision Plus France - Motorvision Plus International - Motorvision Plus International - Motorvision Plus International - Motowizja - Motowizja - Motowizja - MovieSmart Classic - MovieSmart Classic - MovieSmart Classic - MovieSmart Türk - MovieSmart Türk - MovieSmart Türk - MTV 00s - MTV 80s - MTV 80s - MTV 90s International - MTV DE - MTV DE - MTV DE - MTV Hits International - MTV Live HD - MTV Live HD - münchen.tv - MyZen TV - MyZen TV - MyZen TV - N24 Doku - N24 Doku - N24 Doku - NatGeo Wild DE - NatGeo Wild DE - National Geographic DE - National Geographic DE - NBC News - NBC News - NDR Hamburg - NDR Hamburg - NDR Hamburg - NDR Mecklenburg-Vorpommern - NDR Mecklenburg-Vorpommern - NDR Niedersachsen - NDR Niedersachsen - NDR Schleswig-Holstein - NDR Schleswig-Holstein - Nick DE - Nick DE - Nick DE - Nick Junior - Nick Junior - Nick Music - Nicktoons - Nicktoons - Niederbayern TV Deggendorf-Straubing - Niederbayern TV Deggendorf-Straubing - NOVE - Novelas+ - Novelas+ - Novelas+ - Novelas Plus 1 - Novelas Plus 1 - Novelas Plus 1 - NOW - NOW - NOW - Nowa TV - Nowa TV - Nowa TV - Now Rock - NTV DE - NTV DE - NTV DE - Number One Türk - Number One Türk - Number One Türk - NUTA.TV HD - NUTA.TV HD - NUTA.TV HD - NUTA GOLD - NUTA GOLD - NUTA GOLD - OBN - OBN - oe24.TV - oe24.TV - oe24.TV - One - One - One - ORF1 - ORF1 - ORF2 - ORF2 - ORF3 - ORF3 - ORF3 - ORF Sport Plus - ORF Sport Plus - PBS America - Phoenix - Phoenix - Phoenix - PINK Film - PINK Film - PINK Film - Pink Folk 1 - Pink Folk 1 - Pink Kids - Pink Koncert - Pink Music 1 - Pink Music 1 - Pink Music 1 - Pink Reality - Planete+ PL - Planete+ PL - Planete+ PL - Playboy TV - Polonia1 - Polonia1 - Polonia1 - POLO TV - POLO TV - POLO TV - Polsat 1 - Polsat 1 - Polsat 2 - Polsat 2 - Polsat 2 - Polsat - Polsat Cafe - Polsat Cafe - Polsat Cafe - Polsat Comedy Central Extra - Polsat Comedy Central Extra - Polsat Comedy Central Extra - Polsat Doku - Polsat Doku - Polsat Doku - Polsat Film - Polsat Film - Polsat Film - Polsat Games - Polsat Games - Polsat Music - Polsat Music - Polsat Music - Polsat News 2 - Polsat News 2 - Polsat News 2 - Polsat News - Polsat News - Polsat News - Polsat Play - Polsat Play - Polsat Play - Polsat Rodzina - Polsat Rodzina - Polsat Rodzina - Polsat Seriale - Polsat Seriale - Polsat Seriale - Polsat Viasat Explore - Polsat Viasat Explore - Polsat Viasat Explore - Power Türk TV - Power TV PL - Power TV PL - Power TV PL - Private TV - PRO 7 Österreich - PRO 7 Österreich - PRO 7 Österreich - ProSieben - ProSieben - ProSieben - ProSieben Fun - ProSieben Fun - ProSieben MAXX - ProSieben MAXX - ProSieben MAXX - Puls 4 - Puls 4 - Puls 4 - Puls 8 - Puls 8 - Puls 8 - QVC 2 DE - QVC 2 DE - QVC Deutschland - QVC Deutschland - QVC Style - QVC STYLE DE - QVC STYLE DE - RadioBremen - RadioBremen - Radio Televizija BN - Radio Ticino Channel HD - Radio Ticino Channel HD - Radio Ticino Channel HD - RAI 4 - RAI 4 - RAI 4 - RAI 5 - RAI 5 - RAI 5 - RAI DUE - RAI DUE - RAI Education - RAI Gulp - RAI Italia Australia - RAI News 24 - RAI News 24 - RAI Sport 1 - RAI Storia - RAI Storia - RAI TRE - RAI TRE - RAI UNO - RAI UNO - RAI World Premium - RAI World Premium - RAI World Premium - Rai Yoyo - Rai Yoyo - Rai Yoyo - RBB Fernsehen Berlin - RBB Fernsehen Berlin - RBB Fernsehen Berlin - RBB Fernsehen Brandenburg - RBB Fernsehen Brandenburg - real time - Red Carpet TV PL - Red Carpet TV PL - Red Carpet TV PL - RedLight HD - Rete 4 - Rhein Neckar Fernsehen - RiC DE - RiC DE - RiC DE - RMC Découverte - RMC Story - Rocket Beans TV - Rocket Beans TV - Romance TV - Romance TV - Romance TV - Romance TV PL - Romance TV PL - Romance TV PL - RSI La 1 - RSI La 2 - RTK 1 - RTK 1 - RTL Crime DE - RTL Crime DE - RTL Crime DE - RTL Croatia World - RTL DE - RTL DE - RTL DE - RTL Living DE - RTL Living DE - RTL Nitro - RTL Nitro - RTL Nitro - RTL Passion DE - RTL Passion DE - RTLup - RTLup - RTLup - RTL Zwei - RTL Zwei - RTL Zwei - RTP3 - RTP Internacional - RTS 2 Suisse - RTS SVET - RTS SVET - RTS Un - Russia Today - Russia Today - S1 CH - S1 CH - S1 CH - S4C - SAT.1 - SAT.1 - SAT.1 - Sat.1 Emotions - Sat.1 Emotions - Sat.1 Gold - Sat.1 Gold - Sat.1 Gold - Sat.1 Österreich - Sat.1 Österreich - Sat.1 Österreich - Schlager Deluxe - Schlager Deluxe - ServusTV - ServusTV - ServusTV - Show Türk - Show Türk - Show TV - Show TV - Show TV - SIC - SIC - SIC Internacional - SIC Internacional - SIC Noticias - SIC Noticias - Silverline Movie Channel - SinemaTV 2 - SinemaTV 2 - SinemaTV 2 - SinemaTV 1001 - SinemaTV 1001 - SinemaTV 1001 - SinemaTV 1002 - SinemaTV 1002 - SinemaTV 1002 - SinemaTV - SinemaTV - SinemaTV - SinemaTV Aile 2 - SinemaTV Aile 2 - SinemaTV Aile 2 - SinemaTV Aile - SinemaTV Aile - SinemaTV Aile - SinemaTV Aksiyon 2 - SinemaTV Aksiyon 2 - SinemaTV Aksiyon 2 - SinemaTV Aksiyon - SinemaTV Aksiyon - SinemaTV Aksiyon - SinemaTV Komedi 2 - SinemaTV Komedi 2 - SinemaTV Komedi 2 - SinemaTV Komedi - SinemaTV Komedi - SinemaTV Komedi - SinemaTV Yerli 2 - SinemaTV Yerli 2 - SinemaTV Yerli 2 - SinemaTV Yerli - SinemaTV Yerli - SinemaTV Yerli - Sixx - Sixx - Sixx - Sixx AT - Sixx AT - Sixx AT - Sky 1 DE - Sky 1 DE - Sky Atlantic DE - Sky Atlantic DE - Sky Bundesliga 1 - Sky Bundesliga 1 - Sky Bundesliga 2 - Sky Bundesliga 2 - Sky Bundesliga 3 - Sky Bundesliga 3 - Sky Bundesliga 4 - Sky Bundesliga 4 - Sky Bundesliga 5 - Sky Bundesliga 5 - Sky Bundesliga 6 - Sky Bundesliga 6 - Sky Bundesliga 7 - Sky Bundesliga 7 - Sky Cinema Action DE - Sky Cinema Action DE - Sky Cinema Best Of DE - Sky Cinema Classics DE - Sky Cinema Classics DE - Sky Cinema Family DE - Sky Cinema Family DE - Sky Cinema Fun DE - Sky Cinema Premieren - Sky Krimi DE - Sky Krimi DE - Sky Krimi DE - Sky Mix - Sky Nature DE - Sky News - Sky News - Sky Sport 1 DE - Sky Sport 1 DE - Sky Sport 3 DE - Sky Sport 3 DE - Sky Sport 4 DE - Sky Sport 4 DE - Sky Sport 5 DE - Sky Sport 5 DE - Sky Sport 6 DE - Sky Sport 6 DE - Sky Sport 7 DE - Sky Sport 7 DE - Sky Sport 8 DE - Sky Sport 8 DE - Sky Sport 9 DE - Sky Sport 9 DE - Sky Sport 10 DE - Sky Sport 10 DE - Sky Sport Bundesliga 8 - Sky Sport Bundesliga 9 - Sky Sport Bundesliga 10 - Sky Sport Bundesliga HD - Sky Sport Bundesliga UHD - Sky Sport Mix DE - Sky Sport News DE - Sky Sport News DE - Sky Sport Premier League DE - Sky TG24 HD - SonLife - SonLife - sonnenklar.TV - sonnenklar.TV - Spiegel Geschichte - Spiegel Geschichte - Spiegel Geschichte - Sport 1 DE - Sport 1 DE - Sport 1 plus DE - Sport 1 plus DE - Sport 1 plus DE - Sportdigital Fussball 2 - Sportdigital Fußball - Sportdigital Fußball - Sportdigital Fußball - Sportitalia - SRF 1 - SRF 1 - SR Fernsehen - SR Fernsehen - SR Fernsehen - SRF Info - SRF Info - SRF Info - SRF Zwei - SRF Zwei - SRF Zwei - Stars TV PL - Stars TV PL - Stars TV PL - Star TV TR - Star TV TR - Star TV TR - Stingray Classica - Stingray Classica - Stingray Classica - Stingray Djazz - Stingray Djazz - Stingray Djazz - Stingray iConcerts - Stopklatka - Stopklatka - Stopklatka - Sundance TV PL - Sundance TV PL - Sundance TV PL - Super! - Super Polsat - Super Polsat - Super Polsat - Super RTL - Super RTL - Super RTL - Swiss1 TV - SWR1 Baden-Württemberg - SWR - SWR - SWR - SWR Baden-Württemberg - SWR Baden-Württemberg - SWR Baden-Württemberg - Syfy HD DE - Syfy HD DE - tagesschau24 - tagesschau24 - tagesschau24 - TAY TV - TAY TV - TAY TV - TBN Polska HD - TBN Polska HD - TBN Polska HD - TELE 1 - TELE 1 - TELE 1 - Tele 5 DE - Tele 5 DE - Tele 5 DE - TeleBärn - TeleBärn - TeleBärn - TeleDeporte - Televisión de Galicia - Télévision française 1 - Televizioni 7 - Telewizja 13 - Telewizja 13 - Telewizja 13 - Tele Zürich - Tele Zürich - Tele Zürich - teve2 - teve2 - teve2 - teve2 TR - teve2 TR - teve2 TR - TF1 Séries Films - TFX - TGCOM24 - TGRT Belgesel - TGRT Belgesel - TGRT Belgesel - TGRT EU - TGRT HABER - TGRT HABER - The History Channel - The Nautical Channel - The Nautical Channel - TiJi - Tivibu Spor - Tivibu Spor - Tivibu Spor - TLC DE - TLC DE - TLC DE - TMC - TOGGO plus - TOGGO plus - TOGGO plus - Top Channel - Top Crime - Trace Urban - TRT 1 - TRT 1 - TRT 2 - TRT 4K - TRT 4K - TRT 4K - TRT Arapça - TRT Arapça - TRT Arapça - TRT Avaz - TRT Avaz - TRT Avaz - TRT Belgesel - TRT Çocuk - TRT Diyanet - TRT Diyanet - TRT Diyanet - TRT EBA TV İlkokul - TRT EBA TV İlkokul - TRT EBA TV İlkokul - TRT EBA TV Lise - TRT EBA TV Lise - TRT EBA TV Lise - TRT EBA TV Ortaokul - TRT EBA TV Ortaokul - TRT EBA TV Ortaokul - TRT Haber - TRT Haber - TRT Haber - TRT Kurdî - TRT Kurdî - TRT Kurdî - TRT Müzik - TRT Spor - TRT Spor Yıldız - TRT Spor Yıldız - TRT Turk - TRT Turk - TRT World - TRT World - TRT World - TTV - TTV - TV 4 - TV 4 - TV5 - TV5 - TV5 - TV5 Monde - TV5 Monde - TV5Monde Europe - TV5Monde Europe - TV 8.5 - TV 8.5 - TV 8.5 - TV8 IT - TV8 IT - TV 8 TR - TV 8 TR - TV24 - TV24 - TV24 - TV25 - TV25 - TV25 - TV100 TR - TV100 TR - TV100 TR - TV1000 Global Kino - TV 2000 - TV Dukagjini - TVE Internacional - TVE Internacional - TVI - TVI - TVM3 - TVN 7 - TVN 7 - TVN 7 - TVN24 - TVN24 - TVN24 BiS - TVN - TVN - TV Net - TV Net - TV Net - TV Now DE - TVN Style - TVN Style - TVN Style - TVN Turbo - TVN Turbo - TVN Turbo - TVP 1 - TVP 1 - TVP 2 - TVP 2 - TVP 2 - TVP 3 - TVP 3 - TVP 3 - TVP ABC - TVP ABC - TVP HD - TVP HD - TVP HD - TVP Historia - TVP Historia - TVP Info - TVP Info - TVP Info - TV PINK EXTRA - TV PINK EXTRA - TV PINK EXTRA - TV PINK PLUS - TV PINK PLUS - TV PINK PLUS - TVP Kobieta - TVP Kobieta - TVP Kobieta - TVP Kultura - TVP Kultura - TVP Polonia - TVP Regionalna - TVP Regionalna - TVP Rozrywka - TVP Rozrywka - TVP Seriale - TVP Seriale - TV Puls PL - TV Puls PL - TVP World - TVR PL - TVR PL - TVR PL - TVS - TVS - TVS - TV Silesia - TV Silesia - TV Silesia - TVT PL - TVT PL - TVT PL - TV Trwam - TV Trwam - TV Trwam - Twenty Seven - Uçankuş TV - Uçankuş TV - Uçankuş TV - U&Dave - U&Drama - Ülke TV - Ülke TV - Universal TV DE - U&Yesterday - VG TV - Vivid Red - Vivid TV - Vivid TV - VOX - VOX - VOX - VOX up - VOX up - W9 - W24 - Warner TV Comedy DE - Warner TV Comedy DE - Warner TV Film DE - Warner TV Film DE - Warner TV Serie DE - Warner TV Serie DE - WDR Fernsehen - WDR Fernsehen - WDR Fernsehen - WDR Fernsehen Köln - WDR Fernsehen Köln - Welt - Welt - Welt - Welt der Wunder - Welt der Wunder - Welt der Wunder - Wetter - Wetter - Wir 24 - World Fashion Channel - World Fashion Channel - World Fashion Channel - wPolsce PL - wPolsce PL - wPolsce PL - Wydarzenia 24 - Wydarzenia 24 - Wydarzenia 24 - Xtreme TV - Xtreme TV - Xtreme TV - XXL - XXL - XXL - Yaban TV - Yaban TV - Yaban TV - Zagrebačka Televizija - ZDF - ZDF - ZDF - ZDFinfo - ZDFinfo - ZDFinfo - ZDFneo - ZDFneo - ZDFneo - 2X2 - 2X2 - 3SAT - 4Fun Dance - 4Fun Dance - 4Fun Dance - 4Fun Kids - 4Fun Kids - 4Fun Kids - 4FunTV - 4FunTV - 4 Seven - 4 Seven - 5ACTION - 5ACTION - 5Select - 5Select - 5 Star - 5 Star - 5 USA - 13 Ulica - 13 Ulica - 13 Ulica - 20 Mediaset - 24 Kitchen - 24Kitchen PT - 360 TuneBox - A2 CNN - A2TV TR - A2TV TR - A2TV TR - ABC News Albania - Active Family - Active Family - Active Family - Adria TV - Adult Channel 2 - Adventure HD - Adventure HD - Adventure HD - Agro TV - a Haber - a Haber - a Haber - Ale Kino+ - Ale Kino+ - Ale Kino+ - ALFA TVP - ALFA TVP - Al Jazeera - Al Jazeera Arabic Arabic - Al Jazeera Arabic English - Al Jazeera Arabic English - Al Jazeera Balkans - AMC - AMC PL - AMC PL - AMC PL - AMC UK - a News - a News - a News - Animal Planet UK - Animal Planet UK - Anixe HD Serie - A Para - A Para - A Para - À Punt ES - Arena Esport - Arena Fight - Arena Sport 1 Premium - Arena Sport 1 RS - Arena Sport 1x2 - Arena Sport 2 Premium - Arena Sport 2 RS - Arena Sport 3 Premium - Arena Sport 3 RS - Arena Sport 4 RS - Arena Sport 5 RS - Arena Sport 6 RS - Arena Sport 7 RS - Arena Sport 8 RS - Arena Sport 9 RS - Arena Sport 10 RS - Arise News - Arise News - ARTE DE - At The Races - At The Races - ATV1 - ATV1 - ATV1 - ATV2 - ATV2 - ATV2 - AXN - AXN Black PL - AXN PL - AXN PL - AXN PL - AXN Spin - AXN Spin PL - AXN Spin PL - AXN Spin PL - AXN White PL - AXN White PL - B92 - BabyTV - BabyTV - BabyTV - Balkan trip - Balkan TV - Bang Bang - BBC1 - BBC1 - BBC1 Northern Ireland - BBC1 Northern Ireland - BBC 1 Wales - BBC2 - BBC2 - BBC 2 Northern Ireland - BBC3 - BBC3 - BBC4 - BBC4 - BBC Alba - BBC Alba - BBC Earth - BBC Earth - BBC News Channel - BBC News Channel - BBC One Scotland - BBC One Scotland - BBC Parliament - BBC Parliament - BBC Scotland - BBC Two Wales - BBC World News - BBC World News - BBN Türk - BBN Türk - Benfica TV - Beyaz TV - BlicTV - Bloomberg Adria - Bloomberg HT - Bloomberg HT - Bloomberg HT - Bloomberg TV - BN 2 HD - BN music - Body in Balance - Boomerang UK - Boomerang UK - Brainz TV - Bravo Music - Brazzers TV (ex. Private Spice) - BT Sport ESPN - Canal+ Domo - CANAL+ Kuchnia - CANAL+ Kuchnia - CANAL+ Kuchnia - Capital XTRA - Capital XTRA - Cartoonito CEE - Cartoonito CEE - Cartoonito CEE - Cartoonito UK - Cartoonito UK - Cartoon Network - Cartoon Network - Cartoon Network - Cartoon Network UK - Cartoon Network UK - CBBC - CBBC - Cbeebies - Cbeebies - CBS Reality - CBS Reality PL - CCTV 4 Europe - CGTN - CGTN - CGTN Documentary - Challenge TV - Challenge TV - Channel 4 +1 - Channel 4 +1 - Channel 4 - Channel 4 - Channel 5 - Channel 5 - Channel S - Children's ITV - Children's ITV - Cinemania TV - Cinemax 2 - Cinemax - CineStar Action&Thriller RS - CineStar Premiere 1 - CineStar Premiere 2 - CineStar TV2 - CineStar TV Comedy Family - CineStar TV Fantasy - CineStar TV RS - Click TV - Clubland TV - Club MTV International - CNBC Europe - CNN Europe - CNN Europe - CNN Türk TV - Comedy Central Extra UK - Comedy Central Extra UK - Comedy Central PL - Comedy Central PL - Comedy Central PL - Comedy Central UK - Comedy Central UK - Crime+Investigation PL - Crime+Investigation PL - Crime+Investigation PL - Crime and Investigation UK - Crime and Investigation UK - Crime & Investigation Channel - Croatian Music Channel - Cufo TV - Cúla4 - Das Erste - Daystar - Deutsche Welle English - Deutsche Welle English - Dexy TV - DigitAlb Melody TV - Disco Polo Music PL - Disco Polo Music PL - Disco Polo Music PL - Discovery Animal Planet - Discovery Animal Planet - Discovery Channel - Discovery Channel UK - Discovery Channel UK - Discovery Historia - Discovery Historia - Discovery Historia - Discovery History UK - Discovery History UK - Discovery Science - Discovery Science UK - Discovery Science UK - Discovery Turbo UK - Discovery Turbo UK - Disney Channel - Disney Junior - DIVA (ex. Universal) - DIZI - Dizi Smart Max - Dizi Smart Max - Dizi Smart Premium - Dizi Smart Premium - DMAX TR - DMAX TR - DMAX TR - DMAX UK - DMAX UK - DM SAT - Dorcel TV - Dorcel XXX - DOX TV - Dream Türk - Dream Türk - Dream Türk - ducktv HD - ducktv SD - E4 - E4 - E4 Extra - E4 Extra - E! Entertainment - Eden UK - Eden UK - Ekotürk - Ekotürk - Ekotürk - Elrodi - English Club TV - Epic Drama (CEE) - Erox HD - Erox HD - Eroxxx HD - Eroxxx HD - Eska Rock TV - Eska Rock TV - Eska Rock TV - Eska TV - Eska TV - Eska TV Extra - Eska TV Extra - Eska TV Extra - Eurochannel - Euro Cinema 1 - Euro Cinema 2 - Euro Cinema 3 - Euro Cinema 4 - Euro D - EuroNews - EuroNews - EuroNews - Euronews Albania - EuroNews Srbija - European League of Football Channel - European League of Football Channel - European League of Football Channel - Eurosport 2 - Eurosport.com - Eurosport - Euro Star - EWTN PL - EWTN PL - EWTN PL - Explorer Histori - Explorer Natyra - Explorer Shkence - Extreme Sports Channel - Extreme Sports Channel - Extreme Sports Channel - FACE TV - FashionBox HD - FashionBox HD - Fashion TV - Fashion TV - Fast&FunBox HD - Fast&FunBox HD - FAX News - FightBox HD - Fight Klub HD - Fight Klub HD - Fight Klub HD - Film4 - Film4 - Filmax - Filmax - FilmBox Arthouse - FilmBox Arthouse - FilmBox Extra RS - FilmBox Family PL - FilmBox Family PL - FilmBox Family PL - FilmBox Premium RS - FilmBox Stars RS - Film Cafe PL - Film Cafe PL - Film Cafe PL - Fokus TV - Fokus TV - Fokus TV - Folx TV - Folx TV - Folx TV - Food Network - Food Network UK - Food Network UK - FOX NEWS - France 3 - France24 - France24 - France24 Arabic - France24 Arabic MENA - France 24 French - Fuel TV - FX Comedy PL - FX Comedy PL - FX Comedy PL - FX PL - FX PL - FX PL - Gametoon HD - Gametoon HD - Gems TV - Gems TV - Ginx Esports TV - Ginx Esports TV - Golica TV - Grand Televizija - Great! Movies +1 - Great! Movies +1 - Great! Movies - Great! Movies - Great! Movies Action - Great! Movies Action - Great! Movies Classic - Great! Movies Classic - Great! romance - Great! TV - Haber Global - Haber Global - Haber Global - Habitat TV - Habitat TV - Habitat TV - HappyTV - HauntTV - Hayat 2 - Hayat Folk Box - Hayat Music Box - Hayatovci - Hayat TV - HBO2 - HBO3 - HBO - HEMA TV - History Channel 2 - History Channel 2 - Home & Garden Television - Home & Garden Television UK - Home & Garden Television UK - HRT 1 - HRT 2 - HRT 3 - HRT 4 - Hustler HD - HUSTLER TV - HUSTLER TV - Ideal Home Shopping - Ideal Home Shopping - IDJ World - Insajder TV - Insight TV - INTV AL - Investigation Discovery - Investigation Discovery UK - Investigation Discovery UK - ITV1 - ITV1 - ITV2+1 - ITV2+1 - ITV2 - ITV2 - ITV3 - ITV3 - ITV4 - ITV4 - iTVN - iTVN - iTVN Extra - iTVN Extra - ITV Quiz - ITV Quiz - Jewellery Channel - Jugoton TV - Junior TV - K1 TV - K::CN 1 Kopernikus - K::CN 2 Music - K::CN 3 Svet Plus - Kanal 3 Prnjavor - Kanal 7 - Kanal 7 - KANAL 24 TR - KANAL 24 TR - KANAL 24 TR - Kanal D - Kanal D TR - Kanal D TR - Kanali 7 - Karadeniz TV - Karadeniz TV - Karadeniz TV - Kazbuka - Ketchup TV - Kino Polska - Kino Polska - Kino Polska - Kino Polska Muzyka - Kino Polska Muzyka - Kino Polska Muzyka - KitchenTV - Klan Kosova - krone.tv - krone.tv - krone.tv - Kurir TV - Legend - Living HD - London Live - Love Nature US - Lov i ribolov - Manchester TV - MCN TV - Mediaset Italia - Melodie TV - Melodie TV - Melodie TV - Metro TV - Metro TV - Mezzo Live HD - MinikaGO TR - MinikaGO TR - Minimax - Minimini+ - Minimini+ - Minimini+ - More4 - More4 - MotorTrend - Motorvision Plus France - Motorvision Plus International - Motorvision Plus International - Motowizja - Motowizja - Motowizja - Movies 24 - Movies 24 - MovieSmart Classic - MovieSmart Classic - MovieSmart Türk - MovieSmart Türk - MTV 00s - MTV 80s - MTV 80s UK - MTV 80s UK - MTV 90s International - MTV 90s UK - MTV 90s UK - MTV Europe - MTV Hits International - MTV Live HD - MTV Live HD - MTV Music UK - MTV Music UK - MTV UK - MTV UK - MUSE - My Music - MyZen TV - MyZen TV - N1 RS - Narodna TV - Nat Geo Wild - Nat Geo Wild HD - Nat Geo Wild UK - Nat Geo Wild UK - National Geographic - National Geographic Channel HD - National Geographic Channel UK - National Geographic Channel UK - NBA TV - NBC News - News 24 - Nickelodeon Commercial - Nickelodeon UK - Nickelodeon UK - Nick Jr. Too UK - Nick Junior - Nick Junior UK - Nick Junior UK - Nick Music - Nicktoons - Nicktoons UK - Nicktoons UK - Nova M - Nova Max - Nova S - Nova Series - Nova Sport Srbija - Novelas+ - Novelas+ - Novelas Plus 1 - Novelas Plus 1 - Now 70s - Now 80s - NOW - Nowa TV - Nowa TV - Nowa TV - Now Rock - NTV IC Kakanj - Number One Türk - Number One Türk - Number One Türk - NUTA.TV HD - NUTA.TV HD - NUTA.TV HD - NUTA GOLD - NUTA GOLD - OBN - oe24.TV - oe24.TV - oe24.TV - Oireachtas TV - O Kanal - Ora News - ORF2 - ORF2 - ORF2 - OTV Valentino - PBS America - PBS America - Pickbox TV RS - Pikaboo - Pink Action - Pink and Roll - Pink BH - Pink Classic - Pink Comedy - Pink Crime & Mystery - Pink Erotic 1 - Pink Erotic 2 - Pink Erotic 3 - Pink Erotic 4 - Pink Erotic 5 - Pink Erotic 6 - Pink Erotic 7 - Pink Erotic 8 - PINK Family - Pink Fashion - PINK Film - Pink Folk 1 - Pink Folk 2 - Pink Ha Ha - Pink Hits 2 - Pink Hits - Pink Horror - Pink Kids - Pink Koncert - Pink Kuvar - Pink LOL - Pink M - Pink Movies - Pink Music 1 - Pink Pedia - Pink Premium - Pink Reality - Pink Romance - Pink SCI FI & Fantasy - Pink Serije - Pink Show - Pink Soap - Pink Style - Pink Super Kids - Pink Thriller - Pink Western - PINK World - Pink World Cinema - PINK Zabava - Planete+ PL - Planete+ PL - Planete+ PL - Playboy TV - Polonia1 - Polonia1 - Polonia1 - POLO TV - POLO TV - POLO TV - Polsat 1 - Polsat 1 - Polsat 1 - Polsat 2 - Polsat 2 - Polsat 2 - Polsat - Polsat Cafe - Polsat Cafe - Polsat Cafe - Polsat Comedy Central Extra - Polsat Comedy Central Extra - Polsat Doku - Polsat Doku - Polsat Doku - Polsat Film - Polsat Film - Polsat Film - Polsat Games - Polsat Games - Polsat Music - Polsat Music - Polsat Music - Polsat News 2 - Polsat News 2 - Polsat News 2 - Polsat News - Polsat News - Polsat News - Polsat Play - Polsat Play - Polsat Play - Polsat Rodzina - Polsat Rodzina - Polsat Rodzina - Polsat Seriale - Polsat Seriale - Polsat Seriale - Polsat Viasat Explore - Polsat Viasat Explore - Polsat Viasat Explore - Polsat Viasat History - Polsat Viasat Nature - Pop Max - Pop Max - Pop UK - Pop UK - Power TV PL - Power TV PL - Power TV PL - Premier League TV - Premier Sports 1 - Premier Sports 1 - Premier Sports 2 - Premier Sports 2 - Private TV - Private TV - PRO 7 Österreich - PRO 7 Österreich - PRO 7 Österreich - ProSieben - Prva FILES - Prva KICK - Prva LIFE - Prva MAX - Prva Plus - Prva Srpska TV - Prva TV Crna Gora - Prva World - Puls 4 - Puls 4 - Puls 4 - Quest Red - Quest Red - Quest TV - Quest TV - QVC Beauty - QVC Beauty - QVC Style - QVC Style - Radio Televizija BN - Radio Televizija Budva - Radio Televizija Federacije BIH - RAI 4 - RAI 4 - RAI 5 - RAI 5 - RAI DUE - RAI DUE - RAI TRE - RAI UNO - RAI UNO - Rai Yoyo - Rai Yoyo - Rai Yoyo - Reality Kings - Really UKTV - Really UKTV - Red Carpet TV PL - Red Carpet TV PL - Red Carpet TV PL - RedLight HD - RED tv - Report TV - Revelation TV - Revelation TV - RT Documentary - RTÉ2 - RTÉjr - RTÉ News - RTÉ One - RTK 1 - RTL 2 - RTL - RTL Croatia World - RTL Kockica - RTL Living - RTRS - RTRS PLUS - RTS 1 - RTS 2 - RTS 3 - RTS Drama - RTS Klasika - RTS Kolo - RTS Muzika - RTS Nauka - RTS Poletarac - RTS SVET - RTS Trezor - RTS Život - RTV Most - RTV Novi Pazar - RT Vojvodina 1 - RT Vojvodina 2 - RTV PINK - RTV Slovenija 1 - Russia Today - Russia Today - S4C - S4C - Sat.1 Österreich - Sat.1 Österreich - Sat.1 Österreich - Scan TV - SciFi - Show TV - SinemaTV 2 - SinemaTV 2 - SinemaTV 1001 - SinemaTV 1001 - SinemaTV 1002 - SinemaTV 1002 - SinemaTV - SinemaTV - SinemaTV Aile 2 - SinemaTV Aile 2 - SinemaTV Aile - SinemaTV Aile - SinemaTV Aksiyon 2 - SinemaTV Aksiyon 2 - SinemaTV Aksiyon - SinemaTV Aksiyon - SinemaTV Komedi 2 - SinemaTV Komedi 2 - SinemaTV Komedi - SinemaTV Komedi - SinemaTV Yerli 2 - SinemaTV Yerli 2 - SinemaTV Yerli - SinemaTV Yerli - Sixx AT - Sixx AT - Sixx AT - SK Fight - SK Golf - Sky Arts - Sky Arts - Sky Atlantic - Sky Atlantic - Sky Cinema Action - Sky Cinema Action - Sky Cinema Comedy - Sky Cinema Comedy - Sky Cinema Drama - Sky Cinema Greats - Sky Cinema Hits - Sky Cinema Premiere - Sky Cinema Select - Sky Cinema SF Horror - Sky Crime - Sky Crime - Sky Documentaries - Sky Family - Sky Family - Sky History 2 - Sky History 2 - Sky History - Sky Kids - Sky Kids - Sky Max - Sky Max - Sky Mix - Sky Mix - Sky Nature UK - Sky Nature UK - Sky News - Sky News - Sky News - Sky Replay - Sky Sci-Fi - Sky Sci-Fi - Sky Showcase - Sky Showcase - Sky Sports+ - Sky Sports Action - Sky Sports Cricket - Sky Sports F1 - Sky Sports Football - Sky Sports Golf - Sky Sports Main Event - Sky Sports Mix - Sky Sports News - Sky Sports Premier League - Sky Sports Racing - Sky Sports Racing - Sky Witness - Sky Witness - Smithsonian Channel - Smithsonian Channel - SonLife - SonLife - Sony Channel - Sony Channel - SOS Plus - Sport Klub 1 Slovenija - Sport Klub 3 - STAR Channel - STAR Crime - STAR Life - STAR Movies - Stars TV PL - Stars TV PL - Star TV TR - Star TV TR - Star TV TR - Stinet - Stingray Djazz - Stopklatka - Stopklatka - Stopklatka - Studio B - STV Folk - STV UK - Sundance TV PL - Sundance TV PL - Sundance TV PL - Super Polsat - Super Polsat - Super Polsat - Superstar 2 - Superstar TV - Syri TV - Talking Pictures TV - Talking Pictures TV - Tanjug Tačno - TAY TV - TAY TV - TAY TV - TBN Polska HD - TBN Polska HD - TBN Polska HD - TELE 1 - TELE 1 - TELE 1 - Televizija 24 - Televizija Crne Gore 1 - Televizija Crne Gore 2 - Televizija Crne Gore 3 - Televizija Crne Gore MNE - Televizija Doktor - Televizija TV7 - Televizioni 7 - Telewizja 13 - Telewizja 13 - Telewizja 13 - teve2 - teve2 - TG4 Ireland - TGCOM24 - TGRT Belgesel - TGRT Belgesel - TGRT Belgesel - TGRT EU - That's TV - The History Channel - The History Channel - The Nautical Channel - Tiny Pop TV - Tiny Pop TV - Tip TV - Tivibu Spor - Tivibu Spor - Tivibu Spor - TLC - TLC UK - TLC UK - TNT Sports 1 - TNT Sports 1 - TNT Sports 2 - TNT Sports 2 - TNT Sports 4 - TNT Sports 4 - TNT Sports Europe - TNT Sports Europe - TNT Sports Ultimate - TNT Sports Ultimate - Together - Together - TOGGO plus - Top Channel - Top News - Toxic Folk - Toxic TV - Trace Urban - Trans World Radio - Travel Channel - Tring Smile - TRT 1 - TRT 4K - TRT 4K - TRT 4K - TRT Arapça - TRT Avaz - TRT Belgesel - TRT Çocuk - TRT Diyanet - TRT EBA TV İlkokul - TRT EBA TV İlkokul - TRT EBA TV İlkokul - TRT EBA TV Lise - TRT EBA TV Lise - TRT EBA TV Lise - TRT EBA TV Ortaokul - TRT EBA TV Ortaokul - TRT EBA TV Ortaokul - TRT Haber - TRT Kurdî - TRT Müzik - TRT Spor - TRT Spor Yıldız - TRUE CRIME - TRUE CRIME - TRUE CRIME XTRA - TRUE CRIME XTRA - TTV - TTV - TV 4 - TV 4 - TV5 - TV5 - TV5 - TV5Monde Europe - TV 8.5 - TV 8.5 - TV 8 TR - TV100 TR - TV100 TR - TV100 TR - TV Duga + SAT - TV Dukagjini - TVE Internacional - TVN 7 - TVN 7 - TVN 7 - TVN24 - TVN - TV Net - TV Net - TV Net - TVN Style - TVN Style - TVN Style - TVN Turbo - TVN Turbo - TVN Turbo - TVP 1 - TVP 1 - TVP 2 - TVP 2 - TVP 2 - TVP 3 - TVP 3 - TVP ABC - TVP ABC - TVP HD - TVP HD - TVP HD - TVP Historia - TVP Historia - TVP Info - TVP Info - TVP Info - TV PINK EXTRA - TV PINK PLUS - TVP Kobieta - TVP Kobieta - TVP Kultura - TVP Kultura - TVP Regionalna - TVP Rozrywka - TVP Rozrywka - TVP Seriale - TVP Seriale - TVP Seriale - TV Puls PL - TVP World - TVR PL - TVR PL - TVS - TVS - TV Silesia - TV Silesia - TVT PL - TVT PL - TVT PL - TV Trwam - TV Trwam - TV Trwam - TV Vijesti - U&Alibi - U&Alibi - Uçankuş TV - Uçankuş TV - U&Dave - U&Dave - U&Drama - U&Drama - U&Gold - U&Gold - Ülke TV - Ulster TV - Ulster TV - UNA TV - U&W - U&W - U&Yesterday - U&Yesterday - Vavoom - Vesti - VG TV - Viasat Explore CEE - Viasat History - Viasat Kino Balkan - Viasat Nature CEE - Vikom - Virgin Media Four - Virgin Media More - Virgin Media One - Virgin Media Three - Virgin Media Two - Vivid Red - Vivid TV - Vizion Plus - WION - World Fashion Channel - wPolsce PL - wPolsce PL - wPolsce PL - Wydarzenia 24 - Wydarzenia 24 - Wydarzenia 24 - Xtreme TV - Xtreme TV - XXL - XXL - Yaban TV - Yaban TV - Yaban TV - ZDFinfo - ZDFneo - Zdrava televizija - Zico TV - Наша ТВ - Россиᴙ 24 - Телевизија Храм - 2X2 - 3/24 - 4Fun Dance - 4Fun Kids - 4FunTV - 7 TV Región Murcia - 13 Ulica - #Vamos - A2TV TR - Acción por M+ - Active Family - Adult Channel 2 - Adult Channel - Adventure HD - a Haber - Ale Kino+ - ALFA TVP - Al Jazeera - Alquiler 1 - AMC Break PT - AMC Crime PT - AMC PL - AMC PT - Andalucía TV - a News - Antena.nova - A Para - À Punt ES - Aragón TV - AXN ES - AXN Movies ES - AXN PL - AXN PT - AXN Spin PL - AXN White PL - AXN White PT - BabyTV - BabyTV ES - BBC World News - BBN Türk - Betis TV ES - BFM TV - Bloomberg HT - Bloomberg TV - Boing ES - BOM Cine - BuenViaje - Canal 24H - Canal 33 - Canal+ Domo - CANAL+ Kuchnia - Canal Extremadura - Canal Fútbol Replay - Canal Hollywood PT - Canal J - Canal Parlamento - Canal Sur - Capital XTRA - Casa e Cozinha - Castilla la Mancha TV - Caza y Pesca ES - Cine Español por M+ - Clan TVE - Clásicos por M+ - Club MTV International - CNBC Europe - CNN Europe - Comedia por M+ - Comedy Central ES - Comedy Central PL - COSMO - Crime+Investigation PL - Daystar - DAZN 1 ES - DAZN 2 ES - DAZN 3 ES - DAZN 4 ES - DAZN F1 - DAZN LALIGA 2 - DAZN LALIGA - DBike Channel - Deportes 2 por M+ - Deportes por M+ - Deutsche Welle English - Deutsche Welle Espanol - Disco Polo Music PL - Discovery Animal Planet - Discovery Historia - Disney Junior ES - Dizi Smart Max - Dizi Smart Premium - DMAX ES - DMAX TR - Documentales por M+ - Dorcel TV - Dorcel XXX - Drama por Movistar Plus+ - Dream Türk - Ekotürk - El Garage TV - Ellas Vamos por M+ - El Toro TV - Erox HD - Eroxxx HD - Eska Rock TV - Eska TV - Eska TV Extra - Esport3 - ETB 1 - ETB 2 - ETB 3 - ETB 4 - ETB Basque - Eurochannel - EuroNews - Euronews FR - European League of Football Channel - Eurosport 1 ES - Eurosport 2 ES - EWTN ES - EWTN PL - Extreme Sports Channel - Fashion TV - Feel Good - Fight Klub HD - Fight Time - Filmax - FilmBox Family PL - Film Cafe PL - Fokus TV - Folx TV - FOX NEWS - France24 - FX Comedy PL - FX PL - Golf 2 por M+ - Golf por M+ - GOL PLAY ES - Haber Global - Habitat TV - HispanTV - HIT TV ES - Hustler HD - HUSTLER TV - Iberalia HD CAZA - Iberalia HD PESCA - Iberalia TV - Indie por M+ - Insight TV - iTVN - iTVN Extra - Kanal 7 - KANAL 24 TR - Kanal D TR - Karadeniz TV - Kino Polska - Kino Polska Muzyka - krone.tv - La 1 Cataluña - La 7 Murcia - LA 8 MEDITERRANEO - La dos - LALIGA TV 2 por M+ - LALIGA TV 3 por M+ - LALIGA TV HYPERMOTION 2 - LALIGA TV HYPERMOTION 3 - LALIGA TV HYPERMOTION - LALIGA TV por M+ - La Otra ES - La primera - La Resistencia por M+ - La Rioja - LEVANTE TV - Liga de Campeones 2 por M+ - Liga de Campeones 3 por M+ - Liga de Campeones por M+ - LUXE TV - M+ Deportes 3 - M+ Originales - Melodie TV - Metro TV - Mezzo - Mezzo Live HD - MinikaGO TR - Minimini+ - MOTO ADV - Motorvision Plus - Motorvision Plus International - Motowizja - MovieSmart Classic - MovieSmart Türk - Movistar Estrenos - Movistar Plus+ 2 - Movistar Plus+ - MTV 00s - MTV 80s - MTV 90s International - MTV ES - MTV Hits International - MTV Live HD - MTV Music UK - MyZen TV - Nat Geo Wild ES - National Geographic ES - Nautica TV - NBC News - Nickelodeon ES - Nickelodeon PT - Nick Junior Commercial Light - Novelas+ - Novelas Plus 1 - Nowa TV - Number One Türk - NUTA.TV HD - NUTA GOLD - Odisseia - oe24.TV - Paramount Comedy ES - Paramount Network ES - Planete+ PL - Polonia1 - POLO TV - Polsat 1 - Polsat 2 - Polsat Cafe - Polsat Comedy Central Extra - Polsat Doku - Polsat Film - Polsat Games - Polsat Music - Polsat News 2 - Polsat News - Polsat Play - Polsat Rodzina - Polsat Seriale - Polsat Viasat Explore - Power TV PL - Private TV - RAI 4 - RAI 5 - RAI DUE - RAI TRE - RAI UNO - RAI World Premium - Rai Yoyo - Real Madrid TV - Red Carpet TV PL - RedLight HD - Russia Today ES - Sevilla FC TV - SinemaTV 2 - SinemaTV 1001 - SinemaTV 1002 - SinemaTV - SinemaTV Aile 2 - SinemaTV Aile - SinemaTV Aksiyon 2 - SinemaTV Aksiyon - SinemaTV Komedi 2 - SinemaTV Komedi - SinemaTV Yerli 2 - SinemaTV Yerli - Sky News - Star Channel ES - Stars TV PL - Star TV TR - Stingray Classica - Stingray Djazz - Stopklatka - Sundance TV PL - Super Polsat - SX3 - TAY TV - TBN Polska HD - TCM ES - TELE 1 - TeleDeporte - TELE ELX - TeleMadrid - Televisión de Galicia - Telewizja 13 - teve2 - teve2 TR - TGRT Belgesel - Tivibu Spor - TPA7 - Trece TV - TRT 1 - TRT 2 - TRT 4K - TRT Arapça - TRT Avaz - TRT Belgesel - TRT Çocuk - TRT EBA TV İlkokul - TRT EBA TV Lise - TRT EBA TV Ortaokul - TRT Haber - TRT Kurdî - TRT Müzik - TRT Spor - TRT Turk - TRT World - TTV - TV3 Catalunya - TV3 ES - TV 4 - TV5 - TV5Monde Europe - TV 8.5 - TV100 TR - TV Canaria - TV Chile - TVE Internacional - TVG2 - TV Galicia - TVN 7 - TVN24 - TVN - TV Net - TVN Style - TVN Turbo - TVP 2 - TVP 3 - TVP ABC - TVP HD - TVP Historia - TVP Info - TVP Kobieta - TVP Kultura - TVP Regionalna - TVP Rozrywka - TVP Seriale - TV Puls PL - TVR PL - TVS - TV Silesia - TVT PL - TV Trwam - Uçankuş TV - Vacaciones por M+ - Verdi - VG TV - Vivid Red - Vivid TV - Warner TV ES - Warner TV IT - World Fashion Channel - wPolsce PL - Wydarzenia 24 - Xtreme TV - XXL - Yaban TV - 2X2 - 3/24 - 3 Plus CH - 4Fun Dance - 4Fun Kids - 4FunTV - 4 Plus - 5 Plus - 6 plus - 6ter - 13 Ulica - 360 TV - A2TV TR - AB3 - Action - Active Family - Adult Channel 2 - Adult Channel - Adventure HD - a Haber - Al Arabiya - Ale Kino+ - ALFA TVP - Al Jazeera - AMC PL - a News - A Para - À Punt ES - ARTE DE - ARTE FR - ATV1 - ATV2 - auftanken.TV - Auto Moto FR - AXN PL - AXN Spin PL - AXN White PL - BabyTV - BBC World News - BBN Türk - beIN GURME - beIN iZ - beIN Movies Premiere TR - beIN Movies Turk - beIN Sports 1 FR - beIN Sports 2 FR - beIN Sports 3 FR - beIN Sports MAX 4 FR - beIN Sports MAX 5 FR - beIN Sports MAX 6 FR - beIN Sports MAX 7 FR - beIN Sports MAX 8 FR - beIN Sports MAX 9 FR - beIN Sports MAX 10 FR - Benfica TV - BET FR - Beyaz TV - BFM Business - BFM TV - Bloomberg HT - Bloomberg TV - Boing - Boomerang FR - Boomerang UK - Brazzers TV (ex. Private Spice) - Canal 24H - Canal+ Cinéma(s) FR - CANAL+ DOCS - Canal+ Domo - Canal+ France - CANAL+ GRAND ECRAN - Canal+ Kids FR - CANAL+ Kuchnia - Canal+ Series FR - CANAL+SPORT360 - Canal J - Canal Plus Sport FR - Cartoonito CEE - Cartoon Network - Cartoon Network FR - CCTV 4 Europe - CGTN - CGTN Documentary - Chérie 25 - Ciné+ Classic - Ciné+ Club - Ciné+ Emotion - Ciné+ Famiz - Ciné+ Frisson - Cine+ Premier FR - CNBC Europe - CNews - CNN Europe - CNN Türk TV - Comedie+ - Comedy Central FR - Comedy Central PL - Comedy Central UK - Crime+Investigation PL - Crime District - CStar FR - CStar Hits France - Das Erste - Daystar - Deutsche Welle English - Disco Polo Music PL - Discovery Animal Planet - Discovery Channel FR - Discovery Historia - Discovery Investigation FR - Disney Channel FR - Disney Junior FR - Dizi Smart Max - Dizi Smart Premium - DMAX TR - Dorcel TV - Dorcel XXX - Dream Türk - E! Entertainment - Ekotürk - English Club TV - Erox HD - Eroxxx HD - Eska Rock TV - Eska TV - Eska TV Extra - ETB 1 - ETB 2 - ETB 3 - ETB 4 - ETB Basque - Eurochannel - Eurochannel FR - Euro D - EuroNews - Euronews FR - European League of Football Channel - Eurosport 2 FR - Eurosport FR - Euro Star - EWTN - EWTN PL - Fashion TV - FightBox HD - Fight Klub HD - Filmax - FilmBox Family PL - Film Cafe PL - Fokus TV - Folx TV - FOX NEWS - France 2 - France 3 - France 4 - France 5 - France24 - France24 Arabic - France24 Arabic MENA - France 24 French - France Info - FX Comedy PL - FX PL - Game One+1 - Game One - Ginx Esports TV - Golf Channel Češka - Golf plus - Gulli - Haber Global - Habertürk - Habitat TV - Hustler HD - HUSTLER TV - i24News FR - Infosport+ - Insight TV - iTVN - iTVN Extra - J-One - Kabel Eins - Kanal 7 - KANAL 24 TR - Kanal D TR - Karadeniz TV - KBS World HD - KiKA - Kino Polska - Kino Polska Muzyka - krone.tv - La7 - La Chaîne Info - LCP - L'Equipe - M6 - Mangas - MCM FR - MCM Top - Mediaset Italia - Melodie TV - Metro TV - Mezzo - Mezzo Live HD - MGG TV - MinikaGO TR - Minimini+ - Motorvision Plus France - Motorvision Plus International - Motowizja - MovieSmart Classic - MovieSmart Türk - MTV 00s - MTV 80s - MTV Europe - MTV FR - MTV Hits FR - MTV Hits International - Museum TV - Nat Geo Wild FR - National Geographic FR - NBC News - Nickelodeon FR - Nickelodeon Junior FR - Nickelodeon Teen FR - Novelas+ - Novelas Plus 1 - Novelas TV FR - NOW - Nowa TV - NTV DE - Number One Türk - NUTA.TV HD - NUTA GOLD - OCS Geants - OCS Max - OCS Pulp - oe24.TV - OLTV - Olympia TV - ORF2 - Paramount Channel FR - Paris premiere - PINK Film - Pink Music 1 - Piwi+ - Planète+ - Planète+ Aventure - Planete+ Crime - Planete+ PL - Playboy TV - Polar+ - Polonia1 - POLO TV - Polsat 1 - Polsat 2 - Polsat Cafe - Polsat Comedy Central Extra - Polsat Doku - Polsat Film - Polsat Games - Polsat Music - Polsat News 2 - Polsat News - Polsat Play - Polsat Rodzina - Polsat Seriale - Polsat Viasat Explore - Power Türk TV - Power TV PL - Private TV - PRO 7 Österreich - ProSieben - Puls 4 - Puls 8 - Radio Ticino Channel HD - RAI 4 - RAI 5 - RAI DUE - RAI Education - RAI News 24 - RAI Storia - RAI TRE - RAI UNO - RAI World Premium - Rai Yoyo - Reality Kings - Real Madrid TV - Red Carpet TV PL - RedLight HD - RMC Découverte - RMC Sport 1 - RMC Sport 2 - RMC Sport Live 3 - RMC Sport Live 4 - RMC Sport Live 5 - RMC Sport Live 6 - RMC Sport Live 7 - RMC Sport Live 8 - RMC Sport Live 9 - RMC Sport Live 10 - RMC Sport Live 11 - RMC Sport Live 12 - RMC Sport Live 13 - RMC Sport Live 14 - RMC Sport Live 15 - RMC Sport Live 16 - RMC Sport Live 17 - RMC Story - RSI La 1 - RSI La 2 - RT Documentary - RTL DE - RTL Nitro - RTL Zwei - RTP3 - RTS 2 Suisse - RTS SVET - Russia Today - S1 CH - SAT.1 - Sat.1 Österreich - Seasons - Show Türk - Show TV - SIC - SIC Internacional - SinemaTV 2 - SinemaTV 1001 - SinemaTV 1002 - SinemaTV - SinemaTV Aile 2 - SinemaTV Aile - SinemaTV Aksiyon 2 - SinemaTV Aksiyon - SinemaTV Komedi 2 - SinemaTV Komedi - SinemaTV Yerli 2 - SinemaTV Yerli - Sixx AT - Sky News - SRF 1 - SR Fernsehen - SRF Info - SRF Zwei - Stars TV PL - Star TV TR - Stingray Classica - Stingray CMusic - Stingray Djazz - Stingray iConcerts - Stopklatka - Sundance TV PL - Super Polsat - Super RTL - SWR - SWR Baden-Württemberg - SX3 - TAY TV - TBN Polska HD - TCM Cinéma - TELE 1 - TeleBärn - TeleDeporte - Télétoon+ FR - Televisión de Galicia - Télévision française 1 - Telewizja 13 - Tele Zürich - teve2 - teve2 TR - TF1 Séries Films - TFX - TGCOM24 - TGRT Belgesel - TGRT EU - TGRT HABER - The Nautical Channel - TiJi - Tivibu Spor - TMC - Trace Urban - Travel Channel - TRT 1 - TRT 4K - TRT Arapça - TRT Avaz - TRT Belgesel - TRT Çocuk - TRT Diyanet - TRT EBA TV İlkokul - TRT EBA TV Lise - TRT EBA TV Ortaokul - TRT Haber - TRT Kurdî - TRT Müzik - TRT Spor - TRT Turk - TRT World - TTV - TV3 Catalunya - TV 4 - TV5 - TV5 Monde - TV5Monde Asia - TV5Monde Europe - TV 8.5 - TV 8 TR - TV24 - TV25 - TV100 TR - TV1000 Global Kino - TV Breizh - TVE Internacional - TVG2 - TV Galicia - TVN 7 - TVN - TV Net - TVN Style - TVN Turbo - TVP 1 - TVP 2 - TVP 3 - TVP ABC - TVP HD - TVP Historia - TVP Info - TV PINK EXTRA - TV PINK PLUS - TVP Kobieta - TVP Kultura - TVP Regionalna - TVP Rozrywka - TVP Seriale - TV Puls PL - TVR PL - TVS - TV Silesia - TVT PL - TV Trwam - Uçankuş TV - Ülke TV - Vesti - VG TV - Vivid Red - Vivid TV - Vosges Télévision - VOX - W9 - Warner TV FR - Welt - wPolsce PL - Wydarzenia 24 - Xtreme TV - Yaban TV - ZDF - ZDFinfo - Первый - 2X2 - 3SAT - 13 Ulica - 20 Mediaset - 24 Kitchen - 24Kitchen PT - 360 TuneBox - A2TV TR - Active Family - Adult Channel - Adventure HD - a Haber - Ale Kino+ - ALFA TVP - Al Jazeera - Al Jazeera Balkans - Alternativna televizija Banja Luka - AMC - AMC PL - a News - A Para - Arena Esport - Arena Fight - Arena Sport 1 HR - Arena Sport 2 HR - Arena Sport 3 HR - Arena Sport 4 HR - Arena Sport 5 HR - Arena Sport 6 HR - Arena Sport 7 HR - Arena Sport 8 HR - Arena Sport 9 HR - Arena Sport 10 HR - ARTE DE - ATV1 - ATV2 - Aurora TV - AXN - AXN Black PL - AXN PL - AXN Spin - AXN Spin PL - AXN White PL - B1 TV - B92 - BabyTV - Balkanika TV - Balkan trip - Balkan TV - BBC Earth - BBC First - BBC World News - BBN Türk - Bloomberg Adria - Bloomberg HT - Bloomberg TV - Blue Hustler - BN 2 HD - BN music - Body in Balance - Brazzers TV (ex. Private Spice) - Canal+ Domo - CANAL+ Kuchnia - Cartoonito CEE - Cartoonito UK - Cartoon Network - CBS Reality - CCTV 4 Europe - CGTN - CGTN Documentary - Cinemax 2 - Cinemax - CineStar Action&Thriller - CineStar Premiere 1 - CineStar Premiere 2 - CineStar TV1 - CineStar TV2 - CineStar TV Comedy Family - CineStar TV Fantasy - Club MTV International - CNBC Europe - CNN Europe - Comedy Central DE - Comedy Central PL - Crime+Investigation PL - Crime & Investigation Channel - Croatian Music Channel - Das Erste - Da Vinci Learning - Deluxe Music - Deutsche Welle English - Diadora TV - Disco Polo Music PL - Discovery Animal Planet - Discovery Channel - Discovery Historia - Disney Channel - DIVA (ex. Universal) - DIZI - Dizi Smart Max - Dizi Smart Premium - DMAX DE - DMAX TR - DMC televizija - DM SAT - DOKU TV - Doma TV - Dorcel TV - Dorcel XXX - DOX TV - Dream Türk - ducktv HD - ducktv SD - E! Entertainment - Ekotürk - English Club TV - Epic Drama (CEE) - Erox HD - Eroxxx HD - Eska Rock TV - Eska TV Extra - Eurochannel - EuroNews - European League of Football Channel - Eurosport 1 DE - Eurosport 2 - Eurosport 2 DE - Eurosport.com - Eurosport - EWTN PL - ExtraTV - Extreme Sports Channel - FACE TV - FashionBox HD - Fashion TV - Fast&FunBox HD - FightBox HD - Fight Klub HD - Film4 - Filmax - FilmBox Arthouse - FilmBox Extra RS - FilmBox Family PL - FilmBox Premium RS - FilmBox Stars RS - Film Cafe PL - Fireplace - Fokus TV - Folx TV - Food Network - FOX NEWS - France24 - France 24 French - Fuel TV - FullTV - FunBox UHD - FX Comedy PL - FX PL - GameHub HR - Gametoon HD - Ginx Esports TV - Golica TV - Good Times - GP1 - Grand Televizija - Great! Movies +1 - Great! Movies Action - Great! Movies Classic - Haber Global - Habitat TV - Hajduk TV - HappyTV - Hayat Folk Box - Hayatovci - Hayat TV - HBO2 - HBO3 - HBO - HEMA TV - History Channel 2 - HIT TV - Home & Garden Television - HRT 1 - HRT 2 - HRT 3 - HRT 4 - HRT Int. - HSE - HUSTLER TV - ICT Business - Italia 2 - JimJam - Jugoton TV - K::CN 1 Kopernikus - K::CN 2 Music - K::CN 3 Svet Plus - Kabel Eins - Kabel eins Doku - Kabel eins Österreich - KANAL 24 TR - Kanal Rijeka - Karadeniz TV - KBS World HD - KiKA - Kino Polska - Kino Polska Muzyka - KinoTV - KitchenTV - Klape i Tambure TV - KLASIK - KLASIK HR - krone.tv - La7 - La7d - La Cinque - Laudato TV - Legend - Libertas TV - Lov i ribolov - LUXE TV - M1 Family - M1 FILM - M1 Gold - Maria Vision - MAXSport 1 - MAXSport 2 - MAXSport 3 - MAXSport 4 - MAXSport 5 - Mediaset Italia - Melodie TV - Metro TV - Mezzo - Mezzo Live HD - MG Movie Generation - MinikaGO TR - Minimini+ - MiniTV - Motowizja - MovieSmart Classic - MovieSmart Türk - MrežaZG - MRT SAT - MTV 00s - MTV 80s - MTV 90s International - MTV Europe - MTV Hits International - MTV Live HD - MyZen TV - N1 HR - N24 Doku - Narodna TV - Nat Geo Wild - Nat Geo Wild HD - National Geographic - National Geographic Channel HD - NBA TV - Nickelodeon - Nick Junior - Nick Junior UK - Nick Music - Nicktoons - Nova BH - Nova Plus Cinema - Nova Plus Family - Nova Sport Srbija - NOVA TV - Nova World - Novelas+ - Novelas Plus 1 - NOW - Nowa TV - NTV DE - Number One Türk - NUTA.TV HD - NUTA GOLD - OBN - oe24.TV - O Kanal - One - ORF1 - ORF2 - Osječka televizija - Otvorena televizija - OTV Valentino - Phoenix - Pickbox TV - Pikaboo 2 - Pink BH - Pink Fashion - PINK Film - Pink Folk 1 - Pink Kids - Pink Koncert - Pink M - Pink Music 1 - PINK World - Planete+ PL - Plava televizija - Plava vinkovačka TV - Playboy TV - Poljoprivredna TV - Polonia1 - POLO TV - Polsat 2 - Polsat Cafe - Polsat Comedy Central Extra - Polsat Doku - Polsat Film - Polsat Games - Polsat Music - Polsat News 2 - Polsat News - Polsat Play - Polsat Rodzina - Polsat Seriale - Polsat Viasat Explore - Polsat Viasat History - Polsat Viasat Nature - Pop Max - Power TV PL - Private TV - PRO 7 Österreich - ProSieben - ProSieben MAXX - Prva Plus - Prva Srpska TV - QVC Deutschland - Radiotelevizija Banovina - Radio Televizija BIH - Radio Televizija BN - Radio Televizija Federacije BIH - RAI 4 - RAI 5 - RAI DUE - RAI Education - RAI News 24 - RAI Sport 1 - RAI Storia - RAI TRE - RAI UNO - Rai Yoyo - Reality Kings - Red Carpet TV PL - RedLight HD - RED tv - RiC DE - RTL 2 - RTL - RTL Adria - RTL Crime - RTL DE - RTL Kockica - RTL Living - RTL Passion - RTLup - RTL Zwei - RTRS - RTS 1 - RTS 2 - RTS 3 - RTS SVET - RTV Herceg-Bosne - RT Vojvodina 1 - RT Vojvodina 2 - RTV PINK - RTV Slovenija 1 - RTV Slovenija 2 - RTV Slovenija 3 - Russia Today - Saborska TV - Samobor TV - SAT.1 - Sat.1 Gold - Sat.1 Österreich - SciFi - ServusTV - Show TV - SinemaTV 2 - SinemaTV 1001 - SinemaTV 1002 - SinemaTV - SinemaTV Aile 2 - SinemaTV Aile - SinemaTV Aksiyon 2 - SinemaTV Aksiyon - SinemaTV Komedi 2 - SinemaTV Komedi - SinemaTV Yerli 2 - SinemaTV Yerli - Sixx AT - SK Fight - SK Golf - Sky News - SkyShowtime 1 Nordic - Slavonskobrodska Televizija - sonnenklar.TV - Sony Channel - SOS Plus - Sport 1 DE - Sport Klub 1 Hrvatska - Sport Klub 2 Hrvatska - Sport Klub 3 - Sport Klub 4 - Sport Klub 5 - Sport Klub 6 - Sportska Televizija - STAR Channel - STAR Crime - STAR Life - STAR Movies - Star TV TR - Stingray Classica - Stingray CMusic - Stingray Djazz - Stingray iConcerts - Stopklatka - Studio B - Sundance TV PL - Super Polsat - Super RTL - Supertennis HD - Tanjug Tačno - TAY TV - TBN Polska HD - TELE 1 - Tele 5 DE - Telequattro - Televizija 24 - Televizija Alfa - Televizija Crne Gore MNE - Televizija Dalmacija - Televizija Zapad Zaprešić - Telewizja 13 - teve2 - teve2 TR - TGRT Belgesel - The History Channel - The Nautical Channel - TiJi - Tiny Pop TV - Tivibu Spor - TLC - TOGGO plus - Toon kids - Toxic Folk - Toxic Rap - Toxic TV - Trace Urban - Travel Channel - Trend TV - TRT 4K - TRT EBA TV İlkokul - TRT EBA TV Lise - TRT EBA TV Ortaokul - TTV - TV 4 - TV5 - TV5 Monde - TV5Monde Europe - TV 8.5 - TV 8 TR - TV100 TR - TVE Internacional - TV Jadran - TVN 7 - TVN24 - TV Net - TV NOVA Pula - TVN Style - TVN Turbo - TVP 2 - TVP 3 - TVP ABC - TVP HD - TVP Historia - TVP Info - TV PINK EXTRA - TV PINK PLUS - TVP Kobieta - TVP Kultura - TVP Regionalna - TVP Rozrywka - TVP Seriale - TV Puls PL - TVR PL - TVS - TV Silesia - TV Slavonije i Baranje - TVT PL - TV Trwam - TV Vijesti - TV Zelina - Uçankuş TV - UNA TV - Varaždinska Televizija - Vavoom - Vesti - VG TV - Viasat Explore CEE - Viasat History - Viasat Kino Balkan - Viasat Nature CEE - Viasat True Crime - Vivid Red - Vivid TV - Vizion Plus - VOX - Welt - Wydarzenia 24 - Xtreme TV - XXL - Yaban TV - Zagrebačka Televizija - ZDF - ZDFinfo - ZDFneo - Zdrava televizija - ZONASPORT TV - 2X2 - 4Fun Dance - 4Fun Kids - 4FunTV - 13 Ulica - 360 TuneBox - A2TV TR - Active Family - Adult Channel 2 - Adult Channel - Adventure HD - a Haber - Ale Kino+ - ALFA TVP - AMC HU - AMC PL - a News - A Para - Apostol TV - ARD-alpha - Arena4 HU - ATV1 - ATV2 - ATV HU - ATV Spirit HU - AXN HU - AXN PL - AXN Spin PL - AXN White PL - BabyTV - BBC News Channel - BBC World News - BBN Türk - Bloomberg HT - Bloomberg TV - Body in Balance - Canal+ Domo - Canal+ France - CANAL+ Kuchnia - Cartoonito CEE - CBS Reality - CCTV 4 Europe - Cinemax 2 HU - Cinemax HU - Club MTV International - CNN Europe - Comedy Central HU - Comedy Central PL - CoolTV - Crime+Investigation PL - D1 TV HU - Das Erste - Da Vinci Learning - Deutsche Welle English - Disco Polo Music PL - Discovery Channel HU - Discovery Historia - Disney Channel HU - Dizi Smart Max - Dizi Smart Premium - DMAX TR - Dorcel XXX - Dream Türk - ducktv HD - ducktv SD - Duna TV - Duna World - E! Entertainment - Ekotürk - English Club TV - Epic Drama (CEE) - Erox HD - Eroxxx HD - Eska Rock TV - Eska TV - Eska TV Extra - European League of Football Channel - Extreme Sports Channel - FashionBox HD - Fashion TV - Fast&FunBox HD - Fehérvár TV - FightBox HD - Fight Klub HD - Film4 HU - Film+ HU - Filmax - FilmBox Arthouse - Filmbox Extra HU - Filmbox Family HU - FilmBox HU - Filmbox Premium HU - Filmbox Stars HU - Film Cafe HU - Film Cafe PL - Film Mania HU - Fokus TV - Folx TV - Food Network - France24 - FunBox UHD - FX Comedy PL - FX PL - Galaxy4 - Gametoon HD - H!T Music Channel RO - Haber Global - Habitat TV - HBO 2 HU - HBO 3 HU - HBO - HBO HU - Hír TV - History Channel 2 - History Channel HU - Home & Garden Television - Home & Garden Television UK - Hustler HD - HUSTLER TV - Investigation Discovery - Investigation Discovery UK - Izaura TV - Jazz TV HU - JimJam - JockyTV - Kanal 7 - KANAL 24 TR - Kanal D TR - Karadeniz TV - KiKA - Kino Polska - Kino Polska Muzyka - Kölyökklub - krone.tv - Life TV HU - LUXE TV - M2 Petőfi - M4 sport - M4 Sport Plus - m5 - Magyar Mozi TV - Magyar Sláger TV - Magyar Televízió 1 - Magyar Televízió 3 - MATCH4 - MAX4 - Mediaset Italia - Melodie TV - Metro TV - Mezzo - Mezzo Live HD - MinikaGO TR - Minimax - Minimax HU - Minimini+ - Motorvision Plus International - Motowizja - MovieSmart Classic - MovieSmart Türk - Moziklub - Mozi plusz TV - Moziverzum - MTV 00s - MTV 80s - MTV 90s International - MTV Europe HU - MTV Hits International - MTV Live HD - Muzsika TV - MyZen TV - Nat Geo Wild HU - National Geographic HU - Nickelodeon Commercial - Nick Junior PL - Nicktoons PL - Novelas+ - Novelas Plus 1 - NOW - Nowa TV - Number One Türk - NUTA.TV HD - NUTA GOLD - oe24.TV - ORF3 - Ozone Network - Paramount Network HU - Planete+ PL - Playboy TV - Polonia1 - POLO TV - Polsat 2 - Polsat Cafe - Polsat Comedy Central Extra - Polsat Doku - Polsat Film - Polsat Games - Polsat Music - Polsat News 2 - Polsat News - Polsat Play - Polsat Rodzina - Polsat Seriale - Polsat Viasat Explore - Polsat Viasat History - Polsat Viasat Nature - Power TV PL - Prime - Private TV - PRO 7 Österreich - ProSieben - Puls 4 - PΛX - RAI TRE - RAI UNO - Rai Yoyo - Red Carpet TV PL - RedLight HD - Romance TV PL - RTL DE - RTL Gold - RTL Három - RTL HU - RTL Ketto - RTL Otthon - RTL Zwei - SAT.1 - Sat.1 Österreich - Show TV - SinemaTV 2 - SinemaTV 1001 - SinemaTV 1002 - SinemaTV - SinemaTV Aile 2 - SinemaTV Aile - SinemaTV Aksiyon 2 - SinemaTV Aksiyon - SinemaTV Komedi 2 - SinemaTV Komedi - SinemaTV Yerli 2 - SinemaTV Yerli - Sixx AT - Sky News - SkyShowtime 1 Nordic - Sorozat+ - Sorozatklub - Spektrum Home HU - Spektrum TV - Spíler1 TV - Spíler2 TV - Sport 1 HU - Sport 2 HU - Stars TV PL - Star TV TR - Stingray Classica - Stingray Djazz - Stingray iConcerts - Stopklatka - Story4 - Sundance TV PL - Super Polsat - Super RTL - Super TV2 - TAY TV - TBN Polska HD - TeenNick - TELE 1 - Telewizja 13 - teve2 - teve2 TR - TGRT Belgesel - The Fishing and Hunting - The History Channel - Tivibu Spor - Travel Channel - TRT 4K - TRT EBA TV İlkokul - TRT EBA TV Lise - TRT EBA TV Ortaokul - TRT World - TTV - TV2 Comedy - TV2 HU - TV2 Kids - TV2 Klub - TV2 Séf - TV 4 - TV4 HU - TV5 - TV 8.5 - TV 8 TR - TV100 TR - TVE Internacional - TVN 7 - TVN24 - TV Net - TVN Style - TVN Turbo - TVP 2 - TVP 3 - TVP ABC - TV Paprika HU - TVP HD - TVP Historia - TVP Info - TVP Kobieta - TVP Kultura - TVP Regionalna - TVP Rozrywka - TVP Seriale - TV Puls PL - TVR PL - TVS - TV Silesia - TVT PL - TV Trwam - Uçankuş TV - UTV RO - Viasat 2 - Viasat3 - Viasat 6 - Viasat Film HU - Viasat History - Viasat Nature CEE - Vivid Red - Vivid TV - VOX - World Fashion Channel - wPolsce PL - Wydarzenia 24 - Xtreme TV - Yaban TV - ZDF - Zenebutik - 2X2 - 3 Plus CH - 4Fun Dance - 4Fun Kids - 4FunTV - 4 Plus - 5 Plus - 6 plus - 7 Gold - 13 Ulica - 20 Mediaset - A2TV TR - Active Family - Adult Channel 2 - Adult Channel - Adventure HD - a Haber - Ale Kino+ - ALFA TVP - Alma TV - AMC PL - a News - A Para - À Punt ES - ATV1 - ATV2 - ATV TR - auftanken.TV - AXN PL - AXN Spin PL - AXN White PL - BabyTV - BBN Türk - BFM TV - Bloomberg HT - Body in Balance - Boing - Boing Plus - Boomerang IT - Canal+ Domo - CANAL+ Kuchnia - Canale 5 - Canale 7 - Canal J - Cartoonito Italia - Cartoon Network IT - CI Crime+ Investigation - cielo - Cine34 - Class TV Moda - Comedy Central PL - Crime+Investigation PL - Daystar - DeA Junior - DeA Kids - Disco Polo Music PL - Discovery Animal Planet - Discovery Channel IT - Discovery Historia - Dizi Smart Max - Dizi Smart Premium - DMAX ES - DMAX IT - DMAX TR - Dream Türk - Ekotürk - Erox HD - Eroxxx HD - Eska Rock TV - Eska TV - Eska TV Extra - European League of Football Channel - Eurosport 2 IT - Eurosport IT - EWTN PL - Fight Klub HD - Filmax - FilmBox Family PL - Film Cafe PL - Focus TV - Fokus TV - Folx TV - Food Network Italia - Frisbee - FX Comedy PL - FX PL - Gambero Rosso Channel - Giallo TV - Haber Global - Habitat TV - History Channel IT - Hustler HD - HUSTLER TV - Insight TV - Inter TV - Italia 1 - Italia 2 - iTVN - iTVN Extra - K2 - Kanal 7 - KANAL 24 TR - Kanal D TR - Karadeniz TV - Kino Polska - Kino Polska Muzyka - krone.tv - La7 - La7 AU - La7d - La Chaîne Info - La Cinque - Lazio Style Channel - LUXE TV - Marco Polo TV - Mediaset Extra - Mediaset Italia - Mediaset Italia AU - Melodie TV - Metro TV - Mezzo Live HD - Milan TV - MinikaGO TR - Minimini+ - MotorTrend - Motowizja - MovieSmart Classic - MovieSmart Türk - MTV 90s International - MTV Hits International - MTV Italia - MTV Music IT - MTV Music UK - MyZen TV - National Geographic - NBC News - Nickelodeon IT - Nick Junior IT - NOVE - Novelas+ - Novelas Plus 1 - NOW - Nowa TV - Number One Türk - NUTA.TV HD - NUTA GOLD - oe24.TV - ORF1 - ORF2 - ORF3 - Parole di Vita - Planete+ PL - Playboy TV - Polonia1 - POLO TV - Polsat 1 - Polsat 2 - Polsat Cafe - Polsat Comedy Central Extra - Polsat Doku - Polsat Film - Polsat Games - Polsat Music - Polsat News 2 - Polsat News - Polsat Play - Polsat Rodzina - Polsat Seriale - Polsat Viasat Explore - Power TV PL - Private TV - PRO 7 Österreich - Puls 4 - Puls 8 - Radiofreccia - Radio Italia TV - RAI 3 Bis - RAI 4 - RAI 5 - RAI DUE - RAI Education - RAI Gulp - RAI News 24 - RAI Sport 1 - RAI Storia - RAI TRE - RAI UNO - RAI World Premium - Rai Yoyo - real time - Red Carpet TV PL - RedLight HD - Rete 4 - RSI La 1 - RSI La 2 - RTS 2 Suisse - RTS Un - S1 CH - Sat.1 Österreich - Show TV - SinemaTV 2 - SinemaTV 1001 - SinemaTV 1002 - SinemaTV - SinemaTV Aile 2 - SinemaTV Aile - SinemaTV Aksiyon 2 - SinemaTV Aksiyon - SinemaTV Komedi 2 - SinemaTV Komedi - SinemaTV Yerli 2 - SinemaTV Yerli - Sixx AT - Sky Arte - Sky Caccia e Pesca - Sky Cinema Action IT - Sky Documentaries IT - Sky Investigation - Sky Nature IT - Sky Pesca e Caccia - Sky Sport 1 IT - Sky Sport 3 IT - Sky Sport24 HD - Sky Sport 251 - Sky Sport 252 - Sky Sport 253 - Sky Sport 254 - Sky Sport 255 - Sky Sport 257 - Sky Sport 258 - Sky Sport 259 - Sky Sport Calcio - Sky Sport F1 IT - Sky Sport MotoGP IT - Sky Sport Plus IT - Sky TG24 HD - Sky Uno - Sportitalia - SRF 1 - SRF Info - SRF Zwei - Stars TV PL - Star TV TR - Stopklatka - Sundance TV PL - Super! - Super Polsat - Supertennis HD - TAY TV - TBN Polska HD - TELE 1 - TeleBärn - Telequattro - Teletutto - Telewizja 13 - Tele Zürich - teve2 - teve2 TR - TGCOM24 - TGRT Belgesel - Tivibu Spor - Top Calcio 24 - Top Crime - TRT 1 - TRT 2 - TRT 4K - TRT Arapça - TRT Avaz - TRT Belgesel - TRT Çocuk - TRT EBA TV İlkokul - TRT EBA TV Lise - TRT EBA TV Ortaokul - TRT Haber - TRT Kurdî - TRT Müzik - TRT Spor - TRT Turk - TRT World - TTV - TV 4 - TV5 - TV 8.5 - TV8 IT - TV24 - TV25 - TV100 TR - TV 2000 - TVE Internacional - TVN 7 - TVN24 - TVN - TV Net - TVN Style - TVN Turbo - TVP 1 - TVP 2 - TVP 3 - TVP ABC - TVP HD - TVP Historia - TVP Info - TVP Kobieta - TVP Kultura - TVP Regionalna - TVP Rozrywka - TVP Seriale - TV Puls PL - TVR PL - TVS - TV Silesia - TVT PL - TV Trwam - Twenty Seven - Uçankuş TV - VG TV - Vivid Red - Vivid TV - Warner TV IT - Welt - World Fashion Channel - wPolsce PL - Wydarzenia 24 - Xtreme TV - XXL - Yaban TV - 2X2 - 4Fun Dance - 4Fun Kids - 13 Ulica - 24 Kitchen - 24Kitchen PT - 360 TV - A2TV TR - ABC News Albania - Active Family - Adventure HD - Agro TV - a Haber - Ale Kino+ - ALFA TV - ALFA TVP - Al Jazeera - Al Jazeera Balkans - Alternativna televizija Banja Luka - AMC - AMC PL - a News - Anixe HD Serie - A Para - Arena Esport - Arena Fight - Arena Sport 1 MK - Arena Sport 1 Premium - Arena Sport 2 Premium - Arena Sport 2 RS - Arena Sport 3 Premium - Arena Sport 3 RS - Arena Sport 4 RS - Arena Sport 5 RS - ARTE DE - ATV1 - ATV2 - ATV Avrupa - AXN - AXN Black PL - AXN PL - AXN Spin - AXN Spin PL - AXN White PL - Balkanika TV - Balkan trip - Bang Bang - BBC First - BBC World News - BBN Türk - Bloomberg Adria - Bloomberg HT - Bloomberg TV - BN 2 HD - BN music - Brainz TV - Brazzers TV (ex. Private Spice) - CANAL+ Kuchnia - Cartoonito CEE - Cartoon Network - CBS Reality - Cinemania TV - Cinemax 2 - Cinemax - CineStar Premiere 1 - CineStar Premiere 2 - CineStar TV2 - Club MTV International - CNBC Europe - CNN Europe - Comedy Central PL - Crime+Investigation PL - Crime & Investigation Channel - Croatian Music Channel - Cufo TV - Das Erste - Da Vinci Learning - Deluxe Music - Deutsche Welle English - DigitAlb Melody TV - Disco Polo Music PL - Discovery Animal Planet - Discovery Channel - Discovery Historia - Discovery Science - Disney Channel - Disney Junior - DIVA (ex. Universal) - DIZI - Dizi Smart Max - Dizi Smart Premium - DMAX TR - DM SAT - Dream Türk - ducktv SD - E! Entertainment - Ekotürk - Epic Drama (CEE) - Erox HD - Eroxxx HD - Eska Rock TV - Eska TV - Eska TV Extra - Eurochannel - Euro D - EuroNews - Euronews Albania - European League of Football Channel - Eurosport 2 - Eurosport - Euro Star - EWTN PL - Explorer Histori - Explorer Natyra - Explorer Shkence - Extreme Sports Channel - FACE TV - FashionBox HD - Fashion TV - Fast&FunBox HD - FightBox HD - Fight Klub HD - Filmax - FilmBox Arthouse - FilmBox Extra RS - FilmBox Family PL - FilmBox Stars RS - Film Cafe PL - Fokus TV - Folx TV - Food Network - France24 - France 24 French - FX Comedy PL - FX PL - Ginx Esports TV - Grand Televizija - Haber Global - Habitat TV - HappyTV - Hayat 2 - Hayat Folk Box - Hayat Music Box - Hayatovci - Hayat TV - HBO2 - HBO3 - HBO - HRT 1 - HRT 3 - HRT 4 - HUSTLER TV - IDJ World - JimJam - Jugoton TV - Junior TV - K::CN 1 Kopernikus - K::CN 2 Music - K::CN 3 Svet Plus - Kabel Eins - Kanal1 - Kanal 7 - Kanal 10 - KANAL 24 TR - Kanal D - Kanal D TR - Karadeniz TV - KiKA - Kino Polska - Kino Polska Muzyka - KitchenTV - Klan Macedonia - Klan TV HD - KLASIK - krone.tv - Living HD - Lov i ribolov - M1 Family - M1 Film MK - M1 Gold MK - Melodie TV - Mezzo - Mezzo Live HD - MinikaGO TR - Minimax - Minimini+ - Motowizja - MovieSmart Classic - MovieSmart Türk - MRT SAT - MTM Televizija - MTV 00s - MTV 80s - MTV 90s International - MTV Europe - MTV Hits International - MTV Live HD - MUSE - My Music - N1 RS - Nat Geo Wild - National Geographic - National Geographic Channel HD - News 24 - Nickelodeon Commercial - Nick Junior - Nicktoons - Nova S - Nova Sport Srbija - Novelas+ - Novelas Plus 1 - NOW - Nowa TV - Number One Türk - NUTA.TV HD - NUTA GOLD - OBN - oe24.TV - O Kanal - Ora News - Pickbox TV MK - Pikaboo - Pink Music 1 - Pink Serije - Pink Show - Planeta TV BG - Planete+ PL - Playboy TV - Polonia1 - POLO TV - Polsat 2 - Polsat Cafe - Polsat Comedy Central Extra - Polsat Doku - Polsat Film - Polsat Music - Polsat News 2 - Polsat News - Polsat Play - Polsat Rodzina - Polsat Seriale - Polsat Viasat Explore - Polsat Viasat History - Polsat Viasat Nature - Power TV PL - Private TV - PRO 7 Österreich - ProSieben - Prva FILES - Prva KICK - Prva LIFE - Prva MAX - Prva Plus - Prva Srpska TV - Prva World - Puls 4 - Radio Televizija BN - Radio Televizija Federacije BIH - RAI DUE - RAI Sport 1 - RAI Storia - RAI TRE - RAI UNO - Rai Yoyo - Red Carpet TV PL - RedLight HD - Report TV - RT Documentary - RTK 1 - RTK 2 - RTK 4 - RTL 2 - RTL - RTL DE - RTL Kockica - RTL Living - RTL Zwei - RTRS - RTS 1 - RTS 2 - RTS 3 - RTSH 1 - RTS SVET - RTV21 - RTV 21 Popullore - RTV Besa - RTV Novi Pazar - RTV Slon Tuzla - RTV Slovenija 1 - RTV Slovenija 2 - RTV Slovenija 3 - Russia Today - SAT.1 - Sat.1 Österreich - Scan TV - SciFi - Shenja TV - Show TV - SinemaTV 2 - SinemaTV 1001 - SinemaTV 1002 - SinemaTV - SinemaTV Aile 2 - SinemaTV Aile - SinemaTV Aksiyon 2 - SinemaTV Aksiyon - SinemaTV Komedi 2 - SinemaTV Komedi - SinemaTV Yerli 2 - SinemaTV Yerli - Sixx AT - SK Fight - SK Golf - Sky News - SOS Plus - Sport 1 DE - Sport Klub 1 Hrvatska - Sport Klub 2 Hrvatska - Sport Klub 3 - STAR Channel - STAR Crime - STAR Life - STAR Movies - Stars TV PL - Star TV TR - Stinet - Stingray CMusic - Stopklatka - Studio B - STV Folk - Sundance TV PL - Super Polsat - Super RTL - Superstar 2 - Superstar TV - Syri TV - Tanjug Tačno - TAY TV - TBN Polska HD - TELE 1 - Televizija 24 - Televizija Crne Gore MNE - Televizioni 7 - Telewizja 13 - Telma - Tera TV - teve2 - teve2 TR - TGRT Belgesel - TGRT EU - The Fishing and Hunting - The History Channel - Tip TV - Tivibu Spor - TLC - Top Channel - Top News - Toxic Folk - Toxic Rap - Toxic TV - Trace Urban - Travel Channel - Travelxp - Tring Bunga Bunga - Tring Desire - Tring Smile - TRT 1 - TRT 4K - TRT EBA TV İlkokul - TRT EBA TV Lise - TRT EBA TV Ortaokul - TRT Spor Yıldız - TRT Turk - TRT World - TTV - TV 4 - TV5 - TV5 Monde - TV 8.5 - TV 8 TR - TV100 TR - TV Duga + SAT - TV Dukagjini - TV Edo - TV Kiss Menada - TVM Ohrid - TVN 7 - TV Net - TVN Style - TVN Turbo - TVP 2 - TVP 3 - TVP ABC - TVP HD - TVP Historia - TVP Info - TVP Kobieta - TVP Kultura - TVP Rozrywka - TVP Seriale - TVR PL - TVS - TV Sarajevo - TV Silesia - TVT PL - TV Trwam - Uçankuş TV - Ülke TV - Valentino Music HD - Vavoom - Viasat Explore CEE - Viasat History - Viasat Kino Balkan - Viasat Nature CEE - Vikom - Vizion Plus - VOX - Welt - Wness TV - World Fashion Channel - wPolsce PL - Wydarzenia 24 - Xtreme TV - XXL - Yaban TV - ZDF - Zdrava televizija - Алсат М - Канал 5 - Канал 8 - МРТ 1 - МРТ 2 - МРТ 3 - МРТ 4 - МРТ 5 - Наша ТВ - Россиᴙ 24 - Сител - ТВ Сонце - 1-2-3.tv - 2X2 - 3SAT - 4Fun Dance - 4Fun Kids - 4FunTV - 13 Ulica - 360 TuneBox - A2TV TR - Active Family - Adult Channel 2 - Adult Channel - Adventure HD - a Haber - Ale Kino+ - ALFA TVP - Al Jazeera - AMC PL - a News - Animal Planet PL - Anixe HD Serie - Antena HD - A Para - ARD-alpha - ARTE DE - ARTE FR - ATV1 - ATV2 - AXN Black PL - AXN PL - AXN Spin PL - AXN White PL - BabyTV - BabyTV PL - Bayerischen Fernsehen Nord - BBC Brit PL - BBC Earth PL - BBC First PL - BBC Lifestyle PL - BBC News Channel - BBC World News - BBN Türk - Belsat TV - Beyaz TV - BFM TV - Bibel TV - Biznes24 - Bloomberg HT - Bloomberg TV - Blue Hustler - Body in Balance - Bollywood PL - Brazzers TV (ex. Private Spice) - BR Fernsehen Süd - CANAL+ 4K Ultra HD - CANAL+ Dokument - Canal+ Domo - Canal+ Extra 1 PL - Canal+ Extra 2 PL - Canal+ Extra 3 PL - Canal+ Extra 4 PL - Canal+ Extra 5 PL - Canal+ Extra 6 PL - CANAL+ Family PL - CANAL+ Film PL - Canal+ France - CANAL+ Kuchnia - CANAL+ Now PL - CANAL+ Premium PL - CANAL+ Seriale PL - Canal+ Series FR - CANAL+ Sport 2 - CANAL+ Sport 3 - CANAL+ Sport 4 - CANAL+ Sport 5 - CANAL+ Sport PL - Cartoonito CEE - Cartoonito UK - Cartoon Network PL - CBeebies PL - CBS Reality PL - CGTN Documentary - Ciné+ Club - Ciné+ Emotion - Ciné+ Famiz - Cinemax 2 PL - Cinemax PL - Club MTV International - CNBC Europe - CNews - CNN Europe - Comedy Central DE - Comedy Central PL - COSMO - Crime+Investigation PL - ČT 1 - ČT 2 - ČT 24 - ČT :D - ČT Sport - Das Bild TV - Das Erste - Das Health TV - Da Vinci Learning PL - Deluxe Music - Deutsche Welle English - Disco Polo Music PL - Discovery Animal Planet - Discovery Channel PL - Discovery Historia - Discovery Life PL - Discovery Science PL - Disney Channel PL - Disney Junior Polska - Disney XD PL - Dizi Smart Max - Dizi Smart Premium - DMAX DE - DMAX TR - Dorcel TV - Dorcel XXX - Dream Türk - DTX PL - ducktv HD - ducktv Plus - E! Entertainment - E-Sport HD - Ekotürk - Eleven Sports 1 PL - Eleven Sports 2 PL - Eleven Sports 3 PL - Eleven Sports 4 PL - Epic Drama (Poland) - Eska Rock TV - Eska TV - Eska TV Extra - Eurochannel - EuroNews - Euronews FR - European League of Football Channel - Eurosport 1 PL - Eurosport 2 PL - EWTN - EWTN PL - EXTASY TV - Extreme Sports PL - FashionBox HD - Fashion TV - Fast&FunBox HD - FightBox HD - Fight Klub HD - Filmax - FilmBox Action PL - FilmBox Arthouse - FilmBox Extra PL - FilmBox Family PL - FilmBox Premium PL - Film Cafe PL - Fokus TV - Folx TV - Food Network PL - France 2 - France 3 - France 4 - France 5 - France24 - France Info - FunBox UHD - FX Comedy PL - FX PL - Gametoon HD - Ginx Esports TV - Golf Channel PL - Haber Global - Habitat TV - HBO 2 PL - HBO 3 PL - HBO PL - HGTV PL - History 2 Polska - History Channel Polska - HR Fernsehen - HSE24 Extra - HSE - Hustler HD - HUSTLER TV - Insight TV - Investigation Discovery PL - Italia 2 - iTVN - iTVN Extra - JimJam PL - K-TV Katholisches Fernsehen - Kabel Eins - Kabel eins Doku - Kabel eins Österreich - Kanal 7 - KANAL 24 TR - Kanal D TR - Karadeniz TV - KiKA - Kino Polska - Kino Polska Muzyka - KinoTV PL - krone.tv - La Chaîne Info - LCP - Leo TV - Love Nature US - LUXE TV - MDR Sachsen-Anhalt - MDR Sachsen - MDR Thüringen - Melodie TV - Metro TV - Mezzo - Mezzo Live HD - MinikaGO TR - Minimini+ - Motorvision Plus International - Motowizja - MovieSmart Classic - MovieSmart Türk - MTV 00s - MTV 80s - MTV 90s International - MTV DE - MTV Europe - MTV Hits International - MTV Live HD - MTV PL - münchen.tv - Music Box Polska - MyZen TV - N24 Doku - National Geographic People PL - National Geographic PL - National Geographic Wild PL - NDR Niedersachsen - News 24 - Nick DE - Nickelodeon PL - Nickelodeon Ukraine Pluto - Nick Junior PL - Nick Music - Nicktoons PL - Niederbayern TV Deggendorf-Straubing - Novelas+ - Novelas Plus 1 - Novela TV - NOW - Nowa TV - NTV DE - Number One Türk - NUTA.TV HD - NUTA GOLD - oe24.TV - One - ORF2 - Paramount Network PL - Parole di Vita - Phoenix - Planete+ PL - Playboy TV - Polonia1 - POLO TV - Polsat 1 - Polsat 2 - Polsat - Polsat Cafe - Polsat Comedy Central Extra - Polsat Doku - Polsat Film - Polsat Games - Polsat Music - Polsat News 2 - Polsat News - Polsat News Polityka - Polsat Play - Polsat Rodzina - Polsat Seriale - Polsat Sport 1 - Polsat Sport 2 - Polsat Sport 3 - Polsat Sport Fight - Polsat Sport Premium 1 - Polsat Sport Premium 2 - Polsat Sport Premium 3 PPV - Polsat Sport Premium 4 PPV - Polsat Sport Premium 5 PPV - Polsat Sport Premium 6 PPV - Polsat Viasat Explore - Polsat Viasat History - Polsat Viasat Nature - Power TV PL - Private TV - PRO 7 Österreich - Proart - ProSieben - ProSieben MAXX - Puls 4 - QVC Deutschland - Radiofreccia - Radio Italia TV - RAI World Premium - RBB Fernsehen Berlin - RBB Fernsehen Brandenburg - Reality Kings - Red Carpet TV PL - RedLight HD - Romance TV PL - RT Documentary - RTL DE - RTL Nitro - RTL Zwei - Russia Today - SAT.1 - Sat.1 Gold - Sat.1 Österreich - SciFi - ServusTV - Show TV - SinemaTV 2 - SinemaTV 1001 - SinemaTV 1002 - SinemaTV - SinemaTV Aile 2 - SinemaTV Aile - SinemaTV Aksiyon 2 - SinemaTV Aksiyon - SinemaTV Komedi 2 - SinemaTV Komedi - SinemaTV Yerli 2 - SinemaTV Yerli - Sixx AT - Sky News - SkyShowtime 1 Nordic - SkyShowtime 2 Nordic - Sky Sport News DE - sonnenklar.TV - Sport 1 DE - Sportklub PL - SR Fernsehen - SRF Info - Stars TV PL - Star TV TR - Stingray Classica - Stingray CMusic - Stingray Djazz - Stingray iConcerts - Stopklatka - Sundance TV PL - Super Polsat - Super RTL - SWR Baden-Württemberg - tagesschau24 - TAY TV - TBN Polska HD - TeenNick - TELE 1 - Tele 5 DE - Tele 5 PL - teleTOON+ - Telewizja 13 - teve2 - teve2 TR - TGRT Belgesel - The Nautical Channel - Tivibu Spor - TLC PL - TOGGO plus - Top Kids PL - Toya TV - Trace Urban - Travel Channel PL - Travelxp - TRT 4K - TRT Arapça - TRT Diyanet - TRT EBA TV İlkokul - TRT EBA TV Lise - TRT EBA TV Ortaokul - TTV - TV 4 - TV5 - TV5Monde Europe - TV6 PL - TV 8.5 - TV 8 TR - TV100 TR - TVC PL - TVE Internacional - TVN 7 - TVN24 - TVN24 BiS - TVN - TV Net - TVN Fabuła - TVN Style - TVN Turbo - TVP 1 - TVP 2 - TVP 3 - TVP3 Białystok - TVP3 Bydgoszcz - TVP3 Gdańsk - TVP3 Katowice - TVP3 Kielce - TVP3 Kraków - TVP3 Łódź - TVP3 Lublin - TVP3 Olsztyn - TVP3 Opole - TVP3 Poznań - TVP3 Rzeszów - TVP3 Szczecin - TVP3 Warszawa - TVP3 Wrocław - TVP ABC 2 - TVP ABC - TVP Dokument - TVP HD - TVP Historia 2 - TVP Historia - TVP Info - TVP Kobieta - TVP Kultura 2 - TVP Kultura - TVP Nauka - TVP Polonia - TVP Regionalna - TVP Rozrywka - TVP Seriale - TVP Sport - TV Puls 2 - TV Puls PL - TVP Wilno - TVP World - TV Regionalna.pl - TV Republika - TVR PL - TVS - TV Silesia - TVT PL - TV Trwam - Uçankuş TV - VG TV - Viasat Explore CEE - Viasat True Crime - ViDoc TV - Vivid Red - Vivid TV - VOX - VOX Music TV PL - Warner TV FR - Warner TV IT - Warner TV PL - Water Planet - WDR Fernsehen Köln - Welt - Welt der Wunder - World Fashion Channel - wPolsce PL - WP TV - Wydarzenia 24 - Xtreme TV - XXL - Yaban TV - ZDFinfo - ZDFneo - Zoom TV PL - Еспресо TV - 2X2 - 3SAT - 4Fun Dance - 4Fun Kids - 4FunTV - 13 Ulica - 24 Kitchen - 24Kitchen PT - A2TV TR - Active Family - Adult Channel 2 - Adult Channel - Adventure HD - a Haber - Ale Kino+ - ALFA TVP - Al Jazeera - AMC Break PT - AMC Crime PT - AMC PL - AMC PT - Andalucía TV - a News - A Para - ARD-alpha - ARTE FR - ARTV - ATV1 - ATV2 - AXN Movies PT - AXN PL - AXN PT - AXN Spin PL - AXN White PL - AXN White PT - BabyTV - BabyTV ES - BBC News Channel - BBC World News - BBN Türk - Benfica TV - BFM TV - Biggs - Bloomberg HT - Bloomberg TV - Canal 11 PT - Canal 24H - Canal+ Domo - CANAL+ Kuchnia - Canal HISTÓRIA PT - Canal Hollywood PT - Canal J - Canal Panda PT - Canal Sur - Cartoonito PT - Cartoon Network PT - Casa e Cozinha - Caza y Pesca ES - CBS Reality - CCTV 4 Europe - CGTN - Cinemundo - CMTV - CNBC Europe - CNN Europe - CNN PT - Comedy Central DE - Comedy Central PL - Crime+Investigation PL - Deutsche Welle English - Disco Polo Music PL - Discovery Animal Planet - Discovery Channel PT - Discovery Historia - Discovery Science - Disney Channel PT - Disney Junior PT - Dizi Smart Max - Dizi Smart Premium - DMAX TR - Dorcel TV - Dorcel XXX - Dream Türk - E! Entertainment - Ekotürk - Eleven Sports 1 PT - Eleven Sports 2 PT - Eleven Sports 3 PT - Eleven Sports 4 PT - Eleven Sports 5 PT - Eleven Sports 6 PT - Erox HD - Eroxxx HD - Eska Rock TV - Eska TV - Eska TV Extra - Eurochannel - EuroNews - European League of Football Channel - Eurosport 1 PT - Eurosport 2 PT - EWTN PL - FashionBox HD - Fashion TV - Fast&FunBox HD - FightBox HD - Fight Klub HD - Filmax - FilmBox Arthouse - FilmBox Family PL - Film Cafe PL - Fokus TV - Folx TV - Food Network - Food Network UK - FOX NEWS - France24 - France 24 French - Fuel TV - FunBox UHD - FX Comedy PL - FX PL - Gametoon HD - Ginx Esports TV - Globo Portugal - Haber Global - Habitat TV - Hustler HD - HUSTLER TV - Insight TV - Investigation Discovery - iTVN - iTVN Extra - JimJam - KANAL 24 TR - Karadeniz TV - KiKA - Kino Polska - Kino Polska Muzyka - krone.tv - LUXE TV - M6 - MCM Top - Melodie TV - Metro TV - Mezzo - Mezzo Live HD - MinikaGO TR - Minimini+ - Motorvision Plus - Motorvision Plus International - Motowizja - MovieSmart Classic - MovieSmart Türk - MTV 00s - MTV ES - MTV Live HD - MTV PT - Museum TV - MyZen TV - Nat Geo Wild ES - National Geographic Portugal - Nickelodeon PT - Nickelodeon Ukraine Pluto - Nick Jr. PT - Nick Junior Commercial Light - Novelas+ - Novelas Plus 1 - Nowa TV - Number One Türk - NUTA.TV HD - NUTA GOLD - Odisseia - oe24.TV - ORF2 - Panda Kids - Phoenix - Planete+ PL - Playboy TV - Polonia1 - POLO TV - Polsat 1 - Polsat 2 - Polsat Cafe - Polsat Comedy Central Extra - Polsat Doku - Polsat Film - Polsat Games - Polsat Music - Polsat News 2 - Polsat News - Polsat Play - Polsat Rodzina - Polsat Seriale - Polsat Viasat Explore - Power TV PL - Private TV - PRO 7 Österreich - ProSieben - Puls 4 - RAI DUE - RAI UNO - RAI World Premium - Rai Yoyo - Red Carpet TV PL - RedLight HD - RT Documentary - RTL DE - RTP1 - RTP2 - RTP3 - RTP Açores - RTP Africa - RTP Internacional - RTP Madeira - RTP Memória - Russia Today - Russia Today ES - SAT.1 - Sat.1 Österreich - SIC - SIC Caras - SIC Internacional - SIC K - SIC Mulher - SIC Noticias - SIC Radical - SinemaTV 2 - SinemaTV 1001 - SinemaTV 1002 - SinemaTV - SinemaTV Aile 2 - SinemaTV Aile - SinemaTV Aksiyon 2 - SinemaTV Aksiyon - SinemaTV Komedi 2 - SinemaTV Komedi - SinemaTV Yerli 2 - SinemaTV Yerli - Sixx AT - Sky News - SkyShowtime 1 Nordic - Sport 1 DE - Sport TV1.pt - Sport TV2 - Sport TV3 - Sport TV4 - Sport TV5 - Sport TV6 - Sport TV7 - Sport TV+ - STAR Comedy PT - STAR Crime PT - STAR Life PT - STAR Movies PT - STAR Mundo PT - STAR PT - Stars TV PL - Star TV TR - Stingray CMusic - Stingray Djazz - Stingray iConcerts - Stopklatka - Sundance TV PL - Super Polsat - Super RTL - Syfy PT - TAY TV - TBN Polska HD - TELE 1 - TeleDeporte - Televisión de Galicia - Telewizja 13 - teve2 - TGRT Belgesel - The Nautical Channel - Tivibu Spor - TLC PAN - TPA7 - Trace Urban - Travel Channel - TRT 4K - TRT EBA TV İlkokul - TRT EBA TV Lise - TRT EBA TV Ortaokul - TTV - TV 4 - TV5 - TV5 Monde - TV5Monde Europe - TV 8.5 - TV100 TR - TV Cine Action - TV Cine Edition - TV Cine Emotion - TV Cine Top - TVE Internacional - TVG2 - TV Galicia - TVI - TVI Reality - TVN 7 - TVN24 - TVN - TV Net - TVN Style - TVN Turbo - TVP 2 - TVP 3 - TVP ABC - TVP HD - TVP Historia - TVP Info - TVP Kobieta - TVP Kultura - TVP Regionalna - TVP Rozrywka - TVP Seriale - TV Puls PL - TVR PL - TVS - TV Silesia - TVT PL - TV Trwam - Uçankuş TV - Vivid Red - Vivid TV - V Mais - VOX - Wydarzenia 24 - Xtreme TV - XXL - Yaban TV - ZDF - ZDFneo - 2X2 - 4Fun Dance - 4Fun Kids - 4FunTV - 13 Ulica - A2TV TR - Acasă - Acasă Gold - Active Family - Adult Channel - Adventure HD - AgroTV RO - a Haber - Ale Kino+ - ALFA TVP - Al Jazeera - Al Jazeera Arabic English - AMC PL - AMC RO - a News - Antena1 RO - Antena 3 CNN - Antena Stars RO - A Para - ARTE FR - ATV1 - ATV2 - ATV HU - ATV Spirit HU - ATV TR - Auto Motor und Sport - AXN Black PL - AXN Black RO - AXN PL - AXN RO - AXN Spin PL - AXN Spin RO - AXN White PL - AXN White RO - B1 TV RO - BabyTV - Balkanika TV - BBC Earth - BBC First - BBC World News - BBN Türk - Bloomberg HT - Bloomberg TV - Body in Balance - Bollywood Film RO - Brazzers TV (ex. Private Spice) - Bucuresti TV RO - Canal 33 RO - Canal+ Domo - CANAL+ Kuchnia - Cartoonito CEE - Cartoon Network RO - CBS Reality - CBS Reality PL - CCTV 4 Europe - CGTN - CGTN Documentary - Cinemaraton RO - Cinemax 2 RO - Cinemax RO - Cinethronix RO - Club MTV International - CNBC Europe - CNN Europe - Comedy Central Extra BG - Comedy Central HU - Comedy Central PL - Comedy Central RO - Credo TV - Crime+Investigation PL - Crime & Investigation Channel - Da Vinci Learning - Deutsche Welle English - Digi 24 - Digi Animal World RO - Digi Life RO - Digi World RO - Disco Polo Music PL - Discovery Animal Planet - Discovery Channel - Discovery Historia - Discovery Science - Disney Channel RO - Disney Junior - Diva Universal RO - DIZI - Dizi Smart Max - Dizi Smart Premium - DMAX TR - Dorcel TV - Dorcel XXX - Dream Türk - ducktv HD - Duna TV - Duna World - E! Entertainment - Ekotürk - Epic Drama (CEE) - Erox HD - Eroxxx HD - Eska Rock TV - Eska TV - Eska TV Extra - Etno TV RO - EuroNews - European League of Football Channel - Eurosport 1 INT - Eurosport 2 INT - Eurosport - EWTN PL - Extreme Sports Channel - Fashion TV - Fast&FunBox HD - Favorit TV RO - FightBox HD - Fight Klub HD - Film+ HU - Filmax - FilmBox Extra RO - FilmBox Family PL - FilmBox Family RO - FilmBox Plus RO - FilmBox Premium RO - FilmBox RO - FilmBox Stars BG - Filmbox Stars HU - Film Cafe PL - Film Cafe RO - Film Now RO - Focus TV - Fokus TV - Folx TV - Food Network - FX Comedy PL - FX PL - Galaxy4 - H!T Music Channel RO - Haber Global - Habitat TV - Happy Channel RO - HBO 2 RO - HBO 3 RO - HBO RO - Hír TV - History Channel RO - Home & Garden Television - HUSTLER TV - IDA RO - Inedit TV RO - Investigation Discovery - Izaura TV - JimJam - Kanal 7 - KANAL 24 TR - Kanal D RO - Kanal D TR - Karadeniz TV - Kino Polska - Kino Polska Muzyka - Kiss TV RO - krone.tv - Love Nature US - LUXE TV - M2 Petőfi - M4 sport - Magic TV BG - Magyar Sláger TV - Magyar Televízió 1 - Mediaset Italia - Medika TV RO - Melodie TV - Metro TV - Mezzo - Mezzo Live HD - MinikaGO TR - Minimax HU - Minimax RO - Minimini+ - Moldova TV - Mooz Dance - Mooz HD RO - Mooz Hits RO - Mooz Ro RO - Motorvision Plus - Motorvision Plus International - Motowizja - MovieSmart Classic - MovieSmart Türk - Mozi plusz TV - MTV 00s - MTV 80s - MTV 90s International - MTV Europe HU - MTV Hits International - MTV Live HD - MTV Music UK - Museum HD RO - Music Channel 1 RO - Muzsika TV - MyZen TV - Nasul TV RO - Nat Geo Wild RO - National 24 Plus RO - National Geographic People RO - National Geographic RO - National TV RO - Nickelodeon Commercial - Nick Junior PL - Nicktoons PL - Novelas+ - Novelas Plus 1 - NOW - Nowa TV - Number One Türk - NUTA.TV HD - NUTA GOLD - oe24.TV - ORF2 - Planete+ PL - Playboy TV - Polonia1 - POLO TV - Polsat 2 - Polsat Cafe - Polsat Comedy Central Extra - Polsat Doku - Polsat Film - Polsat Games - Polsat Music - Polsat News 2 - Polsat News - Polsat Play - Polsat Rodzina - Polsat Seriale - Polsat Sport 2 - Polsat Viasat Explore - Polsat Viasat History - Polsat Viasat Nature - Power TV PL - Prima Sport 1 RO - Prima Sport 2 RO - Prima Sport 3 RO - Prima Sport 4 RO - Prima Sport 5 RO - Prima TV RO - Prime - Private TV - PRO 7 Österreich - PRO ARENA RO - Pro Cinema RO - ProSieben - ProTV - PRO TV Internațional - Puls 4 - RAI TRE - RAI UNO - RAI World Premium - Rai Yoyo - Realitatea Plus RO - Reality Kings - Red Carpet TV PL - Romance TV PL - România TV - RTL DE - RTL Gold - RTL HU - RTL Ketto - RTLup - SAT.1 - Sat.1 Österreich - Show TV - SinemaTV 2 - SinemaTV 1001 - SinemaTV 1002 - SinemaTV - SinemaTV Aile 2 - SinemaTV Aile - SinemaTV Aksiyon 2 - SinemaTV Aksiyon - SinemaTV Komedi 2 - SinemaTV Komedi - SinemaTV Yerli 2 - SinemaTV Yerli - Sixx AT - Sky News - SkyShowtime 1 Nordic - Sorozat+ - Speranta TV - Spíler1 TV - Stars TV PL - Star TV TR - Stingray Classica - Stingray CMusic - Stingray Djazz - Stingray iConcerts - Stopklatka - Story4 - Sundance TV PL - Super Polsat - Super RTL - Super TV2 - Taraf Tv RO - TAY TV - TBN Polska HD - TeenNick - TELE 1 - Televiziunea Româna 1 - Televiziunea Româna 2 - Televiziunea Româna International - Telewizja 13 - teve2 - teve2 TR - TGRT Belgesel - The Fishing and Hunting RO - The Nautical Channel - Tivibu Spor - TLC - Trace Urban - Travel Channel - Travel Mix RO - Travelxp - Trinitas HD RO - TRT 4K - TRT EBA TV İlkokul - TRT EBA TV Lise - TRT EBA TV Ortaokul - TRT Turk - TRT World - TTV - TV2 Comedy - TV2 HU - TV2 Kids - TV2 Klub - TV2 Séf - TV 4 - TV4 HU - TV5 - TV5Monde Europe - TV 8.5 - TV 8 TR - TV100 TR - TV1000 Global Kino - TV1000 Russian Kino RO - TV Digi Sport 1 - TV Digi Sport 2 - TV Digi Sport 3 - TV Digi Sport 4 - TVE Internacional - TVN 7 - TVN24 - TVN24 BiS - TV Net - TVN Style - TVN Turbo - TVP 2 - TVP 3 - TVP ABC - TV Paprika HU - TV Paprika RO - TVP HD - TVP Historia - TVP Info - TVP Kobieta - TVP Kultura - TVP Regionalna - TVP Rozrywka - TVP Seriale - TV Puls PL - TVR3 RO - TVR Cluj RO - TVR Craiova RO - TVR Iasi RO - TVR PL - TVR Tg-Mures RO - TVR Timisoara RO - TVS - TV Silesia - TV SudEst - TVT PL - TV Trwam - Uçankuş TV - UTV RO - Viasat Explore CEE - Viasat History - Viasat Kino Balkan - Viasat Nature CEE - VTV RO - Warner TV IT - Warner TV RO - World Fashion Channel - wPolsce PL - Wydarzenia 24 - Xtreme TV - XXL - Yaban TV - Zenebutik - Zu TV - 2X2 - 3SAT - 4Fun Dance - 4Fun Kids - 4FunTV - 13 Ulica - 20 Mediaset - 24 Kitchen - 24Kitchen PT - A2TV TR - Active Family - Adventure HD - Agro TV - a Haber - Ale Kino+ - ALFA TV - ALFA TVP - Al Jazeera - Al Jazeera Balkans - Alternativna televizija Banja Luka - AMC PL - AMC SI - a News - Anixe HD Serie - A Para - Arena Esport - Arena Fight - Arena Sport 1 Premium SI - Arena Sport 1 SI - Arena Sport 2 SI - Arena Sport 3 SI - Arena Sport 4 SI - ARTE DE - ARTE FR - ASTRA TV - ATV1 - ATV2 - ATV TR - AXN - AXN Black PL - AXN PL - AXN Spin - AXN Spin PL - AXN White PL - B92 - BabyTV - Balkanika TV - Balkan trip - BBC Earth - BBC First - BBC World News - BBN Türk - BK TV - Bloomberg Adria - Bloomberg HT - Bloomberg TV - BN 2 HD - BN music - Brazzers TV (ex. Private Spice) - BRIO - Canal+ Domo - CANAL+ Kuchnia - Canale 5 - Cartoonito CEE - Cartoon Network - Cartoon Network DE - CBS Reality - CCTV 4 Europe - CGTN - Cinemax 2 - Cinemax - CineStar Action&Thriller - CineStar Premiere 1 - CineStar Premiere 2 - CineStar TV1 - CineStar TV2 - CineStar TV Comedy Family - CineStar TV Fantasy - Club MTV International - CNBC Europe - CNN Europe - Comedy Central DE - Comedy Central PL - Crime+Investigation PL - Crime & Investigation Channel - Croatian Music Channel - Das Erste - Da Vinci Learning - Disco Polo Music PL - Discovery Animal Planet - Discovery Channel - Discovery Historia - Discovery Science - Disney Channel - Disney Junior - DIVA (ex. Universal) - Dizi Smart Max - Dizi Smart Premium - DMAX TR - DM SAT - Dorcel TV - Dorcel XXX - Dream Türk - ducktv HD - ducktv SD - Duna TV - Duna World - E! Entertainment - Ekotürk - English Club TV - Epic Drama (CEE) - Erox HD - Eroxxx HD - Eska Rock TV - Eska TV - Eska TV Extra - ETV HD - Eurochannel - EuroNews - European League of Football Channel - Eurosport 2 - Eurosport - EWTN - EWTN PL - Exodus TV - Extreme Sports Channel - FashionBox HD - Fashion TV - Fast&FunBox HD - FightBox HD - Fight Klub HD - Filmax - FilmBox Arthouse - FilmBox Extra RS - FilmBox Family PL - FilmBox Stars RS - Film Cafe PL - Focus TV - Fokus TV - Folx TV - France 2 - France24 - France 24 French - FX Comedy PL - FX PL - GEA TV - Ginx Esports TV - Golica TV - Gorenjska televizija - Grand Televizija - Haber Global - Habitat TV - HappyTV - Hayat 2 - Hayat Folk Box - Hayat Music Box - Hayatovci - Hayat TV - HBO2 - HBO3 - HBO - HEMA TV - History Channel 2 - HIT TV - Home & Garden Television - HRT 1 - HRT 2 - HRT 3 - HRT 4 - HUSTLER TV - IDJ World - Investigation Discovery - Italia 1 - JimJam - Jugoton TV - K::CN 1 Kopernikus - K::CN 2 Music - K::CN 3 Svet Plus - Kabel Eins - KANAL 24 TR - Kanal A, SLO - Kanal D - Kanal Rijeka - Karadeniz TV - KiKA - Kino Polska - Kino Polska Muzyka - Klape i Tambure TV - KLASIK - krone.tv - KTV Ormož - Living HD - Lov i ribolov - LUXE TV - M2 Petőfi - Mediaset Italia - Melodie TV - Metro TV - Mezzo - Mezzo Live HD - MinikaGO TR - Minimax - Minimax SI - Minimini+ - Motorvision Plus - Motowizja - MovieSmart Classic - MovieSmart Türk - MrežaZG - MTV 00s - MTV 80s - MTV 90s International - MTV DE - MTV Europe - MTV Hits International - MTV Live HD - MyZen TV - N1 BA - N1 HR - N1 RS - Nat Geo Wild - Nat Geo Wild HD - Nat Geo Wild SI - National Geographic - National Geographic Channel HD - NBA TV - Net TV - Nickelodeon - Nickelodeon Commercial - Nick Junior - Nicktoons - Nova 24 TV - NOVA TV - Novelas+ - Novelas Plus 1 - NOW - Nowa TV - NTV DE - NTV IC Kakanj - Number One Türk - NUTA.TV HD - NUTA GOLD - OBN - oe24.TV - O Kanal - ORF1 - ORF2 - ORF3 - OTO - Otvorena televizija - OTV Valentino - Pickbox TV SI - Pikaboo 2 - PINK Family - Pink Fashion - PINK Film - Pink Folk 1 - Pink Folk 2 - Pink Kids - Pink Movies - Pink Music 1 - Pink Reality - Pink Serije - Pink Show - PINK World - PINK Zabava - Planet 2 - Planete+ PL - Planet TV SI - Playboy TV - Polonia1 - POLO TV - Polsat 2 - Polsat Cafe - Polsat Comedy Central Extra - Polsat Doku - Polsat Film - Polsat Games - Polsat Music - Polsat News 2 - Polsat News - Polsat Play - Polsat Rodzina - Polsat Seriale - Polsat Viasat Explore - Polsat Viasat History - Polsat Viasat Nature - POP KINO - POP TV - Power TV PL - Private TV - PRO 7 Österreich - ProSieben - Prva Srpska TV - Prva World - Ptujska televizija - Puls 4 - Radio Televizija BIH - Radio Televizija BN - Radio Televizija Federacije BIH - RAI 3 Bis - RAI 4 - RAI 5 - RAI DUE - RAI Education - RAI Gulp - RAI News 24 - RAI Sport 1 - RAI Storia - RAI TRE - RAI UNO - RAI World Premium - Rai Yoyo - Reality Kings - Red Carpet TV PL - RedLight HD - RED tv - Rete 4 - RTK 1 - RTL 2 - RTL - RTL DE - RTL Kockica - RTL Living - RTL Zwei - RTRS - RTS 1 - RTS 2 - RTS SVET - RTV International - RTV PINK - RTV Slovenija 1 - RTV Slovenija 2 - RTV Slovenija 3 - RTV Tuzlanskog Kantona - RTV Unsko-sanskog kantona - Russia Today - SAT.1 - Sat.1 Österreich - SciFi - ServusTV - Sexation TV - Show TV - SinemaTV 2 - SinemaTV 1001 - SinemaTV 1002 - SinemaTV - SinemaTV Aile 2 - SinemaTV Aile - SinemaTV Aksiyon 2 - SinemaTV Aksiyon - SinemaTV Komedi 2 - SinemaTV Komedi - SinemaTV Yerli 2 - SinemaTV Yerli - Sixx - Sixx AT - SK Fight - SK Golf - Sky News - SOS Plus - Sport 1 DE - Sportitalia - Sport Klub 1 Slovenija - Sport Klub 2 Slovenija - Sport Klub 3 - Sport Klub 4 - Sport Klub 5 - Sport Klub 6 - Sport TV1.pt - Sport TV2 - Šport TV 1 - Šport TV 2 - STAR Crime SI - STAR Life SI - STAR Movies SI - STAR SI - Stars TV PL - Star TV TR - Stingray Djazz - Stingray iConcerts - Stopklatka - ŠTV3 - Sundance TV PL - Super Polsat - Super RTL - Tanjug Tačno - TAY TV - TBN Polska HD - TELE 1 - Telequattro - Televizija Alfa - Televizija AS - Televizija Crne Gore 1 - Televizija Crne Gore 2 - Televizija Crne Gore MNE - Televizija skupnih internih programov - Telewizja 13 - Telma - teve2 - teve2 TR - TGRT Belgesel - The Fishing and Hunting - The History Channel - The Nautical Channel - Tivibu Spor - TLC - Top TV - Toxic TV - Trace Urban - Travel Channel - Travelxp - TRT 4K - TRT EBA TV İlkokul - TRT EBA TV Lise - TRT EBA TV Ortaokul - TRT World - TTV - TV3 - TV 3 Medias - TV 4 - TV5 - TV5 Monde - TV 8.5 - TV100 TR - TV Arena - TV ATM - TV Celje - TV Duga + SAT - TV Galeja - TV Idea - TV Jadran - TV Koper - TV Maribor - TVN 7 - TVN24 - TV Net - TVN Style - TVN Turbo - TVP 2 - TVP 3 - TVP ABC - TVP HD - TVP Historia - TVP Info - TV PINK EXTRA - TV PINK PLUS - TVP Kobieta - TVP Kultura - TVP Regionalna - TVP Rozrywka - TVP Seriale - TV Puls PL - TVR PL - TVS - TV Sarajevo - TV Silesia - TVT PL - TV Trwam - TV Veseljak - TV Vijesti - Uçankuş TV - Varaždinska Televizija - Vaš kanal - Vavoom - Viasat Explore CEE - Viasat History - Viasat Kino Balkan - Viasat Nature CEE - Viasat True Crime - Vikom - VOX - VTV Studio - Welt - wPolsce PL - Wydarzenia 24 - Xtreme TV - XXL - Yaban TV - Zagrebačka Televizija - ZDF - Zdrava televizija - Алсат М - БНТ 2 - Канал 5 - МРТ 1 - МРТ 2 - МРТ 3 - Россиᴙ 24 - Сител - 1-2-3.tv - 2X2 - 2X2 - 4Fun Dance - 4Fun Kids - 4FunTV - 4FunTV - 13 Ulica - 13 Ulica - 20 Mediaset - 21 Mix - 21 Mix - 24 Kitchen - 24Kitchen PT - 24Kitchen PT - 360 TuneBox - 360 TuneBox - A2 CNN - A2TV TR - ABC News Albania - ABC News Albania - Active Family - Active Family - Adventure HD - Agro TV - a Haber - a Haber - Ale Kino+ - Ale Kino+ - ALFA TVP - ALFA TVP - Al Jazeera - Al Jazeera - Al Jazeera Balkans - Al Jazeera Balkans - Alpha TV - AMC - AMC PL - AMC PL - a News - a News - A Para - A Para - Apollon TV - Apollon TV - Arena Sport 1 RS - Arena Sport 2 RS - Arena Sport 3 RS - Arena Sport 4 RS - Arena Sport 5 RS - Arena Sport 6 RS - ArtDoku 1 - ArtDoku 2 - ARTE DE - ARTE DE - ART Sport 1 - ART Sport 2 - ART Sport 3 - ART Sport 4 - ART Sport 5 - ART Sport 6 - ATV1 - ATV2 - ATV2 - ATV Avrupa - ATV KS - ATV TR - ATV TR - AXN - AXN Black PL - AXN PL - AXN PL - AXN Spin - AXN Spin PL - AXN Spin PL - AXN White PL - B92 - BabyTV - BabyTV - Balkanika TV - Balkanika TV - Bang Bang - Bang Bang - BBC Earth - BBC World News - BBC World News - BBN Türk - BBN Türk - Beyaz TV - Beyaz TV - Bloomberg HT - Bloomberg HT - BN music - BN music - Brazzers TV (ex. Private Spice) - Bubble TV - Bubble TV - CANAL+ Kuchnia - CANAL+ Kuchnia - Canale 5 - Cartoonito CEE - Cartoonito CEE - Cartoon Network - Cartoon Network - CBS Reality - Cinemax 2 - Cinemax - CineStar Action&Thriller RS - CineStar TV Comedy Family - CineStar TV Fantasy - CineStar TV RS - City TV - Click TV - Click TV - CNBC Europe - CNN Europe - CNN Europe - CNN Türk TV - Comedy Central DE - Comedy Central Extra UK - Comedy Central PL - Comedy Central PL - Crime+Investigation PL - Crime+Investigation PL - Crime & Investigation Channel - Cufo TV - Cufo TV - Das Erste - Da Vinci Learning - Deluxe Music - Deutsche Welle English - Deutsche Welle English - DigitAlb Melody TV - Disco Polo Music PL - Disco Polo Music PL - Discovery Animal Planet - Discovery Animal Planet - Discovery Channel - Discovery Channel - Discovery Historia - Discovery Historia - Discovery Science - Discovery Turbo UK - Disney Channel - Disney Channel - DIVA (ex. Universal) - DIZI - DIZI - Dizi Smart Max - Dizi Smart Max - Dizi Smart Premium - Dizi Smart Premium - DMAX DE - DMAX TR - DMAX TR - DM SAT - DM SAT - Dorcel TV - Dream Türk - Dream Türk - ducktv HD - ducktv SD - E! Entertainment - Ekotürk - Ekotürk - Elrodi - Elrodi - English Club TV - English Club TV - Epic Drama (CEE) - Erox HD - Eroxxx HD - Eroxxx HD - Eska Rock TV - Eska TV - Eska TV Extra - Eska TV Extra - Eurochannel - Eurochannel - Euro D - EuroNews - EuroNews - Euronews Albania - Euronews Albania - European League of Football Channel - Eurosport - Euro Star - EWTN PL - Explorer Histori - Explorer Histori - Explorer Natyra - Explorer Natyra - Explorer Shkence - Explorer Shkence - EXTASY TV - Extreme Sports Channel - FashionBox HD - FashionBox HD - Fashion TV - Fast&FunBox HD - Fast&FunBox HD - FAX News - FAX News - FightBox HD - FightBox HD - Fight Klub HD - Fight Klub HD - Filmax - Filmax - FilmBox Arthouse - FilmBox Arthouse - FilmBox Extra RS - FilmBox Extra RS - FilmBox Family PL - FilmBox Premium RS - FilmBox Stars RS - FilmBox Stars RS - Film Cafe PL - Film Cafe PL - Fokus TV - Fokus TV - Folx TV - Food Network - France 2 - France24 - France24 - France 24 French - FX Comedy PL - FX Comedy PL - FX PL - FX PL - Gametoon HD - Gametoon HD - Haber Global - Haber Global - Habertürk - Habitat TV - Habitat TV - HappyTV - Hayat 2 - Hayat Folk Box - Hayat TV - HBO2 - HBO3 - HBO - History Channel 2 - HRT 1 - HRT 3 - HSE24 Extra - HSE24 Trend - HUSTLER TV - Info24 Albania - INTV AL - Investigation Discovery - Investigation Discovery - Italia 1 - JimJam - Junior TV - Junior TV - Kabel Eins - Kabel eins Doku - Kanal 6 - Kanal 7 - Kanal 7 - Kanal 10 - Kanal 10 - KANAL 24 TR - KANAL 24 TR - Kanal D - Kanal D - Kanal D TR - Kanali 7 - Kanali 7 - Karadeniz TV - Karadeniz TV - KB Peja TV - KiKA - Kino Polska - Kino Polska - Kino Polska Muzyka - KitchenTV - Klan Kosova - Klan Kosova - Klan Macedonia - Klan Music - Klan Plus - Klan Plus - Klan TV HD - Klan TV HD - Kopliku TV - Kopliku TV - krone.tv - La7 - Living HD - Living HD - MCN 24 - MCN TV - Mediaset Italia - Melodie TV - MinikaGO TR - MinikaGO TR - Minimax - Minimini+ - Minimini+ - Motorvision Plus International - Motowizja - MovieSmart Classic - MovieSmart Classic - MovieSmart Türk - MovieSmart Türk - MTV 00s - MTV 80s - MTV 90s International - MTV Europe - MTV Hits International - MTV Live HD - MTV Live HD - MUSE - MUSE - My Music - My Music - N24 Doku - Nat Geo Wild - Nat Geo Wild - National Geographic - National Geographic - National Geographic Channel HD - NBA TV - Ndihma e Klientit - News 24 - News 24 - Nickelodeon - Nickelodeon - Nick Junior - Novelas+ - Novelas+ - Novelas Plus 1 - Novelas Plus 1 - NOW - Nowa TV - Nowa TV - NTV DE - NTV DE - Number One Türk - Number One Türk - NUTA.TV HD - NUTA.TV HD - NUTA GOLD - NUTA GOLD - OBN - oe24.TV - One - Ora News - Ora News - ORF2 - ORF2 - Pikaboo - Pink Action - Pink and Roll - Pink Comedy - Pink Crime & Mystery - Pink Erotic 1 - Pink Erotic 2 - PINK Family - PINK Film - Pink Hits - Pink Kids - Pink LOL - Pink Music 1 - Pink Premium - Pink Reality - Pink Romance - Pink SCI FI & Fantasy - Pink Serije - Pink Super Kids - Pink Thriller - Pink Western - Pink World Cinema - Planete+ PL - Planete+ PL - Playboy TV - Polonia1 - Polonia1 - POLO TV - Polsat 2 - Polsat 2 - Polsat Cafe - Polsat Cafe - Polsat Comedy Central Extra - Polsat Comedy Central Extra - Polsat Doku - Polsat Doku - Polsat Film - Polsat Music - Polsat News 2 - Polsat News - Polsat Play - Polsat Rodzina - Polsat Seriale - Polsat Viasat Explore - Polsat Viasat Explore - Power Türk TV - Power Türk TV - Power TV - Power TV PL - Power TV PL - Premium Channel - Private TV - PRO 7 Österreich - PRO 7 Österreich - ProSieben - ProSieben MAXX - Prva FILES - Prva KICK - Prva LIFE - Prva MAX - Prva Srpska TV - Prva World - QVC 2 DE - QVC Deutschland - Radio Televizija BN - Radio Televizija Federacije BIH - RAI 4 - RAI 5 - RAI DUE - RAI DUE - RAI Gulp - RAI News 24 - RAI News 24 - RAI Storia - RAI TRE - RAI TRE - RAI UNO - Rai Yoyo - real time - Red Carpet TV PL - Red Carpet TV PL - RED tv - Report TV - Report TV - Rete 4 - RT Documentary - RTK 1 - RTK 1 - RTK 2 - RTK 2 - RTK 4 - RTL 2 - RTL - RTL DE - RTL Nitro - RTLup - RTL Zwei - RTRS - RTRS PLUS - RTS 1 - RTS 2 - RTS 2 - RTS 3 - RTS Drama - RTSH 1 - RTSH 2 - RTSH 3 - RTSH 24 - RTSH Agro - RTSH Femijë - RTSH Film - RTSH Gjirokastra - RTSH Korça - RTSH Kuvend - RTSH Plus - RTSH Shkollë - RTSH Shqip - RTSH Sport - RTS Kolo - RTS Muzika - RTS Poletarac - RTS SVET - RTS Trezor - RTS Život - RTV21 - RTV 21 Popullore - RTV 21 Popullore - RTV Besa - RTV Besa - RTV Most - RT Vojvodina 1 - RT Vojvodina 2 - RTV Ora - RTV PINK - Russia Today - Russia Today - SAT.1 - Sat.1 Gold - Sat.1 Österreich - Sat.1 Österreich - Scan TV - SciFi - Shenja TV - Shenja TV - Show Türk - Show TV - Show TV - SinemaTV 2 - SinemaTV 2 - SinemaTV 1001 - SinemaTV 1001 - SinemaTV 1002 - SinemaTV 1002 - SinemaTV - SinemaTV - SinemaTV Aile 2 - SinemaTV Aile 2 - SinemaTV Aile - SinemaTV Aile - SinemaTV Aksiyon 2 - SinemaTV Aksiyon 2 - SinemaTV Aksiyon - SinemaTV Aksiyon - SinemaTV Komedi 2 - SinemaTV Komedi 2 - SinemaTV Komedi - SinemaTV Komedi - SinemaTV Yerli 2 - SinemaTV Yerli 2 - SinemaTV Yerli - SinemaTV Yerli - Sixx - Sixx AT - Sixx AT - Sky News - SOS Plus - STAR Channel - STAR Channel - STAR Crime - STAR Crime - STAR Life - STAR Life - Stars TV PL - Star TV TR - Stinet - Stingray iConcerts - Stopklatka - Stopklatka - Studio B - STV Folk - Sundance TV PL - Sundance TV PL - Super Polsat - Super RTL - Super RTL - Superstar TV - Syri TV - Syri TV - Syri Vizion - Syri Vizion - TAY TV - TAY TV - TBN Polska HD - TELE 1 - TELE 1 - Tele 5 DE - Telewizja 13 - Telewizja 13 - teve2 - teve2 - teve2 TR - TGCOM24 - TGRT Belgesel - TGRT Belgesel - TGRT EU - The History Channel - Tip TV - Tip TV - Tivibu Spor - Tivibu Spor - TLC - TLC - Top Channel - Top Channel - Top News - Travel Channel - Travel Channel - Tring Bizarre - Tring Bizarre - Tring Bunga Bunga - Tring Bunga Bunga - Tring Desire - Tring Desire - Tring Kanal 7 - Tring Smile - Tring Smile - Tring Sport 1 - Tring Sport 2 - Tring Sport 3 - Tring Sport 4 - Tring Sport News - Tring Sport News - TRT 1 - TRT 2 - TRT 4K - TRT 4K - TRT Belgesel - TRT Çocuk - TRT EBA TV İlkokul - TRT EBA TV İlkokul - TRT EBA TV Lise - TRT EBA TV Lise - TRT EBA TV Ortaokul - TRT EBA TV Ortaokul - TRT Haber - TRT Müzik - TRT Spor - TRT Spor Yıldız - TRT Turk - TRT World - TTV - TV5 - TV5 - TV5 Monde - TV 7 - TV 7 - TV 8.5 - TV 8.5 - TV 8 TR - TV100 TR - TV100 TR - TV Dukagjini - TV Dukagjini - TVN 7 - TVN 7 - TV Net - TV Net - TVN Style - TVN Style - TVN Turbo - TVN Turbo - TVP 2 - TVP 3 - TVP ABC - TVP ABC - TVP HD - TVP HD - TVP Historia - TVP Historia - TVP Info - TVP Kobieta - TVP Kobieta - TVP Kultura - TVP Kultura - TVP Rozrywka - TVP Rozrywka - TVP Seriale - TVP Seriale - TVR PL - TVR PL - TVS - TV Silesia - TVT PL - TV Trwam - TV Trwam - Uçankuş TV - Uçankuş TV - Ülke TV - Vavoom - Vesti - Viasat Explore CEE - Viasat History - Viasat Kino Balkan - Viasat Nature CEE - Vizion Plus - Vizion Plus - VOX - VOX - VOX up - Welt - World Fashion Channel - Wydarzenia 24 - Xtreme TV - Xtreme TV - XXL - Yaban TV - Yaban TV - ZDF - ZDFneo - Zico TV - Zico TV - Zjarr TV - Zjarr TV - ΕΡΤ1 - Алсат М - Телевизија Храм - ФЕН ТВ - 2X2 - 3SAT - 4Fun Dance - 4Fun Kids - 4FunTV - 13 Ulica - 24 Kitchen - 24Kitchen PT - 360 TuneBox - A2TV TR - Active Family - Adult Channel 2 - Adventure HD - Agro TV - a Haber - Ale Kino+ - ALFA TV - ALFA TVP - Al Jazeera - Al Jazeera Balkans - AMC - AMC HU - AMC PL - a News - ANIXE plus - A Para - Arena Esport - Arena Fight - Arena Sport 1 Premium - Arena Sport 1 RS - Arena Sport 1x2 - Arena Sport 2 Premium - Arena Sport 2 RS - Arena Sport 3 Premium - Arena Sport 3 RS - Arena Sport 4 RS - Arena Sport 5 RS - Arena Sport 6 RS - Arena Sport 7 RS - Arena Sport 8 RS - Arena Sport 9 RS - Arena Sport 10 RS - ATV1 - ATV2 - Aurora TV - AXN - AXN Black PL - AXN PL - AXN Spin - AXN Spin PL - AXN White PL - B1 TV - B92 - BabyTV - Balkanika TV - Balkan trip - Balkan TV - BBC Earth - BBC News Channel - BBC World News - BBN Türk - Bit TV - BK TV - BlicTV - Bloomberg Adria - Bloomberg HT - Bloomberg TV - BN 2 HD - BN music - Brainz TV - Bravo Music - Brazzers TV (ex. Private Spice) - CANAL+ Kuchnia - Cartoonito CEE - Cartoon Network - CBS Reality - CCTV 4 Europe - CGTN - CGTN Documentary - Cinemania TV - Cinemax 2 - Cinemax 2 HU - Cinemax - Cinemax HU - CineStar Action&Thriller RS - CineStar Premiere 1 - CineStar Premiere 2 - CineStar TV2 - CineStar TV Comedy Family - CineStar TV Fantasy - CineStar TV RS - City Play - City TV - Class TV Moda - Club MTV International - CNBC Europe - CNN Europe - Comedy Central HU - Comedy Central PL - Comedy Central UK - CoolTV - Crime+Investigation PL - Crime & Investigation Channel - Croatian Music Channel - Das Erste - Da Vinci Learning - Deluxe Music - Deutsche Welle English - Dexy TV - Digi 24 - Disco Polo Music PL - Discovery Animal Planet - Discovery Channel - Discovery Channel HU - Discovery Historia - Discovery Science - Disney Channel - Disney Channel DE - Disney Channel HU - Disney Junior - DIVA (ex. Universal) - DIZI - Dizi Smart Max - Dizi Smart Premium - DMAX DE - DMAX TR - DM SAT - Dorcel TV - Dorcel XXX - DOX TV - Dream Türk - ducktv HD - ducktv SD - Duna TV - Duna World - E! Entertainment - Ekotürk - Epic Drama (CEE) - Erox HD - Eroxxx HD - Eska Rock TV - Eska TV Extra - Etno TV RO - Eurochannel - Euro Cinema 1 - Euro Cinema 2 - Euro Cinema 3 - Euro Cinema 4 - EuroNews - Euronews FR - EuroNews Srbija - European League of Football Channel - Eurosport 1 DE - Eurosport 2 - Eurosport - EWTN - EWTN PL - Extreme Sports Channel - FACE TV - FashionBox HD - Fashion TV - Fast&FunBox HD - Favorit TV RO - FightBox HD - Fight Klub HD - Film4 HU - Filmax - FilmBox Arthouse - Filmbox Extra HU - FilmBox Extra RS - FilmBox Family PL - Filmbox Premium HU - FilmBox Premium RS - Filmbox Stars HU - FilmBox Stars RS - Film Cafe PL - Fokus TV - Folklorika SK - Folx TV - Food Network - FOX NEWS - France24 - France 24 French - FX Comedy PL - FX PL - Gametoon HD - Golica TV - Grand Televizija - Haber Global - Habitat TV - HappyTV - Hayat 2 - Hayat Folk Box - Hayat Music Box - Hayat TV - HBO2 - HBO 2 HU - HBO3 - HBO 3 HU - HBO - HBO HU - HEMA TV - Hír TV - History Channel 2 - Home & Garden Television - HRT 1 - HRT 2 - HRT 3 - HRT 4 - HSE - HUSTLER TV - Hype TV - ICT Business - IDJ World - Info24 Albania - InformerTV - Insajder TV - Investigation Discovery - Izaura TV - JimJam - K1 TV - K::CN 1 Kopernikus - K::CN 2 Music - K::CN 3 Svet Plus - Kabel Eins - Kabel eins Doku - Kanal 9 TV - KANAL 24 TR - Karadeniz TV - Kazbuka - KB Peja TV - KiKA - Kino Polska - Kino Polska Muzyka - KitchenTV - KLASIK - KLASIK HR - krone.tv - Kurir TV - Lala TV - LifeTV SK - Love Nature US - Lov i ribolov - M2 Petőfi - M4 sport - M4 Sport Plus - m5 - Magyar Televízió 1 - Mediaset Italia - Melodie TV - Mezzo - MinikaGO TR - Minimax - Minimax HU - Minimini+ - Moja Happy Muzika - Moja Happy Zemlja - Moje Happy Društvo - Moj Happy Život - Motowizja - MovieSmart Classic - MovieSmart Türk - MTV 00s - MTV 80s - MTV 90s International - MTV Europe - MTV Hits International - MTV Live HD - Muzsika TV - N1 HR - N1 RS - N24 Doku - Narodna TV - Nat Geo Wild - Nat Geo Wild HD - National Geographic - National Geographic Channel HD - National Geographic HU - National Geographic RS - NBA TV - Newsmax Balkans - Nickelodeon Commercial - Nick Junior - Nick Junior PL - Nick Music - Nicktoons - Nicktoons PL - Nova Max - Nova S - Nova Series - Nova Sport Srbija - NOVA TV - Novelas+ - Novelas Plus 1 - Novosadska TV - NOW - Nowa TV - Now Rock - NTV 101 Sanski most - Number One Türk - NUTA.TV HD - NUTA GOLD - OBN - oe24.TV - O Kanal - Ozone Network - Pickbox TV RS - Pikaboo - Pink Action - Pink and Roll - Pink BH - Pink Classic - Pink Comedy - Pink Crime & Mystery - Pink Erotic 1 - Pink Erotic 2 - Pink Erotic 3 - Pink Erotic 4 - Pink Erotic 5 - Pink Erotic 6 - Pink Erotic 7 - Pink Erotic 8 - PINK Family - Pink Fashion - Pink Fight Network - PINK Film - Pink Folk 1 - Pink Folk 2 - Pink Ha Ha - Pink Hits 2 - Pink Hits - Pink Horror - Pink Kids - Pink Koncert - Pink Kuvar - Pink LOL - Pink M - Pink Movies - Pink Music 1 - Pink Pedia - Pink Premium - Pink Reality - Pink Romance - Pink SCI FI & Fantasy - Pink Serije - Pink Show - Pink Soap - Pink Style - Pink Super Kids - Pink Thriller - Pink Timeout - Pink Western - PINK World - Pink World Cinema - PINK Zabava - Planete+ PL - Planet TV SI - Playboy TV - Polonia1 - POLO TV - Polsat 2 - Polsat Cafe - Polsat Comedy Central Extra - Polsat Doku - Polsat Film - Polsat Music - Polsat News 2 - Polsat News - Polsat Play - Polsat Rodzina - Polsat Seriale - Polsat Viasat Explore - Polsat Viasat History - Polsat Viasat Nature - Power TV PL - Private TV - PRO 7 Österreich - ProSieben - ProSieben MAXX - Prva FILES - Prva KICK - Prva LIFE - Prva MAX - Prva Plus - Prva Srpska TV - Prva TV Crna Gora - Prva World - Puls 4 - QVC Deutschland - Radio Televizija BN - Radio Televizija Federacije BIH - RAI DUE - RAI Education - RAI News 24 - RAI Storia - RAI TRE - RAI UNO - RAI World Premium - Rai Yoyo - Reality Kings - Red Carpet TV PL - RedLight HD - RED tv - România TV - RT Documentary - RTL - RTL Croatia World - RTL Gold - RTL Három - RTL Nitro - RTLup - RTRS - RTRS PLUS - RTS 1 - RTS 2 - RTS 3 - RTS Drama - RTSH 3 - RTSH 24 - RTSH Plus - RTSH Shkollë - RTS Klasika - RTS Kolo - RTS Muzika - RTS Nauka - RTS Poletarac - RTS SVET - RTS Trezor - RTS Život - RTV HIT Brčko - RTV Most - RTV Novi Pazar - RT Vojvodina 1 - RT Vojvodina 2 - RTV Pančevo - RTV PINK - RTVS Dvojka - RTVS Jednotka - RTV Slovenija 1 - RTV Slovenija 2 - RTV Slovenija 3 - Russia Today - SAT.1 - Sat.1 Gold - Sat.1 Österreich - SAT TV - SciFi - Show TV - SinemaTV 2 - SinemaTV 1001 - SinemaTV 1002 - SinemaTV - SinemaTV Aile 2 - SinemaTV Aile - SinemaTV Aksiyon 2 - SinemaTV Aksiyon - SinemaTV Komedi 2 - SinemaTV Komedi - SinemaTV Yerli 2 - SinemaTV Yerli - Sixx - Sixx AT - SK Fight - SK Golf - Sky News - Sorozat+ - SOS Plus - Spektrum Home HU - Sport 1 DE - Sremska TV - STAR Channel - STAR Crime - STAR Life - STAR Movies - Stars TV PL - Star TV TR - Stingray Classica - Stingray iConcerts - Stopklatka - Studio B - Sundance TV PL - Super Polsat - Super RTL - SuperSat TV - Superstar 2 - Superstar 3 - Superstar TV - Super TV2 - TA3 SK - Tanjug Tačno - TAY TV - TBN Polska HD - TELE 1 - Tele 5 DE - Televizija 5 - Televizija 24 - Televizija Alfa - Televizija Crne Gore MNE - Televizija Doktor - Televiziunea Româna International - Telewizja 13 - teve2 - teve2 TR - TGRT Belgesel - The History Channel - Tivibu Spor - TLC - TLC DE - TOGGO plus - Top TV - Toxic Folk - Toxic Rap - Toxic TV - Trace Urban - Travel Channel - Travelxp - TRT 4K - TRT EBA TV İlkokul - TRT EBA TV Lise - TRT EBA TV Ortaokul - TTV - TV2 Klub - TV2 Séf - TV 4 - TV4 HU - TV5 - TV5 Monde - TV5Monde Europe - TV 8.5 - TV 8 TR - TV100 TR - TV Belle Amie - TV Duga + SAT - TV Galaksija - TV K23 - TV Markíza SK - TVN 7 - TV Net - TVN Style - TVN Turbo - TVP 2 - TVP 3 - TVP ABC - TV Paprika HU - TVP HD - TVP Historia - TVP Info - TV PINK EXTRA - TV PINK PLUS - TVP Kobieta - TVP Kultura - TVP Rozrywka - TVP Seriale - TV Ras - TVR Cluj RO - TVR Craiova RO - TVR Iasi RO - TVR PL - TVR Tg-Mures RO - TVR Timisoara RO - TV Silesia - TVT PL - TV Trwam - TV Vijesti - Uçankuş TV - UNA TV - Vavoom - Vesti - Viasat Explore CEE - Viasat History - Viasat Kino Balkan - Viasat Nature CEE - Viasat True Crime - VOX - VOX up - wPolsce PL - Wydarzenia 24 - Xtreme TV - XXL - Yaban TV - Zagrebačka Televizija - ZDF - ZDFinfo - ZDFneo - Первый - Россиᴙ 24 - Телевизија Храм - 2X2 - 4Fun Dance - 4Fun Kids - 4FunTV - 13 Ulica - 24 Kitchen - 360 TV - A2TV TR - Active Family - Adult Channel 2 - Adult Channel - Adventure HD - a Haber - ALFA TVP - Al Jazeera - Al Jazeera Arabic English - AMC PL - a News - A Para - ATV2 - ATV TR - AXN PL - AXN Spin PL - BabyTV TR - BBC First - BBC World News - BBN Türk - beIN Box Office 1 TR - beIN Box Office 2 TR - beIN Box Office 3 TR - beIN GURME - beIN HOME & ENTERTAINMENT - beIN iZ - beIN Movies Premiere 2 - beIN Movies Premiere TR - beIN Movies Stars - beIN Movies Turk - beIN Series 1 TR - beIN Series 2 TR - beIN Series 3 TR - beIN Series 4 TR - beIN Sports 1 TR - beIN Sports 2 TR - beIN Sports 3 TR - beIN Sports 4 TR - beIN Sports 5 TR - beIN Sports Haber - beIN Sports MAX 1 TR - beIN Sports MAX 2 TR - beIN TR - Beyaz TV - Bloomberg HT - Bloomberg TV - Bloomberg TV MENA - Body in Balance - CANAL+ Kuchnia - Cartoonito CEE - Cartoonito TR - Cartoon Network TR - Cbeebies - CGTN - CGTN Documentary - CNBC Europe - CNN Europe - CNN Türk TV - Comedy Central PL - Crime+Investigation PL - Da Vinci Learning - Da Vinci Learning TR - Deutsche Welle English - Disco Polo Music PL - Discovery Animal Planet - Discovery Channel - Discovery Historia - Discovery Science - Disney Junior - Dizi Smart Max - Dizi Smart Premium - DMAX TR - Dream Türk - Ekotürk - Epic Drama (CEE) - Erox HD - Eroxxx HD - Eska Rock TV - Eska TV - Eska TV Extra - Euro D - EuroNews - European League of Football Channel - Eurosport 2 - Euro Star - EWTN PL - Fashion TV - Fast&FunBox HD - FENERBAHÇE TV - Fight Klub HD - Filmax - FilmBox Family PL - Film Cafe PL - Fokus TV - Folx TV - France24 - France 24 French - FX Comedy PL - FX PL - FX Turkey - Haber Global - Habertürk - Habitat TV - Hustler HD - HUSTLER TV - Insight TV - Kanal 7 - KANAL 24 TR - Kanal B - Kanal D - Kanal D TR - Karadeniz TV - Kino Polska - Kino Polska Muzyka - Kral Pop TV - krone.tv - Love Nature US - LUXE TV - MCM FR - MCM Top - Melodie TV - Mezzo - Mezzo Live HD - Minika Çocuk - MinikaGO TR - Minimini+ - Motorvision Plus International - Motowizja - MovieSmart Classic - MovieSmart Türk - MTV 00s - MTV Hits International - MTV Live HD - National Geographic Turkey - National Geographic Wild TR - NBA TV - Nick Junior - Nicktoons - Novelas+ - Novelas Plus 1 - NOW - Nowa TV - NTV TR - Number One Türk - NUTA.TV HD - NUTA GOLD - oe24.TV - Planete+ PL - Playboy TV - Polonia1 - POLO TV - Polsat 2 - Polsat Cafe - Polsat Comedy Central Extra - Polsat Doku - Polsat Film - Polsat Music - Polsat News 2 - Polsat News - Polsat Play - Polsat Rodzina - Polsat Seriale - Polsat Viasat Explore - Polsat Viasat History - Power Türk TV - Power TV - Power TV PL - Private TV - PRO 7 Österreich - Rai Yoyo - Red Carpet TV PL - RedLight HD - Sat.1 Österreich - Show Türk - Show TV - SinemaTV 2 - SinemaTV 1001 - SinemaTV 1002 - SinemaTV - SinemaTV Aile 2 - SinemaTV Aile - SinemaTV Aksiyon 2 - SinemaTV Aksiyon - SinemaTV Komedi 2 - SinemaTV Komedi - SinemaTV Yerli 2 - SinemaTV Yerli - Sixx AT - Spor Smart 2 - Spor Smart - Sports TV TR - S Sport 2 - S Sport - Stars TV PL - Star TV TR - Stopklatka - Sundance TV PL - Super Polsat - TAY TV - TBN Polska HD - TELE 1 - Telewizja 13 - teve2 - teve2 TR - TGRT Belgesel - TGRT EU - TGRT HABER - The History Channel - Tivibu Spor 1 - Tivibu Spor 2 - Tivibu Spor 3 - Tivibu Spor 4 - Tivibu Spor 5 - Tivibu Spor - TLC - TLC TR - Trace Urban - TRT 1 - TRT 2 - TRT 4K - TRT Arapça - TRT Avaz - TRT Belgesel - TRT Çocuk - TRT Diyanet - TRT EBA TV İlkokul - TRT EBA TV Lise - TRT EBA TV Ortaokul - TRT Haber - TRT Kurdî - TRT Müzik - TRT Spor - TRT Spor Yıldız - TRT Turk - TRT World - TTV - TV 4 - TV5 - TV 8.5 - TV 8 TR - TV100 TR - TVE Internacional - TVN 7 - TV Net - TVN Style - TVN Turbo - TVP 2 - TVP 3 - TVP ABC - TVP HD - TVP Historia - TVP Info - TVP Kobieta - TVP Kultura - TVP Rozrywka - TVP Seriale - TVR PL - TVS - TV Silesia - TVT PL - Uçankuş TV - Ülke TV - Viasat Explore CEE - Viasat History - Viasat Nature CEE - Vivid Red - Vivid TV - wPolsce PL - Wydarzenia 24 - Xtreme TV - Yaban TV - + + + 2X2 + 4Fun Dance + 4Fun Kids + 4FunTV + 7/8 TV + 13 Ulica + 24 Kitchen + 24Kitchen BG + 360 TuneBox + A2TV TR + Active Family + Adventure HD + AGRO TV BG + a Haber + Ale Kino+ + Alfa BG + ALFA TVP + Al Jazeera + Al Jazeera Balkans + AMC + AMC PL + a News + A Para + ARTE FR + ATV1 + ATV2 + Auto Motor und Sport + AXN BG + AXN Black BG + AXN Black PL + AXN PL + AXN Spin PL + AXN White BG + AXN White PL + BabyTV + Balkanika TV + Barely Legal TV + BBC News Channel + BBC World News + BBN Türk + BG-DNES + BG Music Channel + BHTV + Bloomberg HT + Bloomberg TV + Bloomberg TV BG + BN music + Body in Balance + BOX TV BG + Brazzers TV (ex. Private Spice) + BSTV BG + bTV Action + bTV BG + bTV Cinema + bTV Comedy + bTV Story + Bulgaria on Air + Canal+ Domo + CANAL+ Kuchnia + Cartoonito CEE + Cartoonito UK + Cartoon Network + CBS Reality + Cinemania BG + Cinemax 2 BG + Cinemax BG + City TV BG + Club MTV International + CNBC Europe + CNN Europe + Comedy Central BG + Comedy Central Extra BG + Comedy Central PL + Crime+Investigation PL + Crime & Investigation Channel + Das Erste + Da Vinci Learning + Deluxe Music + Deutsche Welle English + Diema + Diema Family + Diema Sport 2 + Diema Sport 3 BG + Diema Sport + Disco Polo Music PL + Discovery Animal Planet + Discovery Channel + Discovery Historia + Discovery Science + Disney Channel BG + Disney Junior BG + Dizi Smart Max + Dizi Smart Premium + DMAX TR + DM SAT + Dorcel TV + Dorcel XXX + Dream Türk + DSTV + ducktv HD + ducktv SD + EKids + Ekotürk + Epic Drama (CEE) + Erox HD + Eroxxx HD + Eska Rock TV + Eska TV + Eska TV Extra + Eurochannel + EuroNews + EuroNews Bulgaria + Euronews FR + European League of Football Channel + Eurosport 1 BG + Eurosport 2 + EWTN PL + EXTASY TV + Extreme Sports Channel + FashionBox HD + Fashion TV + Fast&FunBox HD + FightBox HD + Fight Klub BG + Fight Klub HD + Filmax + FilmBox Arthouse + FilmBox BG + FilmBox Extra BG + FilmBox Family PL + FilmBox Stars BG + Film Cafe PL + Fix & Foxi + Fokus TV + Folx TV + France 2 + France 3 + France24 + France 24 French + Fuel TV + FunBox UHD + FX Comedy PL + FX PL + Gametoon HD + Haber Global + Habitat TV + HBO 2 BG + HBO 3 BG + HBO BG + History Channel 2 + Home & Garden Television + HUSTLER TV + Insight TV + Investigation Discovery + JimJam BG + Jukebox + K::CN 2 Music + Kanal 7 + KANAL 24 TR + Kanal D TR + Karadeniz TV + KiKA + Kino Nova + Kino Polska + Kino Polska Muzyka + krone.tv + Love Nature US + LUXE TV + Magic TV BG + MAX Sport 1 BG + MAX Sport 2 BG + MAX Sport 3 BG + Max Sport 4 BG + MCM Top + Melodie TV + Metro TV + Mezzo + Mezzo Live HD + MinikaGO TR + Minimini+ + Motorvision Plus + Motowizja + MovieSmart Classic + MovieSmart Türk + Movie Star + MTV 00s + MTV 80s + MTV 90s International + MTV Europe + MTV Hits International + MTV Live HD + MyZen TV + Nat Geo Wild BG + National Geographic BG + Nickelodeon Commercial + Nick Junior + Nick Junior BG + Nova News + Nova Sport BG + Nova TV BG + Novelas+ + Novelas Plus 1 + NOW + Nowa TV + Number One Türk + NUTA.TV HD + NUTA GOLD + oe24.TV + ORF2 + Passion XXX + Pickbox TV BG + Planeta Folk BG + Planeta TV BG + Planete+ PL + Playboy TV + Plovdivska Pravoslavna TV + Polonia1 + POLO TV + Polsat 2 + Polsat Cafe + Polsat Comedy Central Extra + Polsat Doku + Polsat Film + Polsat Games + Polsat Music + Polsat News 2 + Polsat News + Polsat Play + Polsat Rodzina + Polsat Seriale + Polsat Viasat Explore + Polsat Viasat History + Polsat Viasat Nature + Power TV PL + PRO 7 Österreich + ProSieben MAXX + Puls 4 + RAI DUE + RAI News 24 + RAI TRE + RAI UNO + Rai Yoyo + Reality Kings + Red Carpet TV PL + RedLight HD + RiC DE + Ring TV + RT Documentary + RTL DE + RTL Zwei + Russia Today + Sat.1 Gold + Sat.1 Österreich + Show TV + SinemaTV 2 + SinemaTV 1001 + SinemaTV 1002 + SinemaTV + SinemaTV Aile 2 + SinemaTV Aile + SinemaTV Aksiyon 2 + SinemaTV Aksiyon + SinemaTV Komedi 2 + SinemaTV Komedi + SinemaTV Yerli 2 + SinemaTV Yerli + Sixx AT + Skat TV + Sky News + SkyShowtime 1 Nordic + Sportdigital EDGE + STAR BG + STAR Channel + STAR Crime BG + STAR Life BG + Stars TV PL + Star TV TR + Stingray CMusic + Stingray Djazz + Stingray iConcerts + Stopklatka + Sundance TV PL + Super Polsat + Super RTL + SuperToons + TAY TV + TBN Polska HD + TELE 1 + Telewizja 13 + teve2 + teve2 TR + TGRT Belgesel + The Fishing and Hunting + The Fishing and Hunting RO + The History Channel + The Voice BG + Tivibu Spor + TLC + Travel Channel + Travel TV BG + Travelxp + TRT 1 + TRT 2 + TRT 4K + TRT Arapça + TRT Avaz + TRT Belgesel + TRT Çocuk + TRT EBA TV İlkokul + TRT EBA TV Lise + TRT EBA TV Ortaokul + TRT Haber + TRT Kurdî + TRT Müzik + TRT Spor + TRT World + TTV + TV1 BG + TV 4 + TV5 + TV5 Monde + TV5Monde Europe + TV 8.5 + TV 8 TR + TV100 TR + TVE Internacional + TVN 7 + TVN24 + TVN24 BiS + TV Net + TVN Style + TVN Turbo + TVP 2 + TVP 3 + TVP ABC + TVP HD + TVP Historia + TVP Info + TVP Kobieta + TVP Kultura + TVP Regionalna + TVP Rozrywka + TVP Seriale + TV Puls PL + TVR PL + TVS + TV Silesia + TVT PL + TV Trwam + Uçankuş TV + Viasat Explore CEE + Viasat History + Viasat Kino Balkan + Viasat Nature CEE + Vivacom Arena + VOX + VTK BG + Wness TV + wPolsce PL + Wydarzenia 24 + Xtreme TV + XXL + ZDF + БНТ 1 + БНТ 2 + БНТ 3 + БНТ 4 + Евроком + Наша ТВ + Первый + Родина + Россиᴙ 24 + Телевизия Стара Загора BG + ФЕН ТВ + Фен Фолк ТВ + 2X2 + 4Fun Dance + 4Fun Kids + 13 Ulica + 24 Kitchen + 24Kitchen PT + 360 TuneBox + A2TV TR + Active Family + Adria TV + Adult Channel + Adventure HD + Agro TV + a Haber + Ale Kino+ + ALFA TV + ALFA TVP + Al Jazeera + Al Jazeera Arabic English + Al Jazeera Balkans + Alternativna televizija Banja Luka + AMC + AMC PL + a News + Anixe HD Serie + ANIXE plus + A Para + Arena Esport + Arena Fight + Arena Sport 1 BiH + Arena Sport 1 HR + Arena Sport 1 Premium + Arena Sport 1 Premium BiH + Arena Sport 1 RS + Arena Sport 1x2 + Arena Sport 2 BiH + Arena Sport 2 Premium + Arena Sport 2 Premium BiH + Arena Sport 3 BiH + Arena Sport 3 Premium + Arena Sport 3 Premium BiH + Arena Sport 4 BiH + Arena Sport 4 RS + Arena Sport 5 BiH + Arena Sport 6 BiH + Arena Sport 6 RS + ATV2 + AXN + AXN Black PL + AXN PL + AXN Spin + AXN Spin PL + AXN White PL + B1 TV + B92 + BabyTV + Balkanika TV + Balkan trip + Balkan TV + BBC Earth + BBC News Channel + BBC World News + BBN Türk + BDC Televizija + Behar TV Sarajevo + Bir TV + BlicTV + Bloomberg Adria + Bloomberg HT + Bloomberg TV + BN 2 HD + BN music + Brainz TV + Bravo Music + Brazzers TV (ex. Private Spice) + CANAL+ Kuchnia + Cartoonito CEE + Cartoon Network + CBS Reality + CCTV 4 Europe + CGTN + CGTN Documentary + Cinema TV + Cinemax 2 + Cinemax + CineStar Action&Thriller + CineStar Premiere 1 + CineStar Premiere 2 + CineStar TV1 + CineStar TV2 + CineStar TV Comedy Family + CineStar TV Fantasy + City Play + City TV + Club MTV International + CNBC Europe + CNN Europe + Comedy Central PL + Crime+Investigation PL + Crime & Investigation Channel + Croatian Music Channel + Das Erste + Da Vinci Learning + Deutsche Welle English + Dexy TV + Digi 24 + Disco Polo Music PL + Discovery Animal Planet + Discovery Channel + Discovery Historia + Discovery Science + Disney Channel + Disney Channel DE + DIVA (ex. Universal) + DIZI + Dizi Smart Max + Dizi Smart Premium + DMAX DE + DMAX TR + DM SAT + Dobra TV + DOKU TV + Doma TV + Dorcel TV + Dorcel XXX + DOX TV + Dream Türk + E! Entertainment + Ekotürk + English Club TV + Epic Drama (CEE) + Erox HD + Eroxxx HD + Eska Rock TV + Eska TV Extra + Etno TV RO + Euro Cinema 1 + Euro Cinema 2 + Euro Cinema 3 + Euro Cinema 4 + EuroNews + Euronews FR + EuroNews Srbija + European League of Football Channel + Eurosport 1 DE + Eurosport 2 + Eurosport + EWTN + EWTN PL + Extreme Sports Channel + FACE TV + FashionBox HD + Fashion TV + Fast&FunBox HD + Favorit TV RO + FightBox HD + Fight Klub HD + Filmax + FilmBox Arthouse + FilmBox Extra RS + FilmBox Family PL + FilmBox Premium RS + FilmBox Stars RS + Film Cafe PL + Fokus TV + Folx TV + Food Network + FOX NEWS + France24 + France24 Arabic + France 24 French + FREEДОМ + FX Comedy PL + FX PL + GameHub HR + Gametoon HD + GP1 + Grand Televizija + Haber Global + Habitat TV + HappyTV + Hayat 2 + Hayat Folk Box + Hayat Love Box + Hayat Music Box + Hayatovci + Hayat Stil i život + Hayat TV + HBO2 + HBO3 + HBO + HEMA TV + Herceg TV + Historija TV + History Channel 2 + Home & Garden Television + HRT 1 + HRT 2 + HRT 3 + HRT 4 + HRT Int. + HSE + HUSTLER TV + Hype TV + IDJ World + Imperia TV + InformerTV + Insajder TV + Investigation Discovery + Izvorna TV + JimJam + Jugoton TV + K1 TV + K::CN 1 Kopernikus + K::CN 2 Music + K::CN 3 Svet Plus + Kabel Eins + Kanal1 + Kanal 3 Prnjavor + Kanal 6 + KANAL 24 TR + Karadeniz TV + Kazbuka + KiKA + Kino Polska + Kino Polska Muzyka + KinoTV + KitchenTV + Klape i Tambure TV + KLASIK + krone.tv + Kurir TV + Laudato TV + Lov i ribolov + M1 Family + M1 FILM + M1 Gold + Maria Vision + Melodie TV + MinikaGO TR + Minimax + Minimini+ + Motorvision Plus International + Motowizja + MovieSmart Classic + MovieSmart Türk + MTV 00s + MTV 80s + MTV 90s International + MTV Europe + MTV Hits International + MTV Igman + MTV Live HD + MY TV + N1 BA + N1 HR + N1 RS + N24 Doku + Narodna TV + Nat Geo Wild + Nat Geo Wild HD + National Geographic + National Geographic Channel HD + National Geographic RS + NBA TV + Neon TV + Newsmax Balkans + Nickelodeon Commercial + Nick Junior + Nick Music + Nicktoons + Nova BH + Nova Max + Nova S + Nova Series + Nova Sport Srbija + NOVA TV + Novelas+ + Novelas Plus 1 + NOW + Nowa TV + NTV 101 Sanski most + NTV IC Kakanj + Number One Türk + NUTA.TV HD + NUTA GOLD + OBN + oe24.TV + O Kanal + O Kanal Music + O Kanal Plus + One + Otvorena televizija + OTV Valentino + Pickbox TV RS + Pikaboo + Pink Action + Pink and Roll + Pink BH + Pink Classic + Pink Comedy + Pink Crime & Mystery + Pink Erotic 1 + Pink Erotic 2 + Pink Erotic 3 + Pink Erotic 4 + Pink Erotic 5 + Pink Erotic 6 + Pink Erotic 7 + Pink Erotic 8 + PINK Family + Pink Fashion + Pink Fight Network + PINK Film + Pink Folk 1 + Pink Folk 2 + Pink Ha Ha + Pink Hits 2 + Pink Hits + Pink Horror + Pink Kids + Pink Koncert + Pink Kuvar + Pink LOL + Pink M + Pink Movies + Pink Music 1 + Pink Pedia + Pink Premium + Pink Reality + Pink Romance + Pink SCI FI & Fantasy + Pink Serije + Pink Show + Pink Soap + Pink Style + Pink Super Kids + Pink Thriller + Pink Timeout + Pink Western + PINK World + Pink World Cinema + PINK Zabava + Planete+ PL + Playboy TV + Poljoprivredna TV + Polonia1 + POLO TV + Polsat 2 + Polsat Cafe + Polsat Comedy Central Extra + Polsat Doku + Polsat Film + Polsat Music + Polsat News 2 + Polsat News + Polsat Play + Polsat Rodzina + Polsat Seriale + Polsat Viasat Explore + Polsat Viasat History + Polsat Viasat Nature + Posavska Televizija + Power TV PL + Premier League TV + Private TV + PRO 7 Österreich + ProSieben + ProSieben MAXX + ProTV Tomislavgrad + Prva FILES + Prva KICK + Prva LIFE + Prva MAX + Prva Plus + Prva Srpska TV + Prva World + Puls 4 + QVC Deutschland + Radio Televizija BIH + Radio Televizija BN + Radio Televizija Federacije BIH + RAI DUE + RAI Education + RAI News 24 + RAI TRE + RAI UNO + Rai Yoyo + Reality Kings + Red Carpet TV PL + RedLight HD + RED tv + RT Documentary + RTL 2 + RTL + RTL DE + RTL Kockica + RTL Living + RTL Nitro + RTL Zwei + RTRS + RTRS PLUS + RTS 1 + RTS 2 + RTS Drama + RTSH 3 + RTSH 24 + RTSH Plus + RTSH Shkollë + RTS Klasika + RTS Kolo + RTS Muzika + RTS Nauka + RTS Poletarac + RTS SVET + RTS Trezor + RTS Život + RTV7 Tuzla + RTV BPK Goražde + RTV Herceg-Bosne + RTV HIT Brčko + RTV Lukavac + RT Vojvodina 1 + RTV PINK + RTV Slon Tuzla + RTV Slovenija 1 + RTV Slovenija 2 + RTV Slovenija 3 + RTV Tuzlanskog Kantona + RTV Unsko-sanskog kantona + RTV Visoko + RTV Vogošća + RTV Zenica + Russia Today + SAT.1 + Sat.1 Gold + Sat.1 Österreich + SciFi + Sevdah TV + Shoptel + Show TV + SinemaTV 2 + SinemaTV 1001 + SinemaTV 1002 + SinemaTV + SinemaTV Aile 2 + SinemaTV Aile + SinemaTV Aksiyon 2 + SinemaTV Aksiyon + SinemaTV Komedi 2 + SinemaTV Komedi + SinemaTV Yerli 2 + SinemaTV Yerli + Sixx AT + SK Fight + SK Golf + Sky News + Slobomir + Smart TV Tešanj + SOS Plus + Sport 1 DE + Sport Klub 1 Hrvatska + Sport Klub 3 + Sport Klub 4 + Sport Klub 5 + Sport Klub 6 + Sportska Televizija + STAR Channel + STAR Crime + STAR Life + STAR Movies + Stars TV PL + Star TV TR + Stingray iConcerts + Stopklatka + Sundance TV PL + Supermedia Televizija + Super Polsat + Super RTL + SuperSat TV + Superstar 2 + Superstar 3 + Superstar TV + Tanjug Tačno + Tatabrada + TAY TV + TBN Polska HD + TELE 1 + Tele 5 DE + Televizija 5 + Televizija 24 + Televizija Alfa + Televizija Crne Gore MNE + Televizija Dalmacija + Televizija Doktor + Telewizja 13 + teve2 + teve2 TR + TGRT Belgesel + The History Channel + Tivibu Spor + TLC + TLC DE + TNT Kids + TOGGO plus + Toxic Folk + Toxic Rap + Toxic TV + Trace Urban + Travel Channel + Tropik TV + TRT 1 + TRT 4K + TRT Arapça + TRT Avaz + TRT EBA TV İlkokul + TRT EBA TV Lise + TRT EBA TV Ortaokul + TRT World + TTV + TV1 Mreža + TV 4 + TV5 + TV 8.5 + TV 8 TR + TV100 TR + TV Arena Bijeljina + TV Duga + SAT + TV Istočno Sarajevo + TVN 7 + TV Net + TVN Style + TVN Turbo + TVP 2 + TVP 3 + TVP ABC + TVP HD + TVP Historia + TVP Info + TV PINK EXTRA + TV PINK PLUS + TVP Kobieta + TVP Kultura + TVP Rozrywka + TVP Seriale + TV Ras + TVR Cluj RO + TVR Craiova RO + TVR Iasi RO + TVR PL + TVR Tg-Mures RO + TVR Timisoara RO + TVS + TV Sarajevo + TV Silesia + TVT PL + TV Trwam + TV Vijesti + Uçankuş TV + Valentino Etno + Valentino Music HD + Vavoom + Vesti + Viasat Explore CEE + Viasat History + Viasat Kino Balkan + Viasat Nature CEE + Viasat True Crime + Vikom + VOX + VOX up + Welt + wPolsce PL + Wydarzenia 24 + Xtreme TV + XXL + Yaban TV + Zagrebačka Televizija + ZDF + ZDFinfo + ZDFneo + МРТ 1 + Россиᴙ 24 + Телевизија Храм + 1-2-3.tv + 1-2-3.tv + 2X2 + 2X2 + 2X2 + 3 Plus CH + 3 Plus CH + 3 Plus CH + 3SAT + 3SAT + 3SAT + 4Fun Dance + 4Fun Dance + 4Fun Dance + 4Fun Kids + 4Fun Kids + 4Fun Kids + 4FunTV + 4FunTV + 4FunTV + 4 Plus + 4 Plus + 4 Plus + 4 Seven + 5 Plus + 5 Plus + 5 Plus + 5Select + 5 Star + 6 plus + 6 plus + 6 plus + 6ter + 13th Street DE + 13th Street DE + 13 Ulica + 13 Ulica + 13 Ulica + 20 Mediaset + A2TV TR + A2TV TR + A2TV TR + Active Family + Active Family + Active Family + Adult Channel 2 + Adult Channel + Adventure HD + Adventure HD + Adventure HD + a Haber + a Haber + a Haber + Ale Kino+ + Ale Kino+ + Ale Kino+ + ALFA TVP + ALFA TVP + ALFA TVP + Al Jazeera + Al Jazeera + Al Jazeera + Al Jazeera Arabic Arabic + Al Jazeera Arabic English + Al Jazeera Balkans + Alternativna televizija Banja Luka + AMC PL + AMC PL + AMC PL + a News + a News + a News + Animal Planet DE + Animal Planet DE + Anixe HD Serie + Anixe HD Serie + Anixe HD Serie + ANIXE plus + ANIXE plus + ANIXE plus + Antena Europe + A Para + A Para + A Para + À Punt ES + ARD-alpha + ARD-alpha + ARD-alpha + ARTE DE + ARTE DE + ARTE DE + ARTE FR + ATV1 + ATV1 + ATV1 + ATV2 + ATV2 + ATV2 + ATV TR + ATV TR + auftanken.TV + auftanken.TV + auftanken.TV + Auto Motor und Sport + AXN Black DE + AXN Black DE + AXN Black PL + AXN Black PL + AXN Black PL + AXN PL + AXN PL + AXN PL + AXN Spin PL + AXN Spin PL + AXN Spin PL + AXN White DE + AXN White DE + AXN White PL + AXN White PL + AXN White PL + BabyTV + Baden TV + Balkanika TV + Bayerischen Fernsehen Nord + Bayerischen Fernsehen Nord + BBC1 + BBC2 + BBC3 + BBC4 + BBC News Channel + BBC Parliament + BBC World News + BBC World News + BBC World News + BBN Türk + BBN Türk + BBN Türk + beIN GURME + beIN GURME + beIN iZ + beIN iZ + beIN iZ + beIN Movies Premiere TR + Benfica TV + Bergblick + Bergblick + Bergblick + Beyaz TV + Beyaz TV + Beyaz TV + BFM Business + BFM TV + BFM TV + BFM TV + Bibel TV + Bibel TV + Bibel TV + Bloomberg HT + Bloomberg HT + Bloomberg HT + Bloomberg TV + Bloomberg TV + Bloomberg TV + Blue Hustler + Boing + Boing Plus + BonGusto + BonGusto + BR Fernsehen Süd + BR Fernsehen Süd + BR Fernsehen Süd + Canal 24H + Canal 24H + Canal+ Domo + Canal+ Domo + Canal+ France + Canal+ France + CANAL+ Kuchnia + CANAL+ Kuchnia + CANAL+ Kuchnia + Canale 5 + CARAC1 + CARAC4 + Cartoonito CEE + Cartoonito Germany + Cartoonito Germany + Cartoonito Italia + Cartoon Network DE + Cartoon Network DE + CBBC + Cbeebies + CCTV 4 Europe + CCTV 4 Europe + CGTN + CGTN + CGTN Documentary + CGTN Documentary + Challenge TV + Channel 4 + Channel 5 + Chérie 25 + Children's ITV + cielo + Cine34 + Class TV Moda + ClipMyHorse.TV Linear TV + Clubland TV + Club MTV International + CNBC Europe + CNBC Europe + CNBC Europe + CNews + CNews + CNN Europe + CNN Europe + CNN Europe + CNN Türk TV + CNN Türk TV + Comedy Central DE + Comedy Central DE + Comedy Central DE + Comedy Central PL + Comedy Central PL + Comedy Central PL + Crime+Investigation PL + Crime+Investigation PL + Crime+Investigation PL + Crime & Investigation Channel + Crime & Investigation DE + Crime & Investigation DE + Croatian Music Channel + Croatian Music Channel + CStar FR + Curiosity Channel + Curiosity Channel + Curiosity Channel + Das Bild TV + Das Bild TV + Das Bild TV + Das Erste + Das Erste + Das Erste + Das Health TV + Das Health TV + Daystar + DAZN 1 DE + DAZN 2 DE + Deluxe Music + Deluxe Music + Deluxe Music + Deutsches Musik Fernsehen + Deutsches Musik Fernsehen + Deutsches Musik Fernsehen + Deutsche Welle English + Deutsche Welle English + Deutsche Welle English + Deutsche Welle Espanol + DF1 + Disco Polo Music PL + Disco Polo Music PL + Disco Polo Music PL + Discovery Channel DE + Discovery Channel DE + Discovery Channel IT + Discovery Historia + Discovery Historia + Discovery Historia + Disney Channel DE + Disney Channel DE + Disney Channel DE + Dizi Smart Max + Dizi Smart Max + Dizi Smart Max + Dizi Smart Premium + Dizi Smart Premium + Dizi Smart Premium + DMAX DE + DMAX DE + DMAX DE + DMAX IT + DMAX TR + DMAX TR + DMAX TR + DMAX UK + DM SAT + DM SAT + DM SAT + Dorcel TV + Dorcel TV + Dorcel XXX + Dream Türk + Dream Türk + Dream Türk + ducktv HD + ducktv SD + Duna TV + Duna World + E4 + E! Entertainment + E! Entertainment + Ekotürk + Ekotürk + Ekotürk + Erox HD + Erox HD + Eroxxx HD + Eroxxx HD + Eroxxx HD + Eska Rock TV + Eska Rock TV + Eska Rock TV + Eska TV + Eska TV + Eska TV + Eska TV Extra + Eska TV Extra + Eska TV Extra + eSports 1 + Euro D + Euro D + EuroNews + EuroNews + EuroNews + Euronews FR + European League of Football Channel + European League of Football Channel + European League of Football Channel + Eurosport 1 DE + Eurosport 1 DE + Eurosport 1 DE + Eurosport 2 DE + Eurosport 2 DE + Eurosport 2 FR + Eurosport 2 IT + Eurosport + Eurosport FR + Eurosport IT + Euro Star + Euro Star + EWTN + EWTN PL + EWTN PL + EWTN PL + Extreme Sports Channel + Fashion TV + Fashion TV + Fashion TV + Fast&FunBox HD + Fight 24 + Fight Klub HD + Fight Klub HD + Fight Klub HD + Film4 + Filmax + Filmax + Filmax + FilmBox Family PL + FilmBox Family PL + FilmBox Family PL + Film Cafe PL + Film Cafe PL + Film Cafe PL + Fix & Foxi + Fix & Foxi + Fix & Foxi + Focus TV + Fokus TV + Fokus TV + Fokus TV + Folx TV + Folx TV + Folx TV + Food Network Italia + Food Network UK + France 2 + France 2 + France 3 + France 3 + France 4 + France 4 + France 5 + France 5 + France24 + France24 + France24 + France 24 French + France 24 French + France 24 French + France Info + Frisbee + FS1 AT + FunBox UHD + FX Comedy PL + FX Comedy PL + FX Comedy PL + FX PL + FX PL + FX PL + Game One + Geo Television + Geo Television + Giallo TV + Ginx Esports TV + Goldstar + Golf plus + Gulli + Gulli + Gute Laune TV + Gute Laune TV + Haber Global + Haber Global + Haber Global + Habertürk + Habertürk + Habitat TV + Habitat TV + Habitat TV + Halk TV + Hamburg 1 + Heimatkanal + Heimatkanal + HGTV DE + HGTV DE + HGTV DE + HGTV IT + History Channel DE + History Channel DE + Home & Garden Television + HR Fernsehen + HR Fernsehen + HR Fernsehen + HRT 1 + HRT 1 + HSE24 Extra + HSE24 Extra + HSE24 Extra + HSE24 Trend + HSE24 Trend + HSE24 Trend + HSE + HSE + HSE + Hustler HD + HUSTLER TV + HUSTLER TV + i24News FR + Insight TV + Insight TV + Insight TV + Italia 1 + Italia 2 + ITV1 + ITV2 + ITV3 + ITV4 + iTVN + iTVN + iTVN Extra + iTVN Extra + ITV Quiz + Jukebox + K2 + K-TV Katholisches Fernsehen + K-TV Katholisches Fernsehen + K-TV Katholisches Fernsehen + Kabel Eins + Kabel Eins + Kabel Eins + Kabel Eins Classics + Kabel Eins Classics + Kabel eins Doku + Kabel eins Doku + Kabel eins Doku + Kabel eins Österreich + Kanal 7 + Kanal 7 + Kanal 7 + Kanal 9 TV + KANAL 24 TR + KANAL 24 TR + KANAL 24 TR + Kanal D TR + Kanal D TR + Kanal D TR + Karadeniz TV + Karadeniz TV + Karadeniz TV + KiKA + KiKA + KiKA + Kino Polska + Kino Polska + Kino Polska Muzyka + Kino Polska Muzyka + Kino Polska Muzyka + Kinowelt + Kinowelt + KIT-TV + Klan Kosova + Klan Kosova + Klan TV HD + KLASIK + Kral Pop TV + krone.tv + krone.tv + krone.tv + Kurier TV + La7 + La7 + La7d + La Chaîne Info + La Cinque + La Télé + L'Equipe + Love Nature US + LUXE TV + LUXE TV + LUXE TV + M6 + M6 + Marco Polo TV + Marco Polo TV + MDR Fernsehen + MDR Fernsehen + MDR Fernsehen + MDR Sachsen-Anhalt + MDR Sachsen-Anhalt + MDR Sachsen + MDR Sachsen + MDR Thüringen + MDR Thüringen + Mediaset Extra + Mediaset Italia + Melodie TV + Melodie TV + Melodie TV + Metro TV + Metro TV + Mezzo + Mezzo Live HD + MGG TV + MinikaGO TR + MinikaGO TR + MinikaGO TR + Minimini+ + Minimini+ + Minimini+ + More4 + More Than Sports TV + More Than Sports TV + Motorvision Plus + Motorvision Plus + Motorvision Plus + Motorvision Plus France + Motorvision Plus International + Motorvision Plus International + Motorvision Plus International + Motowizja + Motowizja + Motowizja + MovieSmart Classic + MovieSmart Classic + MovieSmart Classic + MovieSmart Türk + MovieSmart Türk + MovieSmart Türk + MTV 00s + MTV 80s + MTV 80s + MTV 90s International + MTV DE + MTV DE + MTV DE + MTV Hits International + MTV Live HD + MTV Live HD + münchen.tv + MyZen TV + MyZen TV + MyZen TV + N24 Doku + N24 Doku + N24 Doku + NatGeo Wild DE + NatGeo Wild DE + National Geographic DE + National Geographic DE + NBC News + NBC News + NDR Hamburg + NDR Hamburg + NDR Hamburg + NDR Mecklenburg-Vorpommern + NDR Mecklenburg-Vorpommern + NDR Niedersachsen + NDR Niedersachsen + NDR Schleswig-Holstein + NDR Schleswig-Holstein + Nick DE + Nick DE + Nick DE + Nick Junior + Nick Junior + Nick Music + Nicktoons + Nicktoons + Niederbayern TV Deggendorf-Straubing + Niederbayern TV Deggendorf-Straubing + NOVE + Novelas+ + Novelas+ + Novelas+ + Novelas Plus 1 + Novelas Plus 1 + Novelas Plus 1 + NOW + NOW + NOW + Nowa TV + Nowa TV + Nowa TV + Now Rock + NTV DE + NTV DE + NTV DE + Number One Türk + Number One Türk + Number One Türk + NUTA.TV HD + NUTA.TV HD + NUTA.TV HD + NUTA GOLD + NUTA GOLD + NUTA GOLD + OBN + OBN + oe24.TV + oe24.TV + oe24.TV + One + One + One + ORF1 + ORF1 + ORF2 + ORF2 + ORF3 + ORF3 + ORF3 + ORF Sport Plus + ORF Sport Plus + PBS America + Phoenix + Phoenix + Phoenix + PINK Film + PINK Film + PINK Film + Pink Folk 1 + Pink Folk 1 + Pink Kids + Pink Koncert + Pink Music 1 + Pink Music 1 + Pink Music 1 + Pink Reality + Planete+ PL + Planete+ PL + Planete+ PL + Playboy TV + Polonia1 + Polonia1 + Polonia1 + POLO TV + POLO TV + POLO TV + Polsat 1 + Polsat 1 + Polsat 2 + Polsat 2 + Polsat 2 + Polsat + Polsat Cafe + Polsat Cafe + Polsat Cafe + Polsat Comedy Central Extra + Polsat Comedy Central Extra + Polsat Comedy Central Extra + Polsat Doku + Polsat Doku + Polsat Doku + Polsat Film + Polsat Film + Polsat Film + Polsat Games + Polsat Games + Polsat Music + Polsat Music + Polsat Music + Polsat News 2 + Polsat News 2 + Polsat News 2 + Polsat News + Polsat News + Polsat News + Polsat Play + Polsat Play + Polsat Play + Polsat Rodzina + Polsat Rodzina + Polsat Rodzina + Polsat Seriale + Polsat Seriale + Polsat Seriale + Polsat Viasat Explore + Polsat Viasat Explore + Polsat Viasat Explore + Power Türk TV + Power TV PL + Power TV PL + Power TV PL + Private TV + PRO 7 Österreich + PRO 7 Österreich + PRO 7 Österreich + ProSieben + ProSieben + ProSieben + ProSieben Fun + ProSieben Fun + ProSieben MAXX + ProSieben MAXX + ProSieben MAXX + Puls 4 + Puls 4 + Puls 4 + Puls 8 + Puls 8 + Puls 8 + QVC 2 DE + QVC 2 DE + QVC Deutschland + QVC Deutschland + QVC Style + QVC STYLE DE + QVC STYLE DE + RadioBremen + RadioBremen + Radio Televizija BN + Radio Ticino Channel HD + Radio Ticino Channel HD + Radio Ticino Channel HD + RAI 4 + RAI 4 + RAI 4 + RAI 5 + RAI 5 + RAI 5 + RAI DUE + RAI DUE + RAI Education + RAI Gulp + RAI Italia Australia + RAI News 24 + RAI News 24 + RAI Sport 1 + RAI Storia + RAI Storia + RAI TRE + RAI TRE + RAI UNO + RAI UNO + RAI World Premium + RAI World Premium + RAI World Premium + Rai Yoyo + Rai Yoyo + Rai Yoyo + RBB Fernsehen Berlin + RBB Fernsehen Berlin + RBB Fernsehen Berlin + RBB Fernsehen Brandenburg + RBB Fernsehen Brandenburg + real time + Red Carpet TV PL + Red Carpet TV PL + Red Carpet TV PL + RedLight HD + Rete 4 + Rhein Neckar Fernsehen + RiC DE + RiC DE + RiC DE + RMC Découverte + RMC Story + Rocket Beans TV + Rocket Beans TV + Romance TV + Romance TV + Romance TV + Romance TV PL + Romance TV PL + Romance TV PL + RSI La 1 + RSI La 2 + RTK 1 + RTK 1 + RTL Crime DE + RTL Crime DE + RTL Crime DE + RTL Croatia World + RTL DE + RTL DE + RTL DE + RTL Living DE + RTL Living DE + RTL Nitro + RTL Nitro + RTL Nitro + RTL Passion DE + RTL Passion DE + RTLup + RTLup + RTLup + RTL Zwei + RTL Zwei + RTL Zwei + RTP3 + RTP Internacional + RTS 2 Suisse + RTS SVET + RTS SVET + RTS Un + Russia Today + Russia Today + S1 CH + S1 CH + S1 CH + S4C + SAT.1 + SAT.1 + SAT.1 + Sat.1 Emotions + Sat.1 Emotions + Sat.1 Gold + Sat.1 Gold + Sat.1 Gold + Sat.1 Österreich + Sat.1 Österreich + Sat.1 Österreich + Schlager Deluxe + Schlager Deluxe + ServusTV + ServusTV + ServusTV + Show Türk + Show Türk + Show TV + Show TV + Show TV + SIC + SIC + SIC Internacional + SIC Internacional + SIC Noticias + SIC Noticias + Silverline Movie Channel + SinemaTV 2 + SinemaTV 2 + SinemaTV 2 + SinemaTV 1001 + SinemaTV 1001 + SinemaTV 1001 + SinemaTV 1002 + SinemaTV 1002 + SinemaTV 1002 + SinemaTV + SinemaTV + SinemaTV + SinemaTV Aile 2 + SinemaTV Aile 2 + SinemaTV Aile 2 + SinemaTV Aile + SinemaTV Aile + SinemaTV Aile + SinemaTV Aksiyon 2 + SinemaTV Aksiyon 2 + SinemaTV Aksiyon 2 + SinemaTV Aksiyon + SinemaTV Aksiyon + SinemaTV Aksiyon + SinemaTV Komedi 2 + SinemaTV Komedi 2 + SinemaTV Komedi 2 + SinemaTV Komedi + SinemaTV Komedi + SinemaTV Komedi + SinemaTV Yerli 2 + SinemaTV Yerli 2 + SinemaTV Yerli 2 + SinemaTV Yerli + SinemaTV Yerli + SinemaTV Yerli + Sixx + Sixx + Sixx + Sixx AT + Sixx AT + Sixx AT + Sky 1 DE + Sky 1 DE + Sky Atlantic DE + Sky Atlantic DE + Sky Bundesliga 1 + Sky Bundesliga 1 + Sky Bundesliga 2 + Sky Bundesliga 2 + Sky Bundesliga 3 + Sky Bundesliga 3 + Sky Bundesliga 4 + Sky Bundesliga 4 + Sky Bundesliga 5 + Sky Bundesliga 5 + Sky Bundesliga 6 + Sky Bundesliga 6 + Sky Bundesliga 7 + Sky Bundesliga 7 + Sky Cinema Action DE + Sky Cinema Action DE + Sky Cinema Best Of DE + Sky Cinema Classics DE + Sky Cinema Classics DE + Sky Cinema Family DE + Sky Cinema Family DE + Sky Cinema Fun DE + Sky Cinema Premieren + Sky Krimi DE + Sky Krimi DE + Sky Krimi DE + Sky Mix + Sky Nature DE + Sky News + Sky News + Sky Sport 1 DE + Sky Sport 1 DE + Sky Sport 3 DE + Sky Sport 3 DE + Sky Sport 4 DE + Sky Sport 4 DE + Sky Sport 5 DE + Sky Sport 5 DE + Sky Sport 6 DE + Sky Sport 6 DE + Sky Sport 7 DE + Sky Sport 7 DE + Sky Sport 8 DE + Sky Sport 8 DE + Sky Sport 9 DE + Sky Sport 9 DE + Sky Sport 10 DE + Sky Sport 10 DE + Sky Sport Bundesliga 8 + Sky Sport Bundesliga 9 + Sky Sport Bundesliga 10 + Sky Sport Bundesliga HD + Sky Sport Bundesliga UHD + Sky Sport Mix DE + Sky Sport News DE + Sky Sport News DE + Sky Sport Premier League DE + Sky TG24 HD + SonLife + SonLife + sonnenklar.TV + sonnenklar.TV + Spiegel Geschichte + Spiegel Geschichte + Spiegel Geschichte + Sport 1 DE + Sport 1 DE + Sport 1 plus DE + Sport 1 plus DE + Sport 1 plus DE + Sportdigital Fussball 2 + Sportdigital Fußball + Sportdigital Fußball + Sportdigital Fußball + Sportitalia + SRF 1 + SRF 1 + SR Fernsehen + SR Fernsehen + SR Fernsehen + SRF Info + SRF Info + SRF Info + SRF Zwei + SRF Zwei + SRF Zwei + Stars TV PL + Stars TV PL + Stars TV PL + Star TV TR + Star TV TR + Star TV TR + Stingray Classica + Stingray Classica + Stingray Classica + Stingray Djazz + Stingray Djazz + Stingray Djazz + Stingray iConcerts + Stopklatka + Stopklatka + Stopklatka + Sundance TV PL + Sundance TV PL + Sundance TV PL + Super! + Super Polsat + Super Polsat + Super Polsat + Super RTL + Super RTL + Super RTL + Swiss1 TV + SWR1 Baden-Württemberg + SWR + SWR + SWR + SWR Baden-Württemberg + SWR Baden-Württemberg + SWR Baden-Württemberg + Syfy HD DE + Syfy HD DE + tagesschau24 + tagesschau24 + tagesschau24 + TAY TV + TAY TV + TAY TV + TBN Polska HD + TBN Polska HD + TBN Polska HD + TELE 1 + TELE 1 + TELE 1 + Tele 5 DE + Tele 5 DE + Tele 5 DE + TeleBärn + TeleBärn + TeleBärn + TeleDeporte + Televisión de Galicia + Télévision française 1 + Televizioni 7 + Telewizja 13 + Telewizja 13 + Telewizja 13 + Tele Zürich + Tele Zürich + Tele Zürich + teve2 + teve2 + teve2 + teve2 TR + teve2 TR + teve2 TR + TF1 Séries Films + TFX + TGCOM24 + TGRT Belgesel + TGRT Belgesel + TGRT Belgesel + TGRT EU + TGRT HABER + TGRT HABER + The History Channel + The Nautical Channel + The Nautical Channel + TiJi + Tivibu Spor + Tivibu Spor + Tivibu Spor + TLC DE + TLC DE + TLC DE + TMC + TOGGO plus + TOGGO plus + TOGGO plus + Top Channel + Top Crime + Trace Urban + TRT 1 + TRT 1 + TRT 2 + TRT 4K + TRT 4K + TRT 4K + TRT Arapça + TRT Arapça + TRT Arapça + TRT Avaz + TRT Avaz + TRT Avaz + TRT Belgesel + TRT Çocuk + TRT Diyanet + TRT Diyanet + TRT Diyanet + TRT EBA TV İlkokul + TRT EBA TV İlkokul + TRT EBA TV İlkokul + TRT EBA TV Lise + TRT EBA TV Lise + TRT EBA TV Lise + TRT EBA TV Ortaokul + TRT EBA TV Ortaokul + TRT EBA TV Ortaokul + TRT Haber + TRT Haber + TRT Haber + TRT Kurdî + TRT Kurdî + TRT Kurdî + TRT Müzik + TRT Spor + TRT Spor Yıldız + TRT Spor Yıldız + TRT Turk + TRT Turk + TRT World + TRT World + TRT World + TTV + TTV + TV 4 + TV 4 + TV5 + TV5 + TV5 + TV5 Monde + TV5 Monde + TV5Monde Europe + TV5Monde Europe + TV 8.5 + TV 8.5 + TV 8.5 + TV8 IT + TV8 IT + TV 8 TR + TV 8 TR + TV24 + TV24 + TV24 + TV25 + TV25 + TV25 + TV100 TR + TV100 TR + TV100 TR + TV1000 Global Kino + TV 2000 + TV Dukagjini + TVE Internacional + TVE Internacional + TVI + TVI + TVM3 + TVN 7 + TVN 7 + TVN 7 + TVN24 + TVN24 + TVN24 BiS + TVN + TVN + TV Net + TV Net + TV Net + TV Now DE + TVN Style + TVN Style + TVN Style + TVN Turbo + TVN Turbo + TVN Turbo + TVP 1 + TVP 1 + TVP 2 + TVP 2 + TVP 2 + TVP 3 + TVP 3 + TVP 3 + TVP ABC + TVP ABC + TVP HD + TVP HD + TVP HD + TVP Historia + TVP Historia + TVP Info + TVP Info + TVP Info + TV PINK EXTRA + TV PINK EXTRA + TV PINK EXTRA + TV PINK PLUS + TV PINK PLUS + TV PINK PLUS + TVP Kobieta + TVP Kobieta + TVP Kobieta + TVP Kultura + TVP Kultura + TVP Polonia + TVP Regionalna + TVP Regionalna + TVP Rozrywka + TVP Rozrywka + TVP Seriale + TVP Seriale + TV Puls PL + TV Puls PL + TVP World + TVR PL + TVR PL + TVR PL + TVS + TVS + TVS + TV Silesia + TV Silesia + TV Silesia + TVT PL + TVT PL + TVT PL + TV Trwam + TV Trwam + TV Trwam + Twenty Seven + Uçankuş TV + Uçankuş TV + Uçankuş TV + U&Dave + U&Drama + Ülke TV + Ülke TV + Universal TV DE + U&Yesterday + VG TV + Vivid Red + Vivid TV + Vivid TV + VOX + VOX + VOX + VOX up + VOX up + W9 + W24 + Warner TV Comedy DE + Warner TV Comedy DE + Warner TV Film DE + Warner TV Film DE + Warner TV Serie DE + Warner TV Serie DE + WDR Fernsehen + WDR Fernsehen + WDR Fernsehen + WDR Fernsehen Köln + WDR Fernsehen Köln + Welt + Welt + Welt + Welt der Wunder + Welt der Wunder + Welt der Wunder + Wetter + Wetter + Wir 24 + World Fashion Channel + World Fashion Channel + World Fashion Channel + wPolsce PL + wPolsce PL + wPolsce PL + Wydarzenia 24 + Wydarzenia 24 + Wydarzenia 24 + Xtreme TV + Xtreme TV + Xtreme TV + XXL + XXL + XXL + Yaban TV + Yaban TV + Yaban TV + Zagrebačka Televizija + ZDF + ZDF + ZDF + ZDFinfo + ZDFinfo + ZDFinfo + ZDFneo + ZDFneo + ZDFneo + 2X2 + 2X2 + 3SAT + 4Fun Dance + 4Fun Dance + 4Fun Dance + 4Fun Kids + 4Fun Kids + 4Fun Kids + 4FunTV + 4FunTV + 4 Seven + 4 Seven + 5ACTION + 5ACTION + 5Select + 5Select + 5 Star + 5 Star + 5 USA + 13 Ulica + 13 Ulica + 13 Ulica + 20 Mediaset + 24 Kitchen + 24Kitchen PT + 360 TuneBox + A2 CNN + A2TV TR + A2TV TR + A2TV TR + ABC News Albania + Active Family + Active Family + Active Family + Adria TV + Adult Channel 2 + Adventure HD + Adventure HD + Adventure HD + Agro TV + a Haber + a Haber + a Haber + Ale Kino+ + Ale Kino+ + Ale Kino+ + ALFA TVP + ALFA TVP + Al Jazeera + Al Jazeera Arabic Arabic + Al Jazeera Arabic English + Al Jazeera Arabic English + Al Jazeera Balkans + AMC + AMC PL + AMC PL + AMC PL + AMC UK + a News + a News + a News + Animal Planet UK + Animal Planet UK + Anixe HD Serie + A Para + A Para + A Para + À Punt ES + Arena Esport + Arena Fight + Arena Sport 1 Premium + Arena Sport 1 RS + Arena Sport 1x2 + Arena Sport 2 Premium + Arena Sport 2 RS + Arena Sport 3 Premium + Arena Sport 3 RS + Arena Sport 4 RS + Arena Sport 5 RS + Arena Sport 6 RS + Arena Sport 7 RS + Arena Sport 8 RS + Arena Sport 9 RS + Arena Sport 10 RS + Arise News + Arise News + ARTE DE + At The Races + At The Races + ATV1 + ATV1 + ATV1 + ATV2 + ATV2 + ATV2 + AXN + AXN Black PL + AXN PL + AXN PL + AXN PL + AXN Spin + AXN Spin PL + AXN Spin PL + AXN Spin PL + AXN White PL + AXN White PL + B92 + BabyTV + BabyTV + BabyTV + Balkan trip + Balkan TV + Bang Bang + BBC1 + BBC1 + BBC1 Northern Ireland + BBC1 Northern Ireland + BBC 1 Wales + BBC2 + BBC2 + BBC 2 Northern Ireland + BBC3 + BBC3 + BBC4 + BBC4 + BBC Alba + BBC Alba + BBC Earth + BBC Earth + BBC News Channel + BBC News Channel + BBC One Scotland + BBC One Scotland + BBC Parliament + BBC Parliament + BBC Scotland + BBC Two Wales + BBC World News + BBC World News + BBN Türk + BBN Türk + Benfica TV + Beyaz TV + BlicTV + Bloomberg Adria + Bloomberg HT + Bloomberg HT + Bloomberg HT + Bloomberg TV + BN 2 HD + BN music + Body in Balance + Boomerang UK + Boomerang UK + Brainz TV + Bravo Music + Brazzers TV (ex. Private Spice) + BT Sport ESPN + Canal+ Domo + CANAL+ Kuchnia + CANAL+ Kuchnia + CANAL+ Kuchnia + Capital XTRA + Capital XTRA + Cartoonito CEE + Cartoonito CEE + Cartoonito CEE + Cartoonito UK + Cartoonito UK + Cartoon Network + Cartoon Network + Cartoon Network + Cartoon Network UK + Cartoon Network UK + CBBC + CBBC + Cbeebies + Cbeebies + CBS Reality + CBS Reality PL + CCTV 4 Europe + CGTN + CGTN + CGTN Documentary + Challenge TV + Challenge TV + Channel 4 +1 + Channel 4 +1 + Channel 4 + Channel 4 + Channel 5 + Channel 5 + Channel S + Children's ITV + Children's ITV + Cinemania TV + Cinemax 2 + Cinemax + CineStar Action&Thriller RS + CineStar Premiere 1 + CineStar Premiere 2 + CineStar TV2 + CineStar TV Comedy Family + CineStar TV Fantasy + CineStar TV RS + Click TV + Clubland TV + Club MTV International + CNBC Europe + CNN Europe + CNN Europe + CNN Türk TV + Comedy Central Extra UK + Comedy Central Extra UK + Comedy Central PL + Comedy Central PL + Comedy Central PL + Comedy Central UK + Comedy Central UK + Crime+Investigation PL + Crime+Investigation PL + Crime+Investigation PL + Crime and Investigation UK + Crime and Investigation UK + Crime & Investigation Channel + Croatian Music Channel + Cufo TV + Cúla4 + Das Erste + Daystar + Deutsche Welle English + Deutsche Welle English + Dexy TV + DigitAlb Melody TV + Disco Polo Music PL + Disco Polo Music PL + Disco Polo Music PL + Discovery Animal Planet + Discovery Animal Planet + Discovery Channel + Discovery Channel UK + Discovery Channel UK + Discovery Historia + Discovery Historia + Discovery Historia + Discovery History UK + Discovery History UK + Discovery Science + Discovery Science UK + Discovery Science UK + Discovery Turbo UK + Discovery Turbo UK + Disney Channel + Disney Junior + DIVA (ex. Universal) + DIZI + Dizi Smart Max + Dizi Smart Max + Dizi Smart Premium + Dizi Smart Premium + DMAX TR + DMAX TR + DMAX TR + DMAX UK + DMAX UK + DM SAT + Dorcel TV + Dorcel XXX + DOX TV + Dream Türk + Dream Türk + Dream Türk + ducktv HD + ducktv SD + E4 + E4 + E4 Extra + E4 Extra + E! Entertainment + Eden UK + Eden UK + Ekotürk + Ekotürk + Ekotürk + Elrodi + English Club TV + Epic Drama (CEE) + Erox HD + Erox HD + Eroxxx HD + Eroxxx HD + Eska Rock TV + Eska Rock TV + Eska Rock TV + Eska TV + Eska TV + Eska TV Extra + Eska TV Extra + Eska TV Extra + Eurochannel + Euro Cinema 1 + Euro Cinema 2 + Euro Cinema 3 + Euro Cinema 4 + Euro D + EuroNews + EuroNews + EuroNews + Euronews Albania + EuroNews Srbija + European League of Football Channel + European League of Football Channel + European League of Football Channel + Eurosport 2 + Eurosport.com + Eurosport + Euro Star + EWTN PL + EWTN PL + EWTN PL + Explorer Histori + Explorer Natyra + Explorer Shkence + Extreme Sports Channel + Extreme Sports Channel + Extreme Sports Channel + FACE TV + FashionBox HD + FashionBox HD + Fashion TV + Fashion TV + Fast&FunBox HD + Fast&FunBox HD + FAX News + FightBox HD + Fight Klub HD + Fight Klub HD + Fight Klub HD + Film4 + Film4 + Filmax + Filmax + FilmBox Arthouse + FilmBox Arthouse + FilmBox Extra RS + FilmBox Family PL + FilmBox Family PL + FilmBox Family PL + FilmBox Premium RS + FilmBox Stars RS + Film Cafe PL + Film Cafe PL + Film Cafe PL + Fokus TV + Fokus TV + Fokus TV + Folx TV + Folx TV + Folx TV + Food Network + Food Network UK + Food Network UK + FOX NEWS + France 3 + France24 + France24 + France24 Arabic + France24 Arabic MENA + France 24 French + Fuel TV + FX Comedy PL + FX Comedy PL + FX Comedy PL + FX PL + FX PL + FX PL + Gametoon HD + Gametoon HD + Gems TV + Gems TV + Ginx Esports TV + Ginx Esports TV + Golica TV + Grand Televizija + Great! Movies +1 + Great! Movies +1 + Great! Movies + Great! Movies + Great! Movies Action + Great! Movies Action + Great! Movies Classic + Great! Movies Classic + Great! romance + Great! TV + Haber Global + Haber Global + Haber Global + Habitat TV + Habitat TV + Habitat TV + HappyTV + HauntTV + Hayat 2 + Hayat Folk Box + Hayat Music Box + Hayatovci + Hayat TV + HBO2 + HBO3 + HBO + HEMA TV + History Channel 2 + History Channel 2 + Home & Garden Television + Home & Garden Television UK + Home & Garden Television UK + HRT 1 + HRT 2 + HRT 3 + HRT 4 + Hustler HD + HUSTLER TV + HUSTLER TV + Ideal Home Shopping + Ideal Home Shopping + IDJ World + Insajder TV + Insight TV + INTV AL + Investigation Discovery + Investigation Discovery UK + Investigation Discovery UK + ITV1 + ITV1 + ITV2+1 + ITV2+1 + ITV2 + ITV2 + ITV3 + ITV3 + ITV4 + ITV4 + iTVN + iTVN + iTVN Extra + iTVN Extra + ITV Quiz + ITV Quiz + Jewellery Channel + Jugoton TV + Junior TV + K1 TV + K::CN 1 Kopernikus + K::CN 2 Music + K::CN 3 Svet Plus + Kanal 3 Prnjavor + Kanal 7 + Kanal 7 + KANAL 24 TR + KANAL 24 TR + KANAL 24 TR + Kanal D + Kanal D TR + Kanal D TR + Kanali 7 + Karadeniz TV + Karadeniz TV + Karadeniz TV + Kazbuka + Ketchup TV + Kino Polska + Kino Polska + Kino Polska + Kino Polska Muzyka + Kino Polska Muzyka + Kino Polska Muzyka + KitchenTV + Klan Kosova + krone.tv + krone.tv + krone.tv + Kurir TV + Legend + Living HD + London Live + Love Nature US + Lov i ribolov + Manchester TV + MCN TV + Mediaset Italia + Melodie TV + Melodie TV + Melodie TV + Metro TV + Metro TV + Mezzo Live HD + MinikaGO TR + MinikaGO TR + Minimax + Minimini+ + Minimini+ + Minimini+ + More4 + More4 + MotorTrend + Motorvision Plus France + Motorvision Plus International + Motorvision Plus International + Motowizja + Motowizja + Motowizja + Movies 24 + Movies 24 + MovieSmart Classic + MovieSmart Classic + MovieSmart Türk + MovieSmart Türk + MTV 00s + MTV 80s + MTV 80s UK + MTV 80s UK + MTV 90s International + MTV 90s UK + MTV 90s UK + MTV Europe + MTV Hits International + MTV Live HD + MTV Live HD + MTV Music UK + MTV Music UK + MTV UK + MTV UK + MUSE + My Music + MyZen TV + MyZen TV + N1 RS + Narodna TV + Nat Geo Wild + Nat Geo Wild HD + Nat Geo Wild UK + Nat Geo Wild UK + National Geographic + National Geographic Channel HD + National Geographic Channel UK + National Geographic Channel UK + NBA TV + NBC News + News 24 + Nickelodeon Commercial + Nickelodeon UK + Nickelodeon UK + Nick Jr. Too UK + Nick Junior + Nick Junior UK + Nick Junior UK + Nick Music + Nicktoons + Nicktoons UK + Nicktoons UK + Nova M + Nova Max + Nova S + Nova Series + Nova Sport Srbija + Novelas+ + Novelas+ + Novelas Plus 1 + Novelas Plus 1 + Now 70s + Now 80s + NOW + Nowa TV + Nowa TV + Nowa TV + Now Rock + NTV IC Kakanj + Number One Türk + Number One Türk + Number One Türk + NUTA.TV HD + NUTA.TV HD + NUTA.TV HD + NUTA GOLD + NUTA GOLD + OBN + oe24.TV + oe24.TV + oe24.TV + Oireachtas TV + O Kanal + Ora News + ORF2 + ORF2 + ORF2 + OTV Valentino + PBS America + PBS America + Pickbox TV RS + Pikaboo + Pink Action + Pink and Roll + Pink BH + Pink Classic + Pink Comedy + Pink Crime & Mystery + Pink Erotic 1 + Pink Erotic 2 + Pink Erotic 3 + Pink Erotic 4 + Pink Erotic 5 + Pink Erotic 6 + Pink Erotic 7 + Pink Erotic 8 + PINK Family + Pink Fashion + PINK Film + Pink Folk 1 + Pink Folk 2 + Pink Ha Ha + Pink Hits 2 + Pink Hits + Pink Horror + Pink Kids + Pink Koncert + Pink Kuvar + Pink LOL + Pink M + Pink Movies + Pink Music 1 + Pink Pedia + Pink Premium + Pink Reality + Pink Romance + Pink SCI FI & Fantasy + Pink Serije + Pink Show + Pink Soap + Pink Style + Pink Super Kids + Pink Thriller + Pink Western + PINK World + Pink World Cinema + PINK Zabava + Planete+ PL + Planete+ PL + Planete+ PL + Playboy TV + Polonia1 + Polonia1 + Polonia1 + POLO TV + POLO TV + POLO TV + Polsat 1 + Polsat 1 + Polsat 1 + Polsat 2 + Polsat 2 + Polsat 2 + Polsat + Polsat Cafe + Polsat Cafe + Polsat Cafe + Polsat Comedy Central Extra + Polsat Comedy Central Extra + Polsat Doku + Polsat Doku + Polsat Doku + Polsat Film + Polsat Film + Polsat Film + Polsat Games + Polsat Games + Polsat Music + Polsat Music + Polsat Music + Polsat News 2 + Polsat News 2 + Polsat News 2 + Polsat News + Polsat News + Polsat News + Polsat Play + Polsat Play + Polsat Play + Polsat Rodzina + Polsat Rodzina + Polsat Rodzina + Polsat Seriale + Polsat Seriale + Polsat Seriale + Polsat Viasat Explore + Polsat Viasat Explore + Polsat Viasat Explore + Polsat Viasat History + Polsat Viasat Nature + Pop Max + Pop Max + Pop UK + Pop UK + Power TV PL + Power TV PL + Power TV PL + Premier League TV + Premier Sports 1 + Premier Sports 1 + Premier Sports 2 + Premier Sports 2 + Private TV + Private TV + PRO 7 Österreich + PRO 7 Österreich + PRO 7 Österreich + ProSieben + Prva FILES + Prva KICK + Prva LIFE + Prva MAX + Prva Plus + Prva Srpska TV + Prva TV Crna Gora + Prva World + Puls 4 + Puls 4 + Puls 4 + Quest Red + Quest Red + Quest TV + Quest TV + QVC Beauty + QVC Beauty + QVC Style + QVC Style + Radio Televizija BN + Radio Televizija Budva + Radio Televizija Federacije BIH + RAI 4 + RAI 4 + RAI 5 + RAI 5 + RAI DUE + RAI DUE + RAI TRE + RAI UNO + RAI UNO + Rai Yoyo + Rai Yoyo + Rai Yoyo + Reality Kings + Really UKTV + Really UKTV + Red Carpet TV PL + Red Carpet TV PL + Red Carpet TV PL + RedLight HD + RED tv + Report TV + Revelation TV + Revelation TV + RT Documentary + RTÉ2 + RTÉjr + RTÉ News + RTÉ One + RTK 1 + RTL 2 + RTL + RTL Croatia World + RTL Kockica + RTL Living + RTRS + RTRS PLUS + RTS 1 + RTS 2 + RTS 3 + RTS Drama + RTS Klasika + RTS Kolo + RTS Muzika + RTS Nauka + RTS Poletarac + RTS SVET + RTS Trezor + RTS Život + RTV Most + RTV Novi Pazar + RT Vojvodina 1 + RT Vojvodina 2 + RTV PINK + RTV Slovenija 1 + Russia Today + Russia Today + S4C + S4C + Sat.1 Österreich + Sat.1 Österreich + Sat.1 Österreich + Scan TV + SciFi + Show TV + SinemaTV 2 + SinemaTV 2 + SinemaTV 1001 + SinemaTV 1001 + SinemaTV 1002 + SinemaTV 1002 + SinemaTV + SinemaTV + SinemaTV Aile 2 + SinemaTV Aile 2 + SinemaTV Aile + SinemaTV Aile + SinemaTV Aksiyon 2 + SinemaTV Aksiyon 2 + SinemaTV Aksiyon + SinemaTV Aksiyon + SinemaTV Komedi 2 + SinemaTV Komedi 2 + SinemaTV Komedi + SinemaTV Komedi + SinemaTV Yerli 2 + SinemaTV Yerli 2 + SinemaTV Yerli + SinemaTV Yerli + Sixx AT + Sixx AT + Sixx AT + SK Fight + SK Golf + Sky Arts + Sky Arts + Sky Atlantic + Sky Atlantic + Sky Cinema Action + Sky Cinema Action + Sky Cinema Comedy + Sky Cinema Comedy + Sky Cinema Drama + Sky Cinema Greats + Sky Cinema Hits + Sky Cinema Premiere + Sky Cinema Select + Sky Cinema SF Horror + Sky Crime + Sky Crime + Sky Documentaries + Sky Family + Sky Family + Sky History 2 + Sky History 2 + Sky History + Sky Kids + Sky Kids + Sky Max + Sky Max + Sky Mix + Sky Mix + Sky Nature UK + Sky Nature UK + Sky News + Sky News + Sky News + Sky Replay + Sky Sci-Fi + Sky Sci-Fi + Sky Showcase + Sky Showcase + Sky Sports+ + Sky Sports Action + Sky Sports Cricket + Sky Sports F1 + Sky Sports Football + Sky Sports Golf + Sky Sports Main Event + Sky Sports Mix + Sky Sports News + Sky Sports Premier League + Sky Sports Racing + Sky Sports Racing + Sky Witness + Sky Witness + Smithsonian Channel + Smithsonian Channel + SonLife + SonLife + Sony Channel + Sony Channel + SOS Plus + Sport Klub 1 Slovenija + Sport Klub 3 + STAR Channel + STAR Crime + STAR Life + STAR Movies + Stars TV PL + Stars TV PL + Star TV TR + Star TV TR + Star TV TR + Stinet + Stingray Djazz + Stopklatka + Stopklatka + Stopklatka + Studio B + STV Folk + STV UK + Sundance TV PL + Sundance TV PL + Sundance TV PL + Super Polsat + Super Polsat + Super Polsat + Superstar 2 + Superstar TV + Syri TV + Talking Pictures TV + Talking Pictures TV + Tanjug Tačno + TAY TV + TAY TV + TAY TV + TBN Polska HD + TBN Polska HD + TBN Polska HD + TELE 1 + TELE 1 + TELE 1 + Televizija 24 + Televizija Crne Gore 1 + Televizija Crne Gore 2 + Televizija Crne Gore 3 + Televizija Crne Gore MNE + Televizija Doktor + Televizija TV7 + Televizioni 7 + Telewizja 13 + Telewizja 13 + Telewizja 13 + teve2 + teve2 + TG4 Ireland + TGCOM24 + TGRT Belgesel + TGRT Belgesel + TGRT Belgesel + TGRT EU + That's TV + The History Channel + The History Channel + The Nautical Channel + Tiny Pop TV + Tiny Pop TV + Tip TV + Tivibu Spor + Tivibu Spor + Tivibu Spor + TLC + TLC UK + TLC UK + TNT Sports 1 + TNT Sports 1 + TNT Sports 2 + TNT Sports 2 + TNT Sports 4 + TNT Sports 4 + TNT Sports Europe + TNT Sports Europe + TNT Sports Ultimate + TNT Sports Ultimate + Together + Together + TOGGO plus + Top Channel + Top News + Toxic Folk + Toxic TV + Trace Urban + Trans World Radio + Travel Channel + Tring Smile + TRT 1 + TRT 4K + TRT 4K + TRT 4K + TRT Arapça + TRT Avaz + TRT Belgesel + TRT Çocuk + TRT Diyanet + TRT EBA TV İlkokul + TRT EBA TV İlkokul + TRT EBA TV İlkokul + TRT EBA TV Lise + TRT EBA TV Lise + TRT EBA TV Lise + TRT EBA TV Ortaokul + TRT EBA TV Ortaokul + TRT EBA TV Ortaokul + TRT Haber + TRT Kurdî + TRT Müzik + TRT Spor + TRT Spor Yıldız + TRUE CRIME + TRUE CRIME + TRUE CRIME XTRA + TRUE CRIME XTRA + TTV + TTV + TV 4 + TV 4 + TV5 + TV5 + TV5 + TV5Monde Europe + TV 8.5 + TV 8.5 + TV 8 TR + TV100 TR + TV100 TR + TV100 TR + TV Duga + SAT + TV Dukagjini + TVE Internacional + TVN 7 + TVN 7 + TVN 7 + TVN24 + TVN + TV Net + TV Net + TV Net + TVN Style + TVN Style + TVN Style + TVN Turbo + TVN Turbo + TVN Turbo + TVP 1 + TVP 1 + TVP 2 + TVP 2 + TVP 2 + TVP 3 + TVP 3 + TVP ABC + TVP ABC + TVP HD + TVP HD + TVP HD + TVP Historia + TVP Historia + TVP Info + TVP Info + TVP Info + TV PINK EXTRA + TV PINK PLUS + TVP Kobieta + TVP Kobieta + TVP Kultura + TVP Kultura + TVP Regionalna + TVP Rozrywka + TVP Rozrywka + TVP Seriale + TVP Seriale + TVP Seriale + TV Puls PL + TVP World + TVR PL + TVR PL + TVS + TVS + TV Silesia + TV Silesia + TVT PL + TVT PL + TVT PL + TV Trwam + TV Trwam + TV Trwam + TV Vijesti + U&Alibi + U&Alibi + Uçankuş TV + Uçankuş TV + U&Dave + U&Dave + U&Drama + U&Drama + U&Gold + U&Gold + Ülke TV + Ulster TV + Ulster TV + UNA TV + U&W + U&W + U&Yesterday + U&Yesterday + Vavoom + Vesti + VG TV + Viasat Explore CEE + Viasat History + Viasat Kino Balkan + Viasat Nature CEE + Vikom + Virgin Media Four + Virgin Media More + Virgin Media One + Virgin Media Three + Virgin Media Two + Vivid Red + Vivid TV + Vizion Plus + WION + World Fashion Channel + wPolsce PL + wPolsce PL + wPolsce PL + Wydarzenia 24 + Wydarzenia 24 + Wydarzenia 24 + Xtreme TV + Xtreme TV + XXL + XXL + Yaban TV + Yaban TV + Yaban TV + ZDFinfo + ZDFneo + Zdrava televizija + Zico TV + Наша ТВ + Россиᴙ 24 + Телевизија Храм + 2X2 + 3/24 + 4Fun Dance + 4Fun Kids + 4FunTV + 7 TV Región Murcia + 13 Ulica + #Vamos + A2TV TR + Acción por M+ + Active Family + Adult Channel 2 + Adult Channel + Adventure HD + a Haber + Ale Kino+ + ALFA TVP + Al Jazeera + Alquiler 1 + AMC Break PT + AMC Crime PT + AMC PL + AMC PT + Andalucía TV + a News + Antena.nova + A Para + À Punt ES + Aragón TV + AXN ES + AXN Movies ES + AXN PL + AXN PT + AXN Spin PL + AXN White PL + AXN White PT + BabyTV + BabyTV ES + BBC World News + BBN Türk + Betis TV ES + BFM TV + Bloomberg HT + Bloomberg TV + Boing ES + BOM Cine + BuenViaje + Canal 24H + Canal 33 + Canal+ Domo + CANAL+ Kuchnia + Canal Extremadura + Canal Fútbol Replay + Canal Hollywood PT + Canal J + Canal Parlamento + Canal Sur + Capital XTRA + Casa e Cozinha + Castilla la Mancha TV + Caza y Pesca ES + Cine Español por M+ + Clan TVE + Clásicos por M+ + Club MTV International + CNBC Europe + CNN Europe + Comedia por M+ + Comedy Central ES + Comedy Central PL + COSMO + Crime+Investigation PL + Daystar + DAZN 1 ES + DAZN 2 ES + DAZN 3 ES + DAZN 4 ES + DAZN F1 + DAZN LALIGA 2 + DAZN LALIGA + DBike Channel + Deportes 2 por M+ + Deportes por M+ + Deutsche Welle English + Deutsche Welle Espanol + Disco Polo Music PL + Discovery Animal Planet + Discovery Historia + Disney Junior ES + Dizi Smart Max + Dizi Smart Premium + DMAX ES + DMAX TR + Documentales por M+ + Dorcel TV + Dorcel XXX + Drama por Movistar Plus+ + Dream Türk + Ekotürk + El Garage TV + Ellas Vamos por M+ + El Toro TV + Erox HD + Eroxxx HD + Eska Rock TV + Eska TV + Eska TV Extra + Esport3 + ETB 1 + ETB 2 + ETB 3 + ETB 4 + ETB Basque + Eurochannel + EuroNews + Euronews FR + European League of Football Channel + Eurosport 1 ES + Eurosport 2 ES + EWTN ES + EWTN PL + Extreme Sports Channel + Fashion TV + Feel Good + Fight Klub HD + Fight Time + Filmax + FilmBox Family PL + Film Cafe PL + Fokus TV + Folx TV + FOX NEWS + France24 + FX Comedy PL + FX PL + Golf 2 por M+ + Golf por M+ + GOL PLAY ES + Haber Global + Habitat TV + HispanTV + HIT TV ES + Hustler HD + HUSTLER TV + Iberalia HD CAZA + Iberalia HD PESCA + Iberalia TV + Indie por M+ + Insight TV + iTVN + iTVN Extra + Kanal 7 + KANAL 24 TR + Kanal D TR + Karadeniz TV + Kino Polska + Kino Polska Muzyka + krone.tv + La 1 Cataluña + La 7 Murcia + LA 8 MEDITERRANEO + La dos + LALIGA TV 2 por M+ + LALIGA TV 3 por M+ + LALIGA TV HYPERMOTION 2 + LALIGA TV HYPERMOTION 3 + LALIGA TV HYPERMOTION + LALIGA TV por M+ + La Otra ES + La primera + La Resistencia por M+ + La Rioja + LEVANTE TV + Liga de Campeones 2 por M+ + Liga de Campeones 3 por M+ + Liga de Campeones por M+ + LUXE TV + M+ Deportes 3 + M+ Originales + Melodie TV + Metro TV + Mezzo + Mezzo Live HD + MinikaGO TR + Minimini+ + MOTO ADV + Motorvision Plus + Motorvision Plus International + Motowizja + MovieSmart Classic + MovieSmart Türk + Movistar Estrenos + Movistar Plus+ 2 + Movistar Plus+ + MTV 00s + MTV 80s + MTV 90s International + MTV ES + MTV Hits International + MTV Live HD + MTV Music UK + MyZen TV + Nat Geo Wild ES + National Geographic ES + Nautica TV + NBC News + Nickelodeon ES + Nickelodeon PT + Nick Junior Commercial Light + Novelas+ + Novelas Plus 1 + Nowa TV + Number One Türk + NUTA.TV HD + NUTA GOLD + Odisseia + oe24.TV + Paramount Comedy ES + Paramount Network ES + Planete+ PL + Polonia1 + POLO TV + Polsat 1 + Polsat 2 + Polsat Cafe + Polsat Comedy Central Extra + Polsat Doku + Polsat Film + Polsat Games + Polsat Music + Polsat News 2 + Polsat News + Polsat Play + Polsat Rodzina + Polsat Seriale + Polsat Viasat Explore + Power TV PL + Private TV + RAI 4 + RAI 5 + RAI DUE + RAI TRE + RAI UNO + RAI World Premium + Rai Yoyo + Real Madrid TV + Red Carpet TV PL + RedLight HD + Russia Today ES + Sevilla FC TV + SinemaTV 2 + SinemaTV 1001 + SinemaTV 1002 + SinemaTV + SinemaTV Aile 2 + SinemaTV Aile + SinemaTV Aksiyon 2 + SinemaTV Aksiyon + SinemaTV Komedi 2 + SinemaTV Komedi + SinemaTV Yerli 2 + SinemaTV Yerli + Sky News + Star Channel ES + Stars TV PL + Star TV TR + Stingray Classica + Stingray Djazz + Stopklatka + Sundance TV PL + Super Polsat + SX3 + TAY TV + TBN Polska HD + TCM ES + TELE 1 + TeleDeporte + TELE ELX + TeleMadrid + Televisión de Galicia + Telewizja 13 + teve2 + teve2 TR + TGRT Belgesel + Tivibu Spor + TPA7 + Trece TV + TRT 1 + TRT 2 + TRT 4K + TRT Arapça + TRT Avaz + TRT Belgesel + TRT Çocuk + TRT EBA TV İlkokul + TRT EBA TV Lise + TRT EBA TV Ortaokul + TRT Haber + TRT Kurdî + TRT Müzik + TRT Spor + TRT Turk + TRT World + TTV + TV3 Catalunya + TV3 ES + TV 4 + TV5 + TV5Monde Europe + TV 8.5 + TV100 TR + TV Canaria + TV Chile + TVE Internacional + TVG2 - TV Galicia + TVN 7 + TVN24 + TVN + TV Net + TVN Style + TVN Turbo + TVP 2 + TVP 3 + TVP ABC + TVP HD + TVP Historia + TVP Info + TVP Kobieta + TVP Kultura + TVP Regionalna + TVP Rozrywka + TVP Seriale + TV Puls PL + TVR PL + TVS + TV Silesia + TVT PL + TV Trwam + Uçankuş TV + Vacaciones por M+ + Verdi + VG TV + Vivid Red + Vivid TV + Warner TV ES + Warner TV IT + World Fashion Channel + wPolsce PL + Wydarzenia 24 + Xtreme TV + XXL + Yaban TV + 2X2 + 3/24 + 3 Plus CH + 4Fun Dance + 4Fun Kids + 4FunTV + 4 Plus + 5 Plus + 6 plus + 6ter + 13 Ulica + 360 TV + A2TV TR + AB3 + Action + Active Family + Adult Channel 2 + Adult Channel + Adventure HD + a Haber + Al Arabiya + Ale Kino+ + ALFA TVP + Al Jazeera + AMC PL + a News + A Para + À Punt ES + ARTE DE + ARTE FR + ATV1 + ATV2 + auftanken.TV + Auto Moto FR + AXN PL + AXN Spin PL + AXN White PL + BabyTV + BBC World News + BBN Türk + beIN GURME + beIN iZ + beIN Movies Premiere TR + beIN Movies Turk + beIN Sports 1 FR + beIN Sports 2 FR + beIN Sports 3 FR + beIN Sports MAX 4 FR + beIN Sports MAX 5 FR + beIN Sports MAX 6 FR + beIN Sports MAX 7 FR + beIN Sports MAX 8 FR + beIN Sports MAX 9 FR + beIN Sports MAX 10 FR + Benfica TV + BET FR + Beyaz TV + BFM Business + BFM TV + Bloomberg HT + Bloomberg TV + Boing + Boomerang FR + Boomerang UK + Brazzers TV (ex. Private Spice) + Canal 24H + Canal+ Cinéma(s) FR + CANAL+ DOCS + Canal+ Domo + Canal+ France + CANAL+ GRAND ECRAN + Canal+ Kids FR + CANAL+ Kuchnia + Canal+ Series FR + CANAL+SPORT360 + Canal J + Canal Plus Sport FR + Cartoonito CEE + Cartoon Network + Cartoon Network FR + CCTV 4 Europe + CGTN + CGTN Documentary + Chérie 25 + Ciné+ Classic + Ciné+ Club + Ciné+ Emotion + Ciné+ Famiz + Ciné+ Frisson + Cine+ Premier FR + CNBC Europe + CNews + CNN Europe + CNN Türk TV + Comedie+ + Comedy Central FR + Comedy Central PL + Comedy Central UK + Crime+Investigation PL + Crime District + CStar FR + CStar Hits France + Das Erste + Daystar + Deutsche Welle English + Disco Polo Music PL + Discovery Animal Planet + Discovery Channel FR + Discovery Historia + Discovery Investigation FR + Disney Channel FR + Disney Junior FR + Dizi Smart Max + Dizi Smart Premium + DMAX TR + Dorcel TV + Dorcel XXX + Dream Türk + E! Entertainment + Ekotürk + English Club TV + Erox HD + Eroxxx HD + Eska Rock TV + Eska TV + Eska TV Extra + ETB 1 + ETB 2 + ETB 3 + ETB 4 + ETB Basque + Eurochannel + Eurochannel FR + Euro D + EuroNews + Euronews FR + European League of Football Channel + Eurosport 2 FR + Eurosport FR + Euro Star + EWTN + EWTN PL + Fashion TV + FightBox HD + Fight Klub HD + Filmax + FilmBox Family PL + Film Cafe PL + Fokus TV + Folx TV + FOX NEWS + France 2 + France 3 + France 4 + France 5 + France24 + France24 Arabic + France24 Arabic MENA + France 24 French + France Info + FX Comedy PL + FX PL + Game One+1 + Game One + Ginx Esports TV + Golf Channel Češka + Golf plus + Gulli + Haber Global + Habertürk + Habitat TV + Hustler HD + HUSTLER TV + i24News FR + Infosport+ + Insight TV + iTVN + iTVN Extra + J-One + Kabel Eins + Kanal 7 + KANAL 24 TR + Kanal D TR + Karadeniz TV + KBS World HD + KiKA + Kino Polska + Kino Polska Muzyka + krone.tv + La7 + La Chaîne Info + LCP + L'Equipe + M6 + Mangas + MCM FR + MCM Top + Mediaset Italia + Melodie TV + Metro TV + Mezzo + Mezzo Live HD + MGG TV + MinikaGO TR + Minimini+ + Motorvision Plus France + Motorvision Plus International + Motowizja + MovieSmart Classic + MovieSmart Türk + MTV 00s + MTV 80s + MTV Europe + MTV FR + MTV Hits FR + MTV Hits International + Museum TV + Nat Geo Wild FR + National Geographic FR + NBC News + Nickelodeon FR + Nickelodeon Junior FR + Nickelodeon Teen FR + Novelas+ + Novelas Plus 1 + Novelas TV FR + NOW + Nowa TV + NTV DE + Number One Türk + NUTA.TV HD + NUTA GOLD + OCS Geants + OCS Max + OCS Pulp + oe24.TV + OLTV + Olympia TV + ORF2 + Paramount Channel FR + Paris premiere + PINK Film + Pink Music 1 + Piwi+ + Planète+ + Planète+ Aventure + Planete+ Crime + Planete+ PL + Playboy TV + Polar+ + Polonia1 + POLO TV + Polsat 1 + Polsat 2 + Polsat Cafe + Polsat Comedy Central Extra + Polsat Doku + Polsat Film + Polsat Games + Polsat Music + Polsat News 2 + Polsat News + Polsat Play + Polsat Rodzina + Polsat Seriale + Polsat Viasat Explore + Power Türk TV + Power TV PL + Private TV + PRO 7 Österreich + ProSieben + Puls 4 + Puls 8 + Radio Ticino Channel HD + RAI 4 + RAI 5 + RAI DUE + RAI Education + RAI News 24 + RAI Storia + RAI TRE + RAI UNO + RAI World Premium + Rai Yoyo + Reality Kings + Real Madrid TV + Red Carpet TV PL + RedLight HD + RMC Découverte + RMC Sport 1 + RMC Sport 2 + RMC Sport Live 3 + RMC Sport Live 4 + RMC Sport Live 5 + RMC Sport Live 6 + RMC Sport Live 7 + RMC Sport Live 8 + RMC Sport Live 9 + RMC Sport Live 10 + RMC Sport Live 11 + RMC Sport Live 12 + RMC Sport Live 13 + RMC Sport Live 14 + RMC Sport Live 15 + RMC Sport Live 16 + RMC Sport Live 17 + RMC Story + RSI La 1 + RSI La 2 + RT Documentary + RTL DE + RTL Nitro + RTL Zwei + RTP3 + RTS 2 Suisse + RTS SVET + Russia Today + S1 CH + SAT.1 + Sat.1 Österreich + Seasons + Show Türk + Show TV + SIC + SIC Internacional + SinemaTV 2 + SinemaTV 1001 + SinemaTV 1002 + SinemaTV + SinemaTV Aile 2 + SinemaTV Aile + SinemaTV Aksiyon 2 + SinemaTV Aksiyon + SinemaTV Komedi 2 + SinemaTV Komedi + SinemaTV Yerli 2 + SinemaTV Yerli + Sixx AT + Sky News + SRF 1 + SR Fernsehen + SRF Info + SRF Zwei + Stars TV PL + Star TV TR + Stingray Classica + Stingray CMusic + Stingray Djazz + Stingray iConcerts + Stopklatka + Sundance TV PL + Super Polsat + Super RTL + SWR + SWR Baden-Württemberg + SX3 + TAY TV + TBN Polska HD + TCM Cinéma + TELE 1 + TeleBärn + TeleDeporte + Télétoon+ FR + Televisión de Galicia + Télévision française 1 + Telewizja 13 + Tele Zürich + teve2 + teve2 TR + TF1 Séries Films + TFX + TGCOM24 + TGRT Belgesel + TGRT EU + TGRT HABER + The Nautical Channel + TiJi + Tivibu Spor + TMC + Trace Urban + Travel Channel + TRT 1 + TRT 4K + TRT Arapça + TRT Avaz + TRT Belgesel + TRT Çocuk + TRT Diyanet + TRT EBA TV İlkokul + TRT EBA TV Lise + TRT EBA TV Ortaokul + TRT Haber + TRT Kurdî + TRT Müzik + TRT Spor + TRT Turk + TRT World + TTV + TV3 Catalunya + TV 4 + TV5 + TV5 Monde + TV5Monde Asia + TV5Monde Europe + TV 8.5 + TV 8 TR + TV24 + TV25 + TV100 TR + TV1000 Global Kino + TV Breizh + TVE Internacional + TVG2 - TV Galicia + TVN 7 + TVN + TV Net + TVN Style + TVN Turbo + TVP 1 + TVP 2 + TVP 3 + TVP ABC + TVP HD + TVP Historia + TVP Info + TV PINK EXTRA + TV PINK PLUS + TVP Kobieta + TVP Kultura + TVP Regionalna + TVP Rozrywka + TVP Seriale + TV Puls PL + TVR PL + TVS + TV Silesia + TVT PL + TV Trwam + Uçankuş TV + Ülke TV + Vesti + VG TV + Vivid Red + Vivid TV + Vosges Télévision + VOX + W9 + Warner TV FR + Welt + wPolsce PL + Wydarzenia 24 + Xtreme TV + Yaban TV + ZDF + ZDFinfo + Первый + 2X2 + 3SAT + 13 Ulica + 20 Mediaset + 24 Kitchen + 24Kitchen PT + 360 TuneBox + A2TV TR + Active Family + Adult Channel + Adventure HD + a Haber + Ale Kino+ + ALFA TVP + Al Jazeera + Al Jazeera Balkans + Alternativna televizija Banja Luka + AMC + AMC PL + a News + A Para + Arena Esport + Arena Fight + Arena Sport 1 HR + Arena Sport 2 HR + Arena Sport 3 HR + Arena Sport 4 HR + Arena Sport 5 HR + Arena Sport 6 HR + Arena Sport 7 HR + Arena Sport 8 HR + Arena Sport 9 HR + Arena Sport 10 HR + ARTE DE + ATV1 + ATV2 + Aurora TV + AXN + AXN Black PL + AXN PL + AXN Spin + AXN Spin PL + AXN White PL + B1 TV + B92 + BabyTV + Balkanika TV + Balkan trip + Balkan TV + BBC Earth + BBC First + BBC World News + BBN Türk + Bloomberg Adria + Bloomberg HT + Bloomberg TV + Blue Hustler + BN 2 HD + BN music + Body in Balance + Brazzers TV (ex. Private Spice) + Canal+ Domo + CANAL+ Kuchnia + Cartoonito CEE + Cartoonito UK + Cartoon Network + CBS Reality + CCTV 4 Europe + CGTN + CGTN Documentary + Cinemax 2 + Cinemax + CineStar Action&Thriller + CineStar Premiere 1 + CineStar Premiere 2 + CineStar TV1 + CineStar TV2 + CineStar TV Comedy Family + CineStar TV Fantasy + Club MTV International + CNBC Europe + CNN Europe + Comedy Central DE + Comedy Central PL + Crime+Investigation PL + Crime & Investigation Channel + Croatian Music Channel + Das Erste + Da Vinci Learning + Deluxe Music + Deutsche Welle English + Diadora TV + Disco Polo Music PL + Discovery Animal Planet + Discovery Channel + Discovery Historia + Disney Channel + DIVA (ex. Universal) + DIZI + Dizi Smart Max + Dizi Smart Premium + DMAX DE + DMAX TR + DMC televizija + DM SAT + DOKU TV + Doma TV + Dorcel TV + Dorcel XXX + DOX TV + Dream Türk + ducktv HD + ducktv SD + E! Entertainment + Ekotürk + English Club TV + Epic Drama (CEE) + Erox HD + Eroxxx HD + Eska Rock TV + Eska TV Extra + Eurochannel + EuroNews + European League of Football Channel + Eurosport 1 DE + Eurosport 2 + Eurosport 2 DE + Eurosport.com + Eurosport + EWTN PL + ExtraTV + Extreme Sports Channel + FACE TV + FashionBox HD + Fashion TV + Fast&FunBox HD + FightBox HD + Fight Klub HD + Film4 + Filmax + FilmBox Arthouse + FilmBox Extra RS + FilmBox Family PL + FilmBox Premium RS + FilmBox Stars RS + Film Cafe PL + Fireplace + Fokus TV + Folx TV + Food Network + FOX NEWS + France24 + France 24 French + Fuel TV + FullTV + FunBox UHD + FX Comedy PL + FX PL + GameHub HR + Gametoon HD + Ginx Esports TV + Golica TV + Good Times + GP1 + Grand Televizija + Great! Movies +1 + Great! Movies Action + Great! Movies Classic + Haber Global + Habitat TV + Hajduk TV + HappyTV + Hayat Folk Box + Hayatovci + Hayat TV + HBO2 + HBO3 + HBO + HEMA TV + History Channel 2 + HIT TV + Home & Garden Television + HRT 1 + HRT 2 + HRT 3 + HRT 4 + HRT Int. + HSE + HUSTLER TV + ICT Business + Italia 2 + JimJam + Jugoton TV + K::CN 1 Kopernikus + K::CN 2 Music + K::CN 3 Svet Plus + Kabel Eins + Kabel eins Doku + Kabel eins Österreich + KANAL 24 TR + Kanal Rijeka + Karadeniz TV + KBS World HD + KiKA + Kino Polska + Kino Polska Muzyka + KinoTV + KitchenTV + Klape i Tambure TV + KLASIK + KLASIK HR + krone.tv + La7 + La7d + La Cinque + Laudato TV + Legend + Libertas TV + Lov i ribolov + LUXE TV + M1 Family + M1 FILM + M1 Gold + Maria Vision + MAXSport 1 + MAXSport 2 + MAXSport 3 + MAXSport 4 + MAXSport 5 + Mediaset Italia + Melodie TV + Metro TV + Mezzo + Mezzo Live HD + MG Movie Generation + MinikaGO TR + Minimini+ + MiniTV + Motowizja + MovieSmart Classic + MovieSmart Türk + MrežaZG + MRT SAT + MTV 00s + MTV 80s + MTV 90s International + MTV Europe + MTV Hits International + MTV Live HD + MyZen TV + N1 HR + N24 Doku + Narodna TV + Nat Geo Wild + Nat Geo Wild HD + National Geographic + National Geographic Channel HD + NBA TV + Nickelodeon + Nick Junior + Nick Junior UK + Nick Music + Nicktoons + Nova BH + Nova Plus Cinema + Nova Plus Family + Nova Sport Srbija + NOVA TV + Nova World + Novelas+ + Novelas Plus 1 + NOW + Nowa TV + NTV DE + Number One Türk + NUTA.TV HD + NUTA GOLD + OBN + oe24.TV + O Kanal + One + ORF1 + ORF2 + Osječka televizija + Otvorena televizija + OTV Valentino + Phoenix + Pickbox TV + Pikaboo 2 + Pink BH + Pink Fashion + PINK Film + Pink Folk 1 + Pink Kids + Pink Koncert + Pink M + Pink Music 1 + PINK World + Planete+ PL + Plava televizija + Plava vinkovačka TV + Playboy TV + Poljoprivredna TV + Polonia1 + POLO TV + Polsat 2 + Polsat Cafe + Polsat Comedy Central Extra + Polsat Doku + Polsat Film + Polsat Games + Polsat Music + Polsat News 2 + Polsat News + Polsat Play + Polsat Rodzina + Polsat Seriale + Polsat Viasat Explore + Polsat Viasat History + Polsat Viasat Nature + Pop Max + Power TV PL + Private TV + PRO 7 Österreich + ProSieben + ProSieben MAXX + Prva Plus + Prva Srpska TV + QVC Deutschland + Radiotelevizija Banovina + Radio Televizija BIH + Radio Televizija BN + Radio Televizija Federacije BIH + RAI 4 + RAI 5 + RAI DUE + RAI Education + RAI News 24 + RAI Sport 1 + RAI Storia + RAI TRE + RAI UNO + Rai Yoyo + Reality Kings + Red Carpet TV PL + RedLight HD + RED tv + RiC DE + RTL 2 + RTL + RTL Adria + RTL Crime + RTL DE + RTL Kockica + RTL Living + RTL Passion + RTLup + RTL Zwei + RTRS + RTS 1 + RTS 2 + RTS 3 + RTS SVET + RTV Herceg-Bosne + RT Vojvodina 1 + RT Vojvodina 2 + RTV PINK + RTV Slovenija 1 + RTV Slovenija 2 + RTV Slovenija 3 + Russia Today + Saborska TV + Samobor TV + SAT.1 + Sat.1 Gold + Sat.1 Österreich + SciFi + ServusTV + Show TV + SinemaTV 2 + SinemaTV 1001 + SinemaTV 1002 + SinemaTV + SinemaTV Aile 2 + SinemaTV Aile + SinemaTV Aksiyon 2 + SinemaTV Aksiyon + SinemaTV Komedi 2 + SinemaTV Komedi + SinemaTV Yerli 2 + SinemaTV Yerli + Sixx AT + SK Fight + SK Golf + Sky News + SkyShowtime 1 Nordic + Slavonskobrodska Televizija + sonnenklar.TV + Sony Channel + SOS Plus + Sport 1 DE + Sport Klub 1 Hrvatska + Sport Klub 2 Hrvatska + Sport Klub 3 + Sport Klub 4 + Sport Klub 5 + Sport Klub 6 + Sportska Televizija + STAR Channel + STAR Crime + STAR Life + STAR Movies + Star TV TR + Stingray Classica + Stingray CMusic + Stingray Djazz + Stingray iConcerts + Stopklatka + Studio B + Sundance TV PL + Super Polsat + Super RTL + Supertennis HD + Tanjug Tačno + TAY TV + TBN Polska HD + TELE 1 + Tele 5 DE + Telequattro + Televizija 24 + Televizija Alfa + Televizija Crne Gore MNE + Televizija Dalmacija + Televizija Zapad Zaprešić + Telewizja 13 + teve2 + teve2 TR + TGRT Belgesel + The History Channel + The Nautical Channel + TiJi + Tiny Pop TV + Tivibu Spor + TLC + TOGGO plus + Toon kids + Toxic Folk + Toxic Rap + Toxic TV + Trace Urban + Travel Channel + Trend TV + TRT 4K + TRT EBA TV İlkokul + TRT EBA TV Lise + TRT EBA TV Ortaokul + TTV + TV 4 + TV5 + TV5 Monde + TV5Monde Europe + TV 8.5 + TV 8 TR + TV100 TR + TVE Internacional + TV Jadran + TVN 7 + TVN24 + TV Net + TV NOVA Pula + TVN Style + TVN Turbo + TVP 2 + TVP 3 + TVP ABC + TVP HD + TVP Historia + TVP Info + TV PINK EXTRA + TV PINK PLUS + TVP Kobieta + TVP Kultura + TVP Regionalna + TVP Rozrywka + TVP Seriale + TV Puls PL + TVR PL + TVS + TV Silesia + TV Slavonije i Baranje + TVT PL + TV Trwam + TV Vijesti + TV Zelina + Uçankuş TV + UNA TV + Varaždinska Televizija + Vavoom + Vesti + VG TV + Viasat Explore CEE + Viasat History + Viasat Kino Balkan + Viasat Nature CEE + Viasat True Crime + Vivid Red + Vivid TV + Vizion Plus + VOX + Welt + Wydarzenia 24 + Xtreme TV + XXL + Yaban TV + Zagrebačka Televizija + ZDF + ZDFinfo + ZDFneo + Zdrava televizija + ZONASPORT TV + 2X2 + 4Fun Dance + 4Fun Kids + 4FunTV + 13 Ulica + 360 TuneBox + A2TV TR + Active Family + Adult Channel 2 + Adult Channel + Adventure HD + a Haber + Ale Kino+ + ALFA TVP + AMC HU + AMC PL + a News + A Para + Apostol TV + ARD-alpha + Arena4 HU + ATV1 + ATV2 + ATV HU + ATV Spirit HU + AXN HU + AXN PL + AXN Spin PL + AXN White PL + BabyTV + BBC News Channel + BBC World News + BBN Türk + Bloomberg HT + Bloomberg TV + Body in Balance + Canal+ Domo + Canal+ France + CANAL+ Kuchnia + Cartoonito CEE + CBS Reality + CCTV 4 Europe + Cinemax 2 HU + Cinemax HU + Club MTV International + CNN Europe + Comedy Central HU + Comedy Central PL + CoolTV + Crime+Investigation PL + D1 TV HU + Das Erste + Da Vinci Learning + Deutsche Welle English + Disco Polo Music PL + Discovery Channel HU + Discovery Historia + Disney Channel HU + Dizi Smart Max + Dizi Smart Premium + DMAX TR + Dorcel XXX + Dream Türk + ducktv HD + ducktv SD + Duna TV + Duna World + E! Entertainment + Ekotürk + English Club TV + Epic Drama (CEE) + Erox HD + Eroxxx HD + Eska Rock TV + Eska TV + Eska TV Extra + European League of Football Channel + Extreme Sports Channel + FashionBox HD + Fashion TV + Fast&FunBox HD + Fehérvár TV + FightBox HD + Fight Klub HD + Film4 HU + Film+ HU + Filmax + FilmBox Arthouse + Filmbox Extra HU + Filmbox Family HU + FilmBox HU + Filmbox Premium HU + Filmbox Stars HU + Film Cafe HU + Film Cafe PL + Film Mania HU + Fokus TV + Folx TV + Food Network + France24 + FunBox UHD + FX Comedy PL + FX PL + Galaxy4 + Gametoon HD + H!T Music Channel RO + Haber Global + Habitat TV + HBO 2 HU + HBO 3 HU + HBO + HBO HU + Hír TV + History Channel 2 + History Channel HU + Home & Garden Television + Home & Garden Television UK + Hustler HD + HUSTLER TV + Investigation Discovery + Investigation Discovery UK + Izaura TV + Jazz TV HU + JimJam + JockyTV + Kanal 7 + KANAL 24 TR + Kanal D TR + Karadeniz TV + KiKA + Kino Polska + Kino Polska Muzyka + Kölyökklub + krone.tv + Life TV HU + LUXE TV + M2 Petőfi + M4 sport + M4 Sport Plus + m5 + Magyar Mozi TV + Magyar Sláger TV + Magyar Televízió 1 + Magyar Televízió 3 + MATCH4 + MAX4 + Mediaset Italia + Melodie TV + Metro TV + Mezzo + Mezzo Live HD + MinikaGO TR + Minimax + Minimax HU + Minimini+ + Motorvision Plus International + Motowizja + MovieSmart Classic + MovieSmart Türk + Moziklub + Mozi plusz TV + Moziverzum + MTV 00s + MTV 80s + MTV 90s International + MTV Europe HU + MTV Hits International + MTV Live HD + Muzsika TV + MyZen TV + Nat Geo Wild HU + National Geographic HU + Nickelodeon Commercial + Nick Junior PL + Nicktoons PL + Novelas+ + Novelas Plus 1 + NOW + Nowa TV + Number One Türk + NUTA.TV HD + NUTA GOLD + oe24.TV + ORF3 + Ozone Network + Paramount Network HU + Planete+ PL + Playboy TV + Polonia1 + POLO TV + Polsat 2 + Polsat Cafe + Polsat Comedy Central Extra + Polsat Doku + Polsat Film + Polsat Games + Polsat Music + Polsat News 2 + Polsat News + Polsat Play + Polsat Rodzina + Polsat Seriale + Polsat Viasat Explore + Polsat Viasat History + Polsat Viasat Nature + Power TV PL + Prime + Private TV + PRO 7 Österreich + ProSieben + Puls 4 + PΛX + RAI TRE + RAI UNO + Rai Yoyo + Red Carpet TV PL + RedLight HD + Romance TV PL + RTL DE + RTL Gold + RTL Három + RTL HU + RTL Ketto + RTL Otthon + RTL Zwei + SAT.1 + Sat.1 Österreich + Show TV + SinemaTV 2 + SinemaTV 1001 + SinemaTV 1002 + SinemaTV + SinemaTV Aile 2 + SinemaTV Aile + SinemaTV Aksiyon 2 + SinemaTV Aksiyon + SinemaTV Komedi 2 + SinemaTV Komedi + SinemaTV Yerli 2 + SinemaTV Yerli + Sixx AT + Sky News + SkyShowtime 1 Nordic + Sorozat+ + Sorozatklub + Spektrum Home HU + Spektrum TV + Spíler1 TV + Spíler2 TV + Sport 1 HU + Sport 2 HU + Stars TV PL + Star TV TR + Stingray Classica + Stingray Djazz + Stingray iConcerts + Stopklatka + Story4 + Sundance TV PL + Super Polsat + Super RTL + Super TV2 + TAY TV + TBN Polska HD + TeenNick + TELE 1 + Telewizja 13 + teve2 + teve2 TR + TGRT Belgesel + The Fishing and Hunting + The History Channel + Tivibu Spor + Travel Channel + TRT 4K + TRT EBA TV İlkokul + TRT EBA TV Lise + TRT EBA TV Ortaokul + TRT World + TTV + TV2 Comedy + TV2 HU + TV2 Kids + TV2 Klub + TV2 Séf + TV 4 + TV4 HU + TV5 + TV 8.5 + TV 8 TR + TV100 TR + TVE Internacional + TVN 7 + TVN24 + TV Net + TVN Style + TVN Turbo + TVP 2 + TVP 3 + TVP ABC + TV Paprika HU + TVP HD + TVP Historia + TVP Info + TVP Kobieta + TVP Kultura + TVP Regionalna + TVP Rozrywka + TVP Seriale + TV Puls PL + TVR PL + TVS + TV Silesia + TVT PL + TV Trwam + Uçankuş TV + UTV RO + Viasat 2 + Viasat3 + Viasat 6 + Viasat Film HU + Viasat History + Viasat Nature CEE + Vivid Red + Vivid TV + VOX + World Fashion Channel + wPolsce PL + Wydarzenia 24 + Xtreme TV + Yaban TV + ZDF + Zenebutik + 2X2 + 3 Plus CH + 4Fun Dance + 4Fun Kids + 4FunTV + 4 Plus + 5 Plus + 6 plus + 7 Gold + 13 Ulica + 20 Mediaset + A2TV TR + Active Family + Adult Channel 2 + Adult Channel + Adventure HD + a Haber + Ale Kino+ + ALFA TVP + Alma TV + AMC PL + a News + A Para + À Punt ES + ATV1 + ATV2 + ATV TR + auftanken.TV + AXN PL + AXN Spin PL + AXN White PL + BabyTV + BBN Türk + BFM TV + Bloomberg HT + Body in Balance + Boing + Boing Plus + Boomerang IT + Canal+ Domo + CANAL+ Kuchnia + Canale 5 + Canale 7 + Canal J + Cartoonito Italia + Cartoon Network IT + CI Crime+ Investigation + cielo + Cine34 + Class TV Moda + Comedy Central PL + Crime+Investigation PL + Daystar + DeA Junior + DeA Kids + Disco Polo Music PL + Discovery Animal Planet + Discovery Channel IT + Discovery Historia + Dizi Smart Max + Dizi Smart Premium + DMAX ES + DMAX IT + DMAX TR + Dream Türk + Ekotürk + Erox HD + Eroxxx HD + Eska Rock TV + Eska TV + Eska TV Extra + European League of Football Channel + Eurosport 2 IT + Eurosport IT + EWTN PL + Fight Klub HD + Filmax + FilmBox Family PL + Film Cafe PL + Focus TV + Fokus TV + Folx TV + Food Network Italia + Frisbee + FX Comedy PL + FX PL + Gambero Rosso Channel + Giallo TV + Haber Global + Habitat TV + History Channel IT + Hustler HD + HUSTLER TV + Insight TV + Inter TV + Italia 1 + Italia 2 + iTVN + iTVN Extra + K2 + Kanal 7 + KANAL 24 TR + Kanal D TR + Karadeniz TV + Kino Polska + Kino Polska Muzyka + krone.tv + La7 + La7 AU + La7d + La Chaîne Info + La Cinque + Lazio Style Channel + LUXE TV + Marco Polo TV + Mediaset Extra + Mediaset Italia + Mediaset Italia AU + Melodie TV + Metro TV + Mezzo Live HD + Milan TV + MinikaGO TR + Minimini+ + MotorTrend + Motowizja + MovieSmart Classic + MovieSmart Türk + MTV 90s International + MTV Hits International + MTV Italia + MTV Music IT + MTV Music UK + MyZen TV + National Geographic + NBC News + Nickelodeon IT + Nick Junior IT + NOVE + Novelas+ + Novelas Plus 1 + NOW + Nowa TV + Number One Türk + NUTA.TV HD + NUTA GOLD + oe24.TV + ORF1 + ORF2 + ORF3 + Parole di Vita + Planete+ PL + Playboy TV + Polonia1 + POLO TV + Polsat 1 + Polsat 2 + Polsat Cafe + Polsat Comedy Central Extra + Polsat Doku + Polsat Film + Polsat Games + Polsat Music + Polsat News 2 + Polsat News + Polsat Play + Polsat Rodzina + Polsat Seriale + Polsat Viasat Explore + Power TV PL + Private TV + PRO 7 Österreich + Puls 4 + Puls 8 + Radiofreccia + Radio Italia TV + RAI 3 Bis + RAI 4 + RAI 5 + RAI DUE + RAI Education + RAI Gulp + RAI News 24 + RAI Sport 1 + RAI Storia + RAI TRE + RAI UNO + RAI World Premium + Rai Yoyo + real time + Red Carpet TV PL + RedLight HD + Rete 4 + RSI La 1 + RSI La 2 + RTS 2 Suisse + RTS Un + S1 CH + Sat.1 Österreich + Show TV + SinemaTV 2 + SinemaTV 1001 + SinemaTV 1002 + SinemaTV + SinemaTV Aile 2 + SinemaTV Aile + SinemaTV Aksiyon 2 + SinemaTV Aksiyon + SinemaTV Komedi 2 + SinemaTV Komedi + SinemaTV Yerli 2 + SinemaTV Yerli + Sixx AT + Sky Arte + Sky Caccia e Pesca + Sky Cinema Action IT + Sky Documentaries IT + Sky Investigation + Sky Nature IT + Sky Pesca e Caccia + Sky Sport 1 IT + Sky Sport 3 IT + Sky Sport24 HD + Sky Sport 251 + Sky Sport 252 + Sky Sport 253 + Sky Sport 254 + Sky Sport 255 + Sky Sport 257 + Sky Sport 258 + Sky Sport 259 + Sky Sport Calcio + Sky Sport F1 IT + Sky Sport MotoGP IT + Sky Sport Plus IT + Sky TG24 HD + Sky Uno + Sportitalia + SRF 1 + SRF Info + SRF Zwei + Stars TV PL + Star TV TR + Stopklatka + Sundance TV PL + Super! + Super Polsat + Supertennis HD + TAY TV + TBN Polska HD + TELE 1 + TeleBärn + Telequattro + Teletutto + Telewizja 13 + Tele Zürich + teve2 + teve2 TR + TGCOM24 + TGRT Belgesel + Tivibu Spor + Top Calcio 24 + Top Crime + TRT 1 + TRT 2 + TRT 4K + TRT Arapça + TRT Avaz + TRT Belgesel + TRT Çocuk + TRT EBA TV İlkokul + TRT EBA TV Lise + TRT EBA TV Ortaokul + TRT Haber + TRT Kurdî + TRT Müzik + TRT Spor + TRT Turk + TRT World + TTV + TV 4 + TV5 + TV 8.5 + TV8 IT + TV24 + TV25 + TV100 TR + TV 2000 + TVE Internacional + TVN 7 + TVN24 + TVN + TV Net + TVN Style + TVN Turbo + TVP 1 + TVP 2 + TVP 3 + TVP ABC + TVP HD + TVP Historia + TVP Info + TVP Kobieta + TVP Kultura + TVP Regionalna + TVP Rozrywka + TVP Seriale + TV Puls PL + TVR PL + TVS + TV Silesia + TVT PL + TV Trwam + Twenty Seven + Uçankuş TV + VG TV + Vivid Red + Vivid TV + Warner TV IT + Welt + World Fashion Channel + wPolsce PL + Wydarzenia 24 + Xtreme TV + XXL + Yaban TV + 2X2 + 4Fun Dance + 4Fun Kids + 13 Ulica + 24 Kitchen + 24Kitchen PT + 360 TV + A2TV TR + ABC News Albania + Active Family + Adventure HD + Agro TV + a Haber + Ale Kino+ + ALFA TV + ALFA TVP + Al Jazeera + Al Jazeera Balkans + Alternativna televizija Banja Luka + AMC + AMC PL + a News + Anixe HD Serie + A Para + Arena Esport + Arena Fight + Arena Sport 1 MK + Arena Sport 1 Premium + Arena Sport 2 Premium + Arena Sport 2 RS + Arena Sport 3 Premium + Arena Sport 3 RS + Arena Sport 4 RS + Arena Sport 5 RS + ARTE DE + ATV1 + ATV2 + ATV Avrupa + AXN + AXN Black PL + AXN PL + AXN Spin + AXN Spin PL + AXN White PL + Balkanika TV + Balkan trip + Bang Bang + BBC First + BBC World News + BBN Türk + Bloomberg Adria + Bloomberg HT + Bloomberg TV + BN 2 HD + BN music + Brainz TV + Brazzers TV (ex. Private Spice) + CANAL+ Kuchnia + Cartoonito CEE + Cartoon Network + CBS Reality + Cinemania TV + Cinemax 2 + Cinemax + CineStar Premiere 1 + CineStar Premiere 2 + CineStar TV2 + Club MTV International + CNBC Europe + CNN Europe + Comedy Central PL + Crime+Investigation PL + Crime & Investigation Channel + Croatian Music Channel + Cufo TV + Das Erste + Da Vinci Learning + Deluxe Music + Deutsche Welle English + DigitAlb Melody TV + Disco Polo Music PL + Discovery Animal Planet + Discovery Channel + Discovery Historia + Discovery Science + Disney Channel + Disney Junior + DIVA (ex. Universal) + DIZI + Dizi Smart Max + Dizi Smart Premium + DMAX TR + DM SAT + Dream Türk + ducktv SD + E! Entertainment + Ekotürk + Epic Drama (CEE) + Erox HD + Eroxxx HD + Eska Rock TV + Eska TV + Eska TV Extra + Eurochannel + Euro D + EuroNews + Euronews Albania + European League of Football Channel + Eurosport 2 + Eurosport + Euro Star + EWTN PL + Explorer Histori + Explorer Natyra + Explorer Shkence + Extreme Sports Channel + FACE TV + FashionBox HD + Fashion TV + Fast&FunBox HD + FightBox HD + Fight Klub HD + Filmax + FilmBox Arthouse + FilmBox Extra RS + FilmBox Family PL + FilmBox Stars RS + Film Cafe PL + Fokus TV + Folx TV + Food Network + France24 + France 24 French + FX Comedy PL + FX PL + Ginx Esports TV + Grand Televizija + Haber Global + Habitat TV + HappyTV + Hayat 2 + Hayat Folk Box + Hayat Music Box + Hayatovci + Hayat TV + HBO2 + HBO3 + HBO + HRT 1 + HRT 3 + HRT 4 + HUSTLER TV + IDJ World + JimJam + Jugoton TV + Junior TV + K::CN 1 Kopernikus + K::CN 2 Music + K::CN 3 Svet Plus + Kabel Eins + Kanal1 + Kanal 7 + Kanal 10 + KANAL 24 TR + Kanal D + Kanal D TR + Karadeniz TV + KiKA + Kino Polska + Kino Polska Muzyka + KitchenTV + Klan Macedonia + Klan TV HD + KLASIK + krone.tv + Living HD + Lov i ribolov + M1 Family + M1 Film MK + M1 Gold MK + Melodie TV + Mezzo + Mezzo Live HD + MinikaGO TR + Minimax + Minimini+ + Motowizja + MovieSmart Classic + MovieSmart Türk + MRT SAT + MTM Televizija + MTV 00s + MTV 80s + MTV 90s International + MTV Europe + MTV Hits International + MTV Live HD + MUSE + My Music + N1 RS + Nat Geo Wild + National Geographic + National Geographic Channel HD + News 24 + Nickelodeon Commercial + Nick Junior + Nicktoons + Nova S + Nova Sport Srbija + Novelas+ + Novelas Plus 1 + NOW + Nowa TV + Number One Türk + NUTA.TV HD + NUTA GOLD + OBN + oe24.TV + O Kanal + Ora News + Pickbox TV MK + Pikaboo + Pink Music 1 + Pink Serije + Pink Show + Planeta TV BG + Planete+ PL + Playboy TV + Polonia1 + POLO TV + Polsat 2 + Polsat Cafe + Polsat Comedy Central Extra + Polsat Doku + Polsat Film + Polsat Music + Polsat News 2 + Polsat News + Polsat Play + Polsat Rodzina + Polsat Seriale + Polsat Viasat Explore + Polsat Viasat History + Polsat Viasat Nature + Power TV PL + Private TV + PRO 7 Österreich + ProSieben + Prva FILES + Prva KICK + Prva LIFE + Prva MAX + Prva Plus + Prva Srpska TV + Prva World + Puls 4 + Radio Televizija BN + Radio Televizija Federacije BIH + RAI DUE + RAI Sport 1 + RAI Storia + RAI TRE + RAI UNO + Rai Yoyo + Red Carpet TV PL + RedLight HD + Report TV + RT Documentary + RTK 1 + RTK 2 + RTK 4 + RTL 2 + RTL + RTL DE + RTL Kockica + RTL Living + RTL Zwei + RTRS + RTS 1 + RTS 2 + RTS 3 + RTSH 1 + RTS SVET + RTV21 + RTV 21 Popullore + RTV Besa + RTV Novi Pazar + RTV Slon Tuzla + RTV Slovenija 1 + RTV Slovenija 2 + RTV Slovenija 3 + Russia Today + SAT.1 + Sat.1 Österreich + Scan TV + SciFi + Shenja TV + Show TV + SinemaTV 2 + SinemaTV 1001 + SinemaTV 1002 + SinemaTV + SinemaTV Aile 2 + SinemaTV Aile + SinemaTV Aksiyon 2 + SinemaTV Aksiyon + SinemaTV Komedi 2 + SinemaTV Komedi + SinemaTV Yerli 2 + SinemaTV Yerli + Sixx AT + SK Fight + SK Golf + Sky News + SOS Plus + Sport 1 DE + Sport Klub 1 Hrvatska + Sport Klub 2 Hrvatska + Sport Klub 3 + STAR Channel + STAR Crime + STAR Life + STAR Movies + Stars TV PL + Star TV TR + Stinet + Stingray CMusic + Stopklatka + Studio B + STV Folk + Sundance TV PL + Super Polsat + Super RTL + Superstar 2 + Superstar TV + Syri TV + Tanjug Tačno + TAY TV + TBN Polska HD + TELE 1 + Televizija 24 + Televizija Crne Gore MNE + Televizioni 7 + Telewizja 13 + Telma + Tera TV + teve2 + teve2 TR + TGRT Belgesel + TGRT EU + The Fishing and Hunting + The History Channel + Tip TV + Tivibu Spor + TLC + Top Channel + Top News + Toxic Folk + Toxic Rap + Toxic TV + Trace Urban + Travel Channel + Travelxp + Tring Bunga Bunga + Tring Desire + Tring Smile + TRT 1 + TRT 4K + TRT EBA TV İlkokul + TRT EBA TV Lise + TRT EBA TV Ortaokul + TRT Spor Yıldız + TRT Turk + TRT World + TTV + TV 4 + TV5 + TV5 Monde + TV 8.5 + TV 8 TR + TV100 TR + TV Duga + SAT + TV Dukagjini + TV Edo + TV Kiss Menada + TVM Ohrid + TVN 7 + TV Net + TVN Style + TVN Turbo + TVP 2 + TVP 3 + TVP ABC + TVP HD + TVP Historia + TVP Info + TVP Kobieta + TVP Kultura + TVP Rozrywka + TVP Seriale + TVR PL + TVS + TV Sarajevo + TV Silesia + TVT PL + TV Trwam + Uçankuş TV + Ülke TV + Valentino Music HD + Vavoom + Viasat Explore CEE + Viasat History + Viasat Kino Balkan + Viasat Nature CEE + Vikom + Vizion Plus + VOX + Welt + Wness TV + World Fashion Channel + wPolsce PL + Wydarzenia 24 + Xtreme TV + XXL + Yaban TV + ZDF + Zdrava televizija + Алсат М + Канал 5 + Канал 8 + МРТ 1 + МРТ 2 + МРТ 3 + МРТ 4 + МРТ 5 + Наша ТВ + Россиᴙ 24 + Сител + ТВ Сонце + 1-2-3.tv + 2X2 + 3SAT + 4Fun Dance + 4Fun Kids + 4FunTV + 13 Ulica + 360 TuneBox + A2TV TR + Active Family + Adult Channel 2 + Adult Channel + Adventure HD + a Haber + Ale Kino+ + ALFA TVP + Al Jazeera + AMC PL + a News + Animal Planet PL + Anixe HD Serie + Antena HD + A Para + ARD-alpha + ARTE DE + ARTE FR + ATV1 + ATV2 + AXN Black PL + AXN PL + AXN Spin PL + AXN White PL + BabyTV + BabyTV PL + Bayerischen Fernsehen Nord + BBC Brit PL + BBC Earth PL + BBC First PL + BBC Lifestyle PL + BBC News Channel + BBC World News + BBN Türk + Belsat TV + Beyaz TV + BFM TV + Bibel TV + Biznes24 + Bloomberg HT + Bloomberg TV + Blue Hustler + Body in Balance + Bollywood PL + Brazzers TV (ex. Private Spice) + BR Fernsehen Süd + CANAL+ 4K Ultra HD + CANAL+ Dokument + Canal+ Domo + Canal+ Extra 1 PL + Canal+ Extra 2 PL + Canal+ Extra 3 PL + Canal+ Extra 4 PL + Canal+ Extra 5 PL + Canal+ Extra 6 PL + CANAL+ Family PL + CANAL+ Film PL + Canal+ France + CANAL+ Kuchnia + CANAL+ Now PL + CANAL+ Premium PL + CANAL+ Seriale PL + Canal+ Series FR + CANAL+ Sport 2 + CANAL+ Sport 3 + CANAL+ Sport 4 + CANAL+ Sport 5 + CANAL+ Sport PL + Cartoonito CEE + Cartoonito UK + Cartoon Network PL + CBeebies PL + CBS Reality PL + CGTN Documentary + Ciné+ Club + Ciné+ Emotion + Ciné+ Famiz + Cinemax 2 PL + Cinemax PL + Club MTV International + CNBC Europe + CNews + CNN Europe + Comedy Central DE + Comedy Central PL + COSMO + Crime+Investigation PL + ČT 1 + ČT 2 + ČT 24 + ČT :D + ČT Sport + Das Bild TV + Das Erste + Das Health TV + Da Vinci Learning PL + Deluxe Music + Deutsche Welle English + Disco Polo Music PL + Discovery Animal Planet + Discovery Channel PL + Discovery Historia + Discovery Life PL + Discovery Science PL + Disney Channel PL + Disney Junior Polska + Disney XD PL + Dizi Smart Max + Dizi Smart Premium + DMAX DE + DMAX TR + Dorcel TV + Dorcel XXX + Dream Türk + DTX PL + ducktv HD + ducktv Plus + E! Entertainment + E-Sport HD + Ekotürk + Eleven Sports 1 PL + Eleven Sports 2 PL + Eleven Sports 3 PL + Eleven Sports 4 PL + Epic Drama (Poland) + Eska Rock TV + Eska TV + Eska TV Extra + Eurochannel + EuroNews + Euronews FR + European League of Football Channel + Eurosport 1 PL + Eurosport 2 PL + EWTN + EWTN PL + EXTASY TV + Extreme Sports PL + FashionBox HD + Fashion TV + Fast&FunBox HD + FightBox HD + Fight Klub HD + Filmax + FilmBox Action PL + FilmBox Arthouse + FilmBox Extra PL + FilmBox Family PL + FilmBox Premium PL + Film Cafe PL + Fokus TV + Folx TV + Food Network PL + France 2 + France 3 + France 4 + France 5 + France24 + France Info + FunBox UHD + FX Comedy PL + FX PL + Gametoon HD + Ginx Esports TV + Golf Channel PL + Haber Global + Habitat TV + HBO 2 PL + HBO 3 PL + HBO PL + HGTV PL + History 2 Polska + History Channel Polska + HR Fernsehen + HSE24 Extra + HSE + Hustler HD + HUSTLER TV + Insight TV + Investigation Discovery PL + Italia 2 + iTVN + iTVN Extra + JimJam PL + K-TV Katholisches Fernsehen + Kabel Eins + Kabel eins Doku + Kabel eins Österreich + Kanal 7 + KANAL 24 TR + Kanal D TR + Karadeniz TV + KiKA + Kino Polska + Kino Polska Muzyka + KinoTV PL + krone.tv + La Chaîne Info + LCP + Leo TV + Love Nature US + LUXE TV + MDR Sachsen-Anhalt + MDR Sachsen + MDR Thüringen + Melodie TV + Metro TV + Mezzo + Mezzo Live HD + MinikaGO TR + Minimini+ + Motorvision Plus International + Motowizja + MovieSmart Classic + MovieSmart Türk + MTV 00s + MTV 80s + MTV 90s International + MTV DE + MTV Europe + MTV Hits International + MTV Live HD + MTV PL + münchen.tv + Music Box Polska + MyZen TV + N24 Doku + National Geographic People PL + National Geographic PL + National Geographic Wild PL + NDR Niedersachsen + News 24 + Nick DE + Nickelodeon PL + Nickelodeon Ukraine Pluto + Nick Junior PL + Nick Music + Nicktoons PL + Niederbayern TV Deggendorf-Straubing + Novelas+ + Novelas Plus 1 + Novela TV + NOW + Nowa TV + NTV DE + Number One Türk + NUTA.TV HD + NUTA GOLD + oe24.TV + One + ORF2 + Paramount Network PL + Parole di Vita + Phoenix + Planete+ PL + Playboy TV + Polonia1 + POLO TV + Polsat 1 + Polsat 2 + Polsat + Polsat Cafe + Polsat Comedy Central Extra + Polsat Doku + Polsat Film + Polsat Games + Polsat Music + Polsat News 2 + Polsat News + Polsat News Polityka + Polsat Play + Polsat Rodzina + Polsat Seriale + Polsat Sport 1 + Polsat Sport 2 + Polsat Sport 3 + Polsat Sport Fight + Polsat Sport Premium 1 + Polsat Sport Premium 2 + Polsat Sport Premium 3 PPV + Polsat Sport Premium 4 PPV + Polsat Sport Premium 5 PPV + Polsat Sport Premium 6 PPV + Polsat Viasat Explore + Polsat Viasat History + Polsat Viasat Nature + Power TV PL + Private TV + PRO 7 Österreich + Proart + ProSieben + ProSieben MAXX + Puls 4 + QVC Deutschland + Radiofreccia + Radio Italia TV + RAI World Premium + RBB Fernsehen Berlin + RBB Fernsehen Brandenburg + Reality Kings + Red Carpet TV PL + RedLight HD + Romance TV PL + RT Documentary + RTL DE + RTL Nitro + RTL Zwei + Russia Today + SAT.1 + Sat.1 Gold + Sat.1 Österreich + SciFi + ServusTV + Show TV + SinemaTV 2 + SinemaTV 1001 + SinemaTV 1002 + SinemaTV + SinemaTV Aile 2 + SinemaTV Aile + SinemaTV Aksiyon 2 + SinemaTV Aksiyon + SinemaTV Komedi 2 + SinemaTV Komedi + SinemaTV Yerli 2 + SinemaTV Yerli + Sixx AT + Sky News + SkyShowtime 1 Nordic + SkyShowtime 2 Nordic + Sky Sport News DE + sonnenklar.TV + Sport 1 DE + Sportklub PL + SR Fernsehen + SRF Info + Stars TV PL + Star TV TR + Stingray Classica + Stingray CMusic + Stingray Djazz + Stingray iConcerts + Stopklatka + Sundance TV PL + Super Polsat + Super RTL + SWR Baden-Württemberg + tagesschau24 + TAY TV + TBN Polska HD + TeenNick + TELE 1 + Tele 5 DE + Tele 5 PL + teleTOON+ + Telewizja 13 + teve2 + teve2 TR + TGRT Belgesel + The Nautical Channel + Tivibu Spor + TLC PL + TOGGO plus + Top Kids PL + Toya TV + Trace Urban + Travel Channel PL + Travelxp + TRT 4K + TRT Arapça + TRT Diyanet + TRT EBA TV İlkokul + TRT EBA TV Lise + TRT EBA TV Ortaokul + TTV + TV 4 + TV5 + TV5Monde Europe + TV6 PL + TV 8.5 + TV 8 TR + TV100 TR + TVC PL + TVE Internacional + TVN 7 + TVN24 + TVN24 BiS + TVN + TV Net + TVN Fabuła + TVN Style + TVN Turbo + TVP 1 + TVP 2 + TVP 3 + TVP3 Białystok + TVP3 Bydgoszcz + TVP3 Gdańsk + TVP3 Katowice + TVP3 Kielce + TVP3 Kraków + TVP3 Łódź + TVP3 Lublin + TVP3 Olsztyn + TVP3 Opole + TVP3 Poznań + TVP3 Rzeszów + TVP3 Szczecin + TVP3 Warszawa + TVP3 Wrocław + TVP ABC 2 + TVP ABC + TVP Dokument + TVP HD + TVP Historia 2 + TVP Historia + TVP Info + TVP Kobieta + TVP Kultura 2 + TVP Kultura + TVP Nauka + TVP Polonia + TVP Regionalna + TVP Rozrywka + TVP Seriale + TVP Sport + TV Puls 2 + TV Puls PL + TVP Wilno + TVP World + TV Regionalna.pl + TV Republika + TVR PL + TVS + TV Silesia + TVT PL + TV Trwam + Uçankuş TV + VG TV + Viasat Explore CEE + Viasat True Crime + ViDoc TV + Vivid Red + Vivid TV + VOX + VOX Music TV PL + Warner TV FR + Warner TV IT + Warner TV PL + Water Planet + WDR Fernsehen Köln + Welt + Welt der Wunder + World Fashion Channel + wPolsce PL + WP TV + Wydarzenia 24 + Xtreme TV + XXL + Yaban TV + ZDFinfo + ZDFneo + Zoom TV PL + Еспресо TV + 2X2 + 3SAT + 4Fun Dance + 4Fun Kids + 4FunTV + 13 Ulica + 24 Kitchen + 24Kitchen PT + A2TV TR + Active Family + Adult Channel 2 + Adult Channel + Adventure HD + a Haber + Ale Kino+ + ALFA TVP + Al Jazeera + AMC Break PT + AMC Crime PT + AMC PL + AMC PT + Andalucía TV + a News + A Para + ARD-alpha + ARTE FR + ARTV + ATV1 + ATV2 + AXN Movies PT + AXN PL + AXN PT + AXN Spin PL + AXN White PL + AXN White PT + BabyTV + BabyTV ES + BBC News Channel + BBC World News + BBN Türk + Benfica TV + BFM TV + Biggs + Bloomberg HT + Bloomberg TV + Canal 11 PT + Canal 24H + Canal+ Domo + CANAL+ Kuchnia + Canal HISTÓRIA PT + Canal Hollywood PT + Canal J + Canal Panda PT + Canal Sur + Cartoonito PT + Cartoon Network PT + Casa e Cozinha + Caza y Pesca ES + CBS Reality + CCTV 4 Europe + CGTN + Cinemundo + CMTV + CNBC Europe + CNN Europe + CNN PT + Comedy Central DE + Comedy Central PL + Crime+Investigation PL + Deutsche Welle English + Disco Polo Music PL + Discovery Animal Planet + Discovery Channel PT + Discovery Historia + Discovery Science + Disney Channel PT + Disney Junior PT + Dizi Smart Max + Dizi Smart Premium + DMAX TR + Dorcel TV + Dorcel XXX + Dream Türk + E! Entertainment + Ekotürk + Eleven Sports 1 PT + Eleven Sports 2 PT + Eleven Sports 3 PT + Eleven Sports 4 PT + Eleven Sports 5 PT + Eleven Sports 6 PT + Erox HD + Eroxxx HD + Eska Rock TV + Eska TV + Eska TV Extra + Eurochannel + EuroNews + European League of Football Channel + Eurosport 1 PT + Eurosport 2 PT + EWTN PL + FashionBox HD + Fashion TV + Fast&FunBox HD + FightBox HD + Fight Klub HD + Filmax + FilmBox Arthouse + FilmBox Family PL + Film Cafe PL + Fokus TV + Folx TV + Food Network + Food Network UK + FOX NEWS + France24 + France 24 French + Fuel TV + FunBox UHD + FX Comedy PL + FX PL + Gametoon HD + Ginx Esports TV + Globo Portugal + Haber Global + Habitat TV + Hustler HD + HUSTLER TV + Insight TV + Investigation Discovery + iTVN + iTVN Extra + JimJam + KANAL 24 TR + Karadeniz TV + KiKA + Kino Polska + Kino Polska Muzyka + krone.tv + LUXE TV + M6 + MCM Top + Melodie TV + Metro TV + Mezzo + Mezzo Live HD + MinikaGO TR + Minimini+ + Motorvision Plus + Motorvision Plus International + Motowizja + MovieSmart Classic + MovieSmart Türk + MTV 00s + MTV ES + MTV Live HD + MTV PT + Museum TV + MyZen TV + Nat Geo Wild ES + National Geographic Portugal + Nickelodeon PT + Nickelodeon Ukraine Pluto + Nick Jr. PT + Nick Junior Commercial Light + Novelas+ + Novelas Plus 1 + Nowa TV + Number One Türk + NUTA.TV HD + NUTA GOLD + Odisseia + oe24.TV + ORF2 + Panda Kids + Phoenix + Planete+ PL + Playboy TV + Polonia1 + POLO TV + Polsat 1 + Polsat 2 + Polsat Cafe + Polsat Comedy Central Extra + Polsat Doku + Polsat Film + Polsat Games + Polsat Music + Polsat News 2 + Polsat News + Polsat Play + Polsat Rodzina + Polsat Seriale + Polsat Viasat Explore + Power TV PL + Private TV + PRO 7 Österreich + ProSieben + Puls 4 + RAI DUE + RAI UNO + RAI World Premium + Rai Yoyo + Red Carpet TV PL + RedLight HD + RT Documentary + RTL DE + RTP1 + RTP2 + RTP3 + RTP Açores + RTP Africa + RTP Internacional + RTP Madeira + RTP Memória + Russia Today + Russia Today ES + SAT.1 + Sat.1 Österreich + SIC + SIC Caras + SIC Internacional + SIC K + SIC Mulher + SIC Noticias + SIC Radical + SinemaTV 2 + SinemaTV 1001 + SinemaTV 1002 + SinemaTV + SinemaTV Aile 2 + SinemaTV Aile + SinemaTV Aksiyon 2 + SinemaTV Aksiyon + SinemaTV Komedi 2 + SinemaTV Komedi + SinemaTV Yerli 2 + SinemaTV Yerli + Sixx AT + Sky News + SkyShowtime 1 Nordic + Sport 1 DE + Sport TV1.pt + Sport TV2 + Sport TV3 + Sport TV4 + Sport TV5 + Sport TV6 + Sport TV7 + Sport TV+ + STAR Comedy PT + STAR Crime PT + STAR Life PT + STAR Movies PT + STAR Mundo PT + STAR PT + Stars TV PL + Star TV TR + Stingray CMusic + Stingray Djazz + Stingray iConcerts + Stopklatka + Sundance TV PL + Super Polsat + Super RTL + Syfy PT + TAY TV + TBN Polska HD + TELE 1 + TeleDeporte + Televisión de Galicia + Telewizja 13 + teve2 + TGRT Belgesel + The Nautical Channel + Tivibu Spor + TLC PAN + TPA7 + Trace Urban + Travel Channel + TRT 4K + TRT EBA TV İlkokul + TRT EBA TV Lise + TRT EBA TV Ortaokul + TTV + TV 4 + TV5 + TV5 Monde + TV5Monde Europe + TV 8.5 + TV100 TR + TV Cine Action + TV Cine Edition + TV Cine Emotion + TV Cine Top + TVE Internacional + TVG2 - TV Galicia + TVI + TVI Reality + TVN 7 + TVN24 + TVN + TV Net + TVN Style + TVN Turbo + TVP 2 + TVP 3 + TVP ABC + TVP HD + TVP Historia + TVP Info + TVP Kobieta + TVP Kultura + TVP Regionalna + TVP Rozrywka + TVP Seriale + TV Puls PL + TVR PL + TVS + TV Silesia + TVT PL + TV Trwam + Uçankuş TV + Vivid Red + Vivid TV + V Mais + VOX + Wydarzenia 24 + Xtreme TV + XXL + Yaban TV + ZDF + ZDFneo + 2X2 + 4Fun Dance + 4Fun Kids + 4FunTV + 13 Ulica + A2TV TR + Acasă + Acasă Gold + Active Family + Adult Channel + Adventure HD + AgroTV RO + a Haber + Ale Kino+ + ALFA TVP + Al Jazeera + Al Jazeera Arabic English + AMC PL + AMC RO + a News + Antena1 RO + Antena 3 CNN + Antena Stars RO + A Para + ARTE FR + ATV1 + ATV2 + ATV HU + ATV Spirit HU + ATV TR + Auto Motor und Sport + AXN Black PL + AXN Black RO + AXN PL + AXN RO + AXN Spin PL + AXN Spin RO + AXN White PL + AXN White RO + B1 TV RO + BabyTV + Balkanika TV + BBC Earth + BBC First + BBC World News + BBN Türk + Bloomberg HT + Bloomberg TV + Body in Balance + Bollywood Film RO + Brazzers TV (ex. Private Spice) + Bucuresti TV RO + Canal 33 RO + Canal+ Domo + CANAL+ Kuchnia + Cartoonito CEE + Cartoon Network RO + CBS Reality + CBS Reality PL + CCTV 4 Europe + CGTN + CGTN Documentary + Cinemaraton RO + Cinemax 2 RO + Cinemax RO + Cinethronix RO + Club MTV International + CNBC Europe + CNN Europe + Comedy Central Extra BG + Comedy Central HU + Comedy Central PL + Comedy Central RO + Credo TV + Crime+Investigation PL + Crime & Investigation Channel + Da Vinci Learning + Deutsche Welle English + Digi 24 + Digi Animal World RO + Digi Life RO + Digi World RO + Disco Polo Music PL + Discovery Animal Planet + Discovery Channel + Discovery Historia + Discovery Science + Disney Channel RO + Disney Junior + Diva Universal RO + DIZI + Dizi Smart Max + Dizi Smart Premium + DMAX TR + Dorcel TV + Dorcel XXX + Dream Türk + ducktv HD + Duna TV + Duna World + E! Entertainment + Ekotürk + Epic Drama (CEE) + Erox HD + Eroxxx HD + Eska Rock TV + Eska TV + Eska TV Extra + Etno TV RO + EuroNews + European League of Football Channel + Eurosport 1 INT + Eurosport 2 INT + Eurosport + EWTN PL + Extreme Sports Channel + Fashion TV + Fast&FunBox HD + Favorit TV RO + FightBox HD + Fight Klub HD + Film+ HU + Filmax + FilmBox Extra RO + FilmBox Family PL + FilmBox Family RO + FilmBox Plus RO + FilmBox Premium RO + FilmBox RO + FilmBox Stars BG + Filmbox Stars HU + Film Cafe PL + Film Cafe RO + Film Now RO + Focus TV + Fokus TV + Folx TV + Food Network + FX Comedy PL + FX PL + Galaxy4 + H!T Music Channel RO + Haber Global + Habitat TV + Happy Channel RO + HBO 2 RO + HBO 3 RO + HBO RO + Hír TV + History Channel RO + Home & Garden Television + HUSTLER TV + IDA RO + Inedit TV RO + Investigation Discovery + Izaura TV + JimJam + Kanal 7 + KANAL 24 TR + Kanal D RO + Kanal D TR + Karadeniz TV + Kino Polska + Kino Polska Muzyka + Kiss TV RO + krone.tv + Love Nature US + LUXE TV + M2 Petőfi + M4 sport + Magic TV BG + Magyar Sláger TV + Magyar Televízió 1 + Mediaset Italia + Medika TV RO + Melodie TV + Metro TV + Mezzo + Mezzo Live HD + MinikaGO TR + Minimax HU + Minimax RO + Minimini+ + Moldova TV + Mooz Dance + Mooz HD RO + Mooz Hits RO + Mooz Ro RO + Motorvision Plus + Motorvision Plus International + Motowizja + MovieSmart Classic + MovieSmart Türk + Mozi plusz TV + MTV 00s + MTV 80s + MTV 90s International + MTV Europe HU + MTV Hits International + MTV Live HD + MTV Music UK + Museum HD RO + Music Channel 1 RO + Muzsika TV + MyZen TV + Nasul TV RO + Nat Geo Wild RO + National 24 Plus RO + National Geographic People RO + National Geographic RO + National TV RO + Nickelodeon Commercial + Nick Junior PL + Nicktoons PL + Novelas+ + Novelas Plus 1 + NOW + Nowa TV + Number One Türk + NUTA.TV HD + NUTA GOLD + oe24.TV + ORF2 + Planete+ PL + Playboy TV + Polonia1 + POLO TV + Polsat 2 + Polsat Cafe + Polsat Comedy Central Extra + Polsat Doku + Polsat Film + Polsat Games + Polsat Music + Polsat News 2 + Polsat News + Polsat Play + Polsat Rodzina + Polsat Seriale + Polsat Sport 2 + Polsat Viasat Explore + Polsat Viasat History + Polsat Viasat Nature + Power TV PL + Prima Sport 1 RO + Prima Sport 2 RO + Prima Sport 3 RO + Prima Sport 4 RO + Prima Sport 5 RO + Prima TV RO + Prime + Private TV + PRO 7 Österreich + PRO ARENA RO + Pro Cinema RO + ProSieben + ProTV + PRO TV Internațional + Puls 4 + RAI TRE + RAI UNO + RAI World Premium + Rai Yoyo + Realitatea Plus RO + Reality Kings + Red Carpet TV PL + Romance TV PL + România TV + RTL DE + RTL Gold + RTL HU + RTL Ketto + RTLup + SAT.1 + Sat.1 Österreich + Show TV + SinemaTV 2 + SinemaTV 1001 + SinemaTV 1002 + SinemaTV + SinemaTV Aile 2 + SinemaTV Aile + SinemaTV Aksiyon 2 + SinemaTV Aksiyon + SinemaTV Komedi 2 + SinemaTV Komedi + SinemaTV Yerli 2 + SinemaTV Yerli + Sixx AT + Sky News + SkyShowtime 1 Nordic + Sorozat+ + Speranta TV + Spíler1 TV + Stars TV PL + Star TV TR + Stingray Classica + Stingray CMusic + Stingray Djazz + Stingray iConcerts + Stopklatka + Story4 + Sundance TV PL + Super Polsat + Super RTL + Super TV2 + Taraf Tv RO + TAY TV + TBN Polska HD + TeenNick + TELE 1 + Televiziunea Româna 1 + Televiziunea Româna 2 + Televiziunea Româna International + Telewizja 13 + teve2 + teve2 TR + TGRT Belgesel + The Fishing and Hunting RO + The Nautical Channel + Tivibu Spor + TLC + Trace Urban + Travel Channel + Travel Mix RO + Travelxp + Trinitas HD RO + TRT 4K + TRT EBA TV İlkokul + TRT EBA TV Lise + TRT EBA TV Ortaokul + TRT Turk + TRT World + TTV + TV2 Comedy + TV2 HU + TV2 Kids + TV2 Klub + TV2 Séf + TV 4 + TV4 HU + TV5 + TV5Monde Europe + TV 8.5 + TV 8 TR + TV100 TR + TV1000 Global Kino + TV1000 Russian Kino RO + TV Digi Sport 1 + TV Digi Sport 2 + TV Digi Sport 3 + TV Digi Sport 4 + TVE Internacional + TVN 7 + TVN24 + TVN24 BiS + TV Net + TVN Style + TVN Turbo + TVP 2 + TVP 3 + TVP ABC + TV Paprika HU + TV Paprika RO + TVP HD + TVP Historia + TVP Info + TVP Kobieta + TVP Kultura + TVP Regionalna + TVP Rozrywka + TVP Seriale + TV Puls PL + TVR3 RO + TVR Cluj RO + TVR Craiova RO + TVR Iasi RO + TVR PL + TVR Tg-Mures RO + TVR Timisoara RO + TVS + TV Silesia + TV SudEst + TVT PL + TV Trwam + Uçankuş TV + UTV RO + Viasat Explore CEE + Viasat History + Viasat Kino Balkan + Viasat Nature CEE + VTV RO + Warner TV IT + Warner TV RO + World Fashion Channel + wPolsce PL + Wydarzenia 24 + Xtreme TV + XXL + Yaban TV + Zenebutik + Zu TV + 2X2 + 3SAT + 4Fun Dance + 4Fun Kids + 4FunTV + 13 Ulica + 20 Mediaset + 24 Kitchen + 24Kitchen PT + A2TV TR + Active Family + Adventure HD + Agro TV + a Haber + Ale Kino+ + ALFA TV + ALFA TVP + Al Jazeera + Al Jazeera Balkans + Alternativna televizija Banja Luka + AMC PL + AMC SI + a News + Anixe HD Serie + A Para + Arena Esport + Arena Fight + Arena Sport 1 Premium SI + Arena Sport 1 SI + Arena Sport 2 SI + Arena Sport 3 SI + Arena Sport 4 SI + ARTE DE + ARTE FR + ASTRA TV + ATV1 + ATV2 + ATV TR + AXN + AXN Black PL + AXN PL + AXN Spin + AXN Spin PL + AXN White PL + B92 + BabyTV + Balkanika TV + Balkan trip + BBC Earth + BBC First + BBC World News + BBN Türk + BK TV + Bloomberg Adria + Bloomberg HT + Bloomberg TV + BN 2 HD + BN music + Brazzers TV (ex. Private Spice) + BRIO + Canal+ Domo + CANAL+ Kuchnia + Canale 5 + Cartoonito CEE + Cartoon Network + Cartoon Network DE + CBS Reality + CCTV 4 Europe + CGTN + Cinemax 2 + Cinemax + CineStar Action&Thriller + CineStar Premiere 1 + CineStar Premiere 2 + CineStar TV1 + CineStar TV2 + CineStar TV Comedy Family + CineStar TV Fantasy + Club MTV International + CNBC Europe + CNN Europe + Comedy Central DE + Comedy Central PL + Crime+Investigation PL + Crime & Investigation Channel + Croatian Music Channel + Das Erste + Da Vinci Learning + Disco Polo Music PL + Discovery Animal Planet + Discovery Channel + Discovery Historia + Discovery Science + Disney Channel + Disney Junior + DIVA (ex. Universal) + Dizi Smart Max + Dizi Smart Premium + DMAX TR + DM SAT + Dorcel TV + Dorcel XXX + Dream Türk + ducktv HD + ducktv SD + Duna TV + Duna World + E! Entertainment + Ekotürk + English Club TV + Epic Drama (CEE) + Erox HD + Eroxxx HD + Eska Rock TV + Eska TV + Eska TV Extra + ETV HD + Eurochannel + EuroNews + European League of Football Channel + Eurosport 2 + Eurosport + EWTN + EWTN PL + Exodus TV + Extreme Sports Channel + FashionBox HD + Fashion TV + Fast&FunBox HD + FightBox HD + Fight Klub HD + Filmax + FilmBox Arthouse + FilmBox Extra RS + FilmBox Family PL + FilmBox Stars RS + Film Cafe PL + Focus TV + Fokus TV + Folx TV + France 2 + France24 + France 24 French + FX Comedy PL + FX PL + GEA TV + Ginx Esports TV + Golica TV + Gorenjska televizija + Grand Televizija + Haber Global + Habitat TV + HappyTV + Hayat 2 + Hayat Folk Box + Hayat Music Box + Hayatovci + Hayat TV + HBO2 + HBO3 + HBO + HEMA TV + History Channel 2 + HIT TV + Home & Garden Television + HRT 1 + HRT 2 + HRT 3 + HRT 4 + HUSTLER TV + IDJ World + Investigation Discovery + Italia 1 + JimJam + Jugoton TV + K::CN 1 Kopernikus + K::CN 2 Music + K::CN 3 Svet Plus + Kabel Eins + KANAL 24 TR + Kanal A, SLO + Kanal D + Kanal Rijeka + Karadeniz TV + KiKA + Kino Polska + Kino Polska Muzyka + Klape i Tambure TV + KLASIK + krone.tv + KTV Ormož + Living HD + Lov i ribolov + LUXE TV + M2 Petőfi + Mediaset Italia + Melodie TV + Metro TV + Mezzo + Mezzo Live HD + MinikaGO TR + Minimax + Minimax SI + Minimini+ + Motorvision Plus + Motowizja + MovieSmart Classic + MovieSmart Türk + MrežaZG + MTV 00s + MTV 80s + MTV 90s International + MTV DE + MTV Europe + MTV Hits International + MTV Live HD + MyZen TV + N1 BA + N1 HR + N1 RS + Nat Geo Wild + Nat Geo Wild HD + Nat Geo Wild SI + National Geographic + National Geographic Channel HD + NBA TV + Net TV + Nickelodeon + Nickelodeon Commercial + Nick Junior + Nicktoons + Nova 24 TV + NOVA TV + Novelas+ + Novelas Plus 1 + NOW + Nowa TV + NTV DE + NTV IC Kakanj + Number One Türk + NUTA.TV HD + NUTA GOLD + OBN + oe24.TV + O Kanal + ORF1 + ORF2 + ORF3 + OTO + Otvorena televizija + OTV Valentino + Pickbox TV SI + Pikaboo 2 + PINK Family + Pink Fashion + PINK Film + Pink Folk 1 + Pink Folk 2 + Pink Kids + Pink Movies + Pink Music 1 + Pink Reality + Pink Serije + Pink Show + PINK World + PINK Zabava + Planet 2 + Planete+ PL + Planet TV SI + Playboy TV + Polonia1 + POLO TV + Polsat 2 + Polsat Cafe + Polsat Comedy Central Extra + Polsat Doku + Polsat Film + Polsat Games + Polsat Music + Polsat News 2 + Polsat News + Polsat Play + Polsat Rodzina + Polsat Seriale + Polsat Viasat Explore + Polsat Viasat History + Polsat Viasat Nature + POP KINO + POP TV + Power TV PL + Private TV + PRO 7 Österreich + ProSieben + Prva Srpska TV + Prva World + Ptujska televizija + Puls 4 + Radio Televizija BIH + Radio Televizija BN + Radio Televizija Federacije BIH + RAI 3 Bis + RAI 4 + RAI 5 + RAI DUE + RAI Education + RAI Gulp + RAI News 24 + RAI Sport 1 + RAI Storia + RAI TRE + RAI UNO + RAI World Premium + Rai Yoyo + Reality Kings + Red Carpet TV PL + RedLight HD + RED tv + Rete 4 + RTK 1 + RTL 2 + RTL + RTL DE + RTL Kockica + RTL Living + RTL Zwei + RTRS + RTS 1 + RTS 2 + RTS SVET + RTV International + RTV PINK + RTV Slovenija 1 + RTV Slovenija 2 + RTV Slovenija 3 + RTV Tuzlanskog Kantona + RTV Unsko-sanskog kantona + Russia Today + SAT.1 + Sat.1 Österreich + SciFi + ServusTV + Sexation TV + Show TV + SinemaTV 2 + SinemaTV 1001 + SinemaTV 1002 + SinemaTV + SinemaTV Aile 2 + SinemaTV Aile + SinemaTV Aksiyon 2 + SinemaTV Aksiyon + SinemaTV Komedi 2 + SinemaTV Komedi + SinemaTV Yerli 2 + SinemaTV Yerli + Sixx + Sixx AT + SK Fight + SK Golf + Sky News + SOS Plus + Sport 1 DE + Sportitalia + Sport Klub 1 Slovenija + Sport Klub 2 Slovenija + Sport Klub 3 + Sport Klub 4 + Sport Klub 5 + Sport Klub 6 + Sport TV1.pt + Sport TV2 + Šport TV 1 + Šport TV 2 + STAR Crime SI + STAR Life SI + STAR Movies SI + STAR SI + Stars TV PL + Star TV TR + Stingray Djazz + Stingray iConcerts + Stopklatka + ŠTV3 + Sundance TV PL + Super Polsat + Super RTL + Tanjug Tačno + TAY TV + TBN Polska HD + TELE 1 + Telequattro + Televizija Alfa + Televizija AS + Televizija Crne Gore 1 + Televizija Crne Gore 2 + Televizija Crne Gore MNE + Televizija skupnih internih programov + Telewizja 13 + Telma + teve2 + teve2 TR + TGRT Belgesel + The Fishing and Hunting + The History Channel + The Nautical Channel + Tivibu Spor + TLC + Top TV + Toxic TV + Trace Urban + Travel Channel + Travelxp + TRT 4K + TRT EBA TV İlkokul + TRT EBA TV Lise + TRT EBA TV Ortaokul + TRT World + TTV + TV3 + TV 3 Medias + TV 4 + TV5 + TV5 Monde + TV 8.5 + TV100 TR + TV Arena + TV ATM + TV Celje + TV Duga + SAT + TV Galeja + TV Idea + TV Jadran + TV Koper + TV Maribor + TVN 7 + TVN24 + TV Net + TVN Style + TVN Turbo + TVP 2 + TVP 3 + TVP ABC + TVP HD + TVP Historia + TVP Info + TV PINK EXTRA + TV PINK PLUS + TVP Kobieta + TVP Kultura + TVP Regionalna + TVP Rozrywka + TVP Seriale + TV Puls PL + TVR PL + TVS + TV Sarajevo + TV Silesia + TVT PL + TV Trwam + TV Veseljak + TV Vijesti + Uçankuş TV + Varaždinska Televizija + Vaš kanal + Vavoom + Viasat Explore CEE + Viasat History + Viasat Kino Balkan + Viasat Nature CEE + Viasat True Crime + Vikom + VOX + VTV Studio + Welt + wPolsce PL + Wydarzenia 24 + Xtreme TV + XXL + Yaban TV + Zagrebačka Televizija + ZDF + Zdrava televizija + Алсат М + БНТ 2 + Канал 5 + МРТ 1 + МРТ 2 + МРТ 3 + Россиᴙ 24 + Сител + 1-2-3.tv + 2X2 + 2X2 + 4Fun Dance + 4Fun Kids + 4FunTV + 4FunTV + 13 Ulica + 13 Ulica + 20 Mediaset + 21 Mix + 21 Mix + 24 Kitchen + 24Kitchen PT + 24Kitchen PT + 360 TuneBox + 360 TuneBox + A2 CNN + A2TV TR + ABC News Albania + ABC News Albania + Active Family + Active Family + Adventure HD + Agro TV + a Haber + a Haber + Ale Kino+ + Ale Kino+ + ALFA TVP + ALFA TVP + Al Jazeera + Al Jazeera + Al Jazeera Balkans + Al Jazeera Balkans + Alpha TV + AMC + AMC PL + AMC PL + a News + a News + A Para + A Para + Apollon TV + Apollon TV + Arena Sport 1 RS + Arena Sport 2 RS + Arena Sport 3 RS + Arena Sport 4 RS + Arena Sport 5 RS + Arena Sport 6 RS + ArtDoku 1 + ArtDoku 2 + ARTE DE + ARTE DE + ART Sport 1 + ART Sport 2 + ART Sport 3 + ART Sport 4 + ART Sport 5 + ART Sport 6 + ATV1 + ATV2 + ATV2 + ATV Avrupa + ATV KS + ATV TR + ATV TR + AXN + AXN Black PL + AXN PL + AXN PL + AXN Spin + AXN Spin PL + AXN Spin PL + AXN White PL + B92 + BabyTV + BabyTV + Balkanika TV + Balkanika TV + Bang Bang + Bang Bang + BBC Earth + BBC World News + BBC World News + BBN Türk + BBN Türk + Beyaz TV + Beyaz TV + Bloomberg HT + Bloomberg HT + BN music + BN music + Brazzers TV (ex. Private Spice) + Bubble TV + Bubble TV + CANAL+ Kuchnia + CANAL+ Kuchnia + Canale 5 + Cartoonito CEE + Cartoonito CEE + Cartoon Network + Cartoon Network + CBS Reality + Cinemax 2 + Cinemax + CineStar Action&Thriller RS + CineStar TV Comedy Family + CineStar TV Fantasy + CineStar TV RS + City TV + Click TV + Click TV + CNBC Europe + CNN Europe + CNN Europe + CNN Türk TV + Comedy Central DE + Comedy Central Extra UK + Comedy Central PL + Comedy Central PL + Crime+Investigation PL + Crime+Investigation PL + Crime & Investigation Channel + Cufo TV + Cufo TV + Das Erste + Da Vinci Learning + Deluxe Music + Deutsche Welle English + Deutsche Welle English + DigitAlb Melody TV + Disco Polo Music PL + Disco Polo Music PL + Discovery Animal Planet + Discovery Animal Planet + Discovery Channel + Discovery Channel + Discovery Historia + Discovery Historia + Discovery Science + Discovery Turbo UK + Disney Channel + Disney Channel + DIVA (ex. Universal) + DIZI + DIZI + Dizi Smart Max + Dizi Smart Max + Dizi Smart Premium + Dizi Smart Premium + DMAX DE + DMAX TR + DMAX TR + DM SAT + DM SAT + Dorcel TV + Dream Türk + Dream Türk + ducktv HD + ducktv SD + E! Entertainment + Ekotürk + Ekotürk + Elrodi + Elrodi + English Club TV + English Club TV + Epic Drama (CEE) + Erox HD + Eroxxx HD + Eroxxx HD + Eska Rock TV + Eska TV + Eska TV Extra + Eska TV Extra + Eurochannel + Eurochannel + Euro D + EuroNews + EuroNews + Euronews Albania + Euronews Albania + European League of Football Channel + Eurosport + Euro Star + EWTN PL + Explorer Histori + Explorer Histori + Explorer Natyra + Explorer Natyra + Explorer Shkence + Explorer Shkence + EXTASY TV + Extreme Sports Channel + FashionBox HD + FashionBox HD + Fashion TV + Fast&FunBox HD + Fast&FunBox HD + FAX News + FAX News + FightBox HD + FightBox HD + Fight Klub HD + Fight Klub HD + Filmax + Filmax + FilmBox Arthouse + FilmBox Arthouse + FilmBox Extra RS + FilmBox Extra RS + FilmBox Family PL + FilmBox Premium RS + FilmBox Stars RS + FilmBox Stars RS + Film Cafe PL + Film Cafe PL + Fokus TV + Fokus TV + Folx TV + Food Network + France 2 + France24 + France24 + France 24 French + FX Comedy PL + FX Comedy PL + FX PL + FX PL + Gametoon HD + Gametoon HD + Haber Global + Haber Global + Habertürk + Habitat TV + Habitat TV + HappyTV + Hayat 2 + Hayat Folk Box + Hayat TV + HBO2 + HBO3 + HBO + History Channel 2 + HRT 1 + HRT 3 + HSE24 Extra + HSE24 Trend + HUSTLER TV + Info24 Albania + INTV AL + Investigation Discovery + Investigation Discovery + Italia 1 + JimJam + Junior TV + Junior TV + Kabel Eins + Kabel eins Doku + Kanal 6 + Kanal 7 + Kanal 7 + Kanal 10 + Kanal 10 + KANAL 24 TR + KANAL 24 TR + Kanal D + Kanal D + Kanal D TR + Kanali 7 + Kanali 7 + Karadeniz TV + Karadeniz TV + KB Peja TV + KiKA + Kino Polska + Kino Polska + Kino Polska Muzyka + KitchenTV + Klan Kosova + Klan Kosova + Klan Macedonia + Klan Music + Klan Plus + Klan Plus + Klan TV HD + Klan TV HD + Kopliku TV + Kopliku TV + krone.tv + La7 + Living HD + Living HD + MCN 24 + MCN TV + Mediaset Italia + Melodie TV + MinikaGO TR + MinikaGO TR + Minimax + Minimini+ + Minimini+ + Motorvision Plus International + Motowizja + MovieSmart Classic + MovieSmart Classic + MovieSmart Türk + MovieSmart Türk + MTV 00s + MTV 80s + MTV 90s International + MTV Europe + MTV Hits International + MTV Live HD + MTV Live HD + MUSE + MUSE + My Music + My Music + N24 Doku + Nat Geo Wild + Nat Geo Wild + National Geographic + National Geographic + National Geographic Channel HD + NBA TV + Ndihma e Klientit + News 24 + News 24 + Nickelodeon + Nickelodeon + Nick Junior + Novelas+ + Novelas+ + Novelas Plus 1 + Novelas Plus 1 + NOW + Nowa TV + Nowa TV + NTV DE + NTV DE + Number One Türk + Number One Türk + NUTA.TV HD + NUTA.TV HD + NUTA GOLD + NUTA GOLD + OBN + oe24.TV + One + Ora News + Ora News + ORF2 + ORF2 + Pikaboo + Pink Action + Pink and Roll + Pink Comedy + Pink Crime & Mystery + Pink Erotic 1 + Pink Erotic 2 + PINK Family + PINK Film + Pink Hits + Pink Kids + Pink LOL + Pink Music 1 + Pink Premium + Pink Reality + Pink Romance + Pink SCI FI & Fantasy + Pink Serije + Pink Super Kids + Pink Thriller + Pink Western + Pink World Cinema + Planete+ PL + Planete+ PL + Playboy TV + Polonia1 + Polonia1 + POLO TV + Polsat 2 + Polsat 2 + Polsat Cafe + Polsat Cafe + Polsat Comedy Central Extra + Polsat Comedy Central Extra + Polsat Doku + Polsat Doku + Polsat Film + Polsat Music + Polsat News 2 + Polsat News + Polsat Play + Polsat Rodzina + Polsat Seriale + Polsat Viasat Explore + Polsat Viasat Explore + Power Türk TV + Power Türk TV + Power TV + Power TV PL + Power TV PL + Premium Channel + Private TV + PRO 7 Österreich + PRO 7 Österreich + ProSieben + ProSieben MAXX + Prva FILES + Prva KICK + Prva LIFE + Prva MAX + Prva Srpska TV + Prva World + QVC 2 DE + QVC Deutschland + Radio Televizija BN + Radio Televizija Federacije BIH + RAI 4 + RAI 5 + RAI DUE + RAI DUE + RAI Gulp + RAI News 24 + RAI News 24 + RAI Storia + RAI TRE + RAI TRE + RAI UNO + Rai Yoyo + real time + Red Carpet TV PL + Red Carpet TV PL + RED tv + Report TV + Report TV + Rete 4 + RT Documentary + RTK 1 + RTK 1 + RTK 2 + RTK 2 + RTK 4 + RTL 2 + RTL + RTL DE + RTL Nitro + RTLup + RTL Zwei + RTRS + RTRS PLUS + RTS 1 + RTS 2 + RTS 2 + RTS 3 + RTS Drama + RTSH 1 + RTSH 2 + RTSH 3 + RTSH 24 + RTSH Agro + RTSH Femijë + RTSH Film + RTSH Gjirokastra + RTSH Korça + RTSH Kuvend + RTSH Plus + RTSH Shkollë + RTSH Shqip + RTSH Sport + RTS Kolo + RTS Muzika + RTS Poletarac + RTS SVET + RTS Trezor + RTS Život + RTV21 + RTV 21 Popullore + RTV 21 Popullore + RTV Besa + RTV Besa + RTV Most + RT Vojvodina 1 + RT Vojvodina 2 + RTV Ora + RTV PINK + Russia Today + Russia Today + SAT.1 + Sat.1 Gold + Sat.1 Österreich + Sat.1 Österreich + Scan TV + SciFi + Shenja TV + Shenja TV + Show Türk + Show TV + Show TV + SinemaTV 2 + SinemaTV 2 + SinemaTV 1001 + SinemaTV 1001 + SinemaTV 1002 + SinemaTV 1002 + SinemaTV + SinemaTV + SinemaTV Aile 2 + SinemaTV Aile 2 + SinemaTV Aile + SinemaTV Aile + SinemaTV Aksiyon 2 + SinemaTV Aksiyon 2 + SinemaTV Aksiyon + SinemaTV Aksiyon + SinemaTV Komedi 2 + SinemaTV Komedi 2 + SinemaTV Komedi + SinemaTV Komedi + SinemaTV Yerli 2 + SinemaTV Yerli 2 + SinemaTV Yerli + SinemaTV Yerli + Sixx + Sixx AT + Sixx AT + Sky News + SOS Plus + STAR Channel + STAR Channel + STAR Crime + STAR Crime + STAR Life + STAR Life + Stars TV PL + Star TV TR + Stinet + Stingray iConcerts + Stopklatka + Stopklatka + Studio B + STV Folk + Sundance TV PL + Sundance TV PL + Super Polsat + Super RTL + Super RTL + Superstar TV + Syri TV + Syri TV + Syri Vizion + Syri Vizion + TAY TV + TAY TV + TBN Polska HD + TELE 1 + TELE 1 + Tele 5 DE + Telewizja 13 + Telewizja 13 + teve2 + teve2 + teve2 TR + TGCOM24 + TGRT Belgesel + TGRT Belgesel + TGRT EU + The History Channel + Tip TV + Tip TV + Tivibu Spor + Tivibu Spor + TLC + TLC + Top Channel + Top Channel + Top News + Travel Channel + Travel Channel + Tring Bizarre + Tring Bizarre + Tring Bunga Bunga + Tring Bunga Bunga + Tring Desire + Tring Desire + Tring Kanal 7 + Tring Smile + Tring Smile + Tring Sport 1 + Tring Sport 2 + Tring Sport 3 + Tring Sport 4 + Tring Sport News + Tring Sport News + TRT 1 + TRT 2 + TRT 4K + TRT 4K + TRT Belgesel + TRT Çocuk + TRT EBA TV İlkokul + TRT EBA TV İlkokul + TRT EBA TV Lise + TRT EBA TV Lise + TRT EBA TV Ortaokul + TRT EBA TV Ortaokul + TRT Haber + TRT Müzik + TRT Spor + TRT Spor Yıldız + TRT Turk + TRT World + TTV + TV5 + TV5 + TV5 Monde + TV 7 + TV 7 + TV 8.5 + TV 8.5 + TV 8 TR + TV100 TR + TV100 TR + TV Dukagjini + TV Dukagjini + TVN 7 + TVN 7 + TV Net + TV Net + TVN Style + TVN Style + TVN Turbo + TVN Turbo + TVP 2 + TVP 3 + TVP ABC + TVP ABC + TVP HD + TVP HD + TVP Historia + TVP Historia + TVP Info + TVP Kobieta + TVP Kobieta + TVP Kultura + TVP Kultura + TVP Rozrywka + TVP Rozrywka + TVP Seriale + TVP Seriale + TVR PL + TVR PL + TVS + TV Silesia + TVT PL + TV Trwam + TV Trwam + Uçankuş TV + Uçankuş TV + Ülke TV + Vavoom + Vesti + Viasat Explore CEE + Viasat History + Viasat Kino Balkan + Viasat Nature CEE + Vizion Plus + Vizion Plus + VOX + VOX + VOX up + Welt + World Fashion Channel + Wydarzenia 24 + Xtreme TV + Xtreme TV + XXL + Yaban TV + Yaban TV + ZDF + ZDFneo + Zico TV + Zico TV + Zjarr TV + Zjarr TV + ΕΡΤ1 + Алсат М + Телевизија Храм + ФЕН ТВ + 2X2 + 3SAT + 4Fun Dance + 4Fun Kids + 4FunTV + 13 Ulica + 24 Kitchen + 24Kitchen PT + 360 TuneBox + A2TV TR + Active Family + Adult Channel 2 + Adventure HD + Agro TV + a Haber + Ale Kino+ + ALFA TV + ALFA TVP + Al Jazeera + Al Jazeera Balkans + AMC + AMC HU + AMC PL + a News + ANIXE plus + A Para + Arena Esport + Arena Fight + Arena Sport 1 Premium + Arena Sport 1 RS + Arena Sport 1x2 + Arena Sport 2 Premium + Arena Sport 2 RS + Arena Sport 3 Premium + Arena Sport 3 RS + Arena Sport 4 RS + Arena Sport 5 RS + Arena Sport 6 RS + Arena Sport 7 RS + Arena Sport 8 RS + Arena Sport 9 RS + Arena Sport 10 RS + ATV1 + ATV2 + Aurora TV + AXN + AXN Black PL + AXN PL + AXN Spin + AXN Spin PL + AXN White PL + B1 TV + B92 + BabyTV + Balkanika TV + Balkan trip + Balkan TV + BBC Earth + BBC News Channel + BBC World News + BBN Türk + Bit TV + BK TV + BlicTV + Bloomberg Adria + Bloomberg HT + Bloomberg TV + BN 2 HD + BN music + Brainz TV + Bravo Music + Brazzers TV (ex. Private Spice) + CANAL+ Kuchnia + Cartoonito CEE + Cartoon Network + CBS Reality + CCTV 4 Europe + CGTN + CGTN Documentary + Cinemania TV + Cinemax 2 + Cinemax 2 HU + Cinemax + Cinemax HU + CineStar Action&Thriller RS + CineStar Premiere 1 + CineStar Premiere 2 + CineStar TV2 + CineStar TV Comedy Family + CineStar TV Fantasy + CineStar TV RS + City Play + City TV + Class TV Moda + Club MTV International + CNBC Europe + CNN Europe + Comedy Central HU + Comedy Central PL + Comedy Central UK + CoolTV + Crime+Investigation PL + Crime & Investigation Channel + Croatian Music Channel + Das Erste + Da Vinci Learning + Deluxe Music + Deutsche Welle English + Dexy TV + Digi 24 + Disco Polo Music PL + Discovery Animal Planet + Discovery Channel + Discovery Channel HU + Discovery Historia + Discovery Science + Disney Channel + Disney Channel DE + Disney Channel HU + Disney Junior + DIVA (ex. Universal) + DIZI + Dizi Smart Max + Dizi Smart Premium + DMAX DE + DMAX TR + DM SAT + Dorcel TV + Dorcel XXX + DOX TV + Dream Türk + ducktv HD + ducktv SD + Duna TV + Duna World + E! Entertainment + Ekotürk + Epic Drama (CEE) + Erox HD + Eroxxx HD + Eska Rock TV + Eska TV Extra + Etno TV RO + Eurochannel + Euro Cinema 1 + Euro Cinema 2 + Euro Cinema 3 + Euro Cinema 4 + EuroNews + Euronews FR + EuroNews Srbija + European League of Football Channel + Eurosport 1 DE + Eurosport 2 + Eurosport + EWTN + EWTN PL + Extreme Sports Channel + FACE TV + FashionBox HD + Fashion TV + Fast&FunBox HD + Favorit TV RO + FightBox HD + Fight Klub HD + Film4 HU + Filmax + FilmBox Arthouse + Filmbox Extra HU + FilmBox Extra RS + FilmBox Family PL + Filmbox Premium HU + FilmBox Premium RS + Filmbox Stars HU + FilmBox Stars RS + Film Cafe PL + Fokus TV + Folklorika SK + Folx TV + Food Network + FOX NEWS + France24 + France 24 French + FX Comedy PL + FX PL + Gametoon HD + Golica TV + Grand Televizija + Haber Global + Habitat TV + HappyTV + Hayat 2 + Hayat Folk Box + Hayat Music Box + Hayat TV + HBO2 + HBO 2 HU + HBO3 + HBO 3 HU + HBO + HBO HU + HEMA TV + Hír TV + History Channel 2 + Home & Garden Television + HRT 1 + HRT 2 + HRT 3 + HRT 4 + HSE + HUSTLER TV + Hype TV + ICT Business + IDJ World + Info24 Albania + InformerTV + Insajder TV + Investigation Discovery + Izaura TV + JimJam + K1 TV + K::CN 1 Kopernikus + K::CN 2 Music + K::CN 3 Svet Plus + Kabel Eins + Kabel eins Doku + Kanal 9 TV + KANAL 24 TR + Karadeniz TV + Kazbuka + KB Peja TV + KiKA + Kino Polska + Kino Polska Muzyka + KitchenTV + KLASIK + KLASIK HR + krone.tv + Kurir TV + Lala TV + LifeTV SK + Love Nature US + Lov i ribolov + M2 Petőfi + M4 sport + M4 Sport Plus + m5 + Magyar Televízió 1 + Mediaset Italia + Melodie TV + Mezzo + MinikaGO TR + Minimax + Minimax HU + Minimini+ + Moja Happy Muzika + Moja Happy Zemlja + Moje Happy Društvo + Moj Happy Život + Motowizja + MovieSmart Classic + MovieSmart Türk + MTV 00s + MTV 80s + MTV 90s International + MTV Europe + MTV Hits International + MTV Live HD + Muzsika TV + N1 HR + N1 RS + N24 Doku + Narodna TV + Nat Geo Wild + Nat Geo Wild HD + National Geographic + National Geographic Channel HD + National Geographic HU + National Geographic RS + NBA TV + Newsmax Balkans + Nickelodeon Commercial + Nick Junior + Nick Junior PL + Nick Music + Nicktoons + Nicktoons PL + Nova Max + Nova S + Nova Series + Nova Sport Srbija + NOVA TV + Novelas+ + Novelas Plus 1 + Novosadska TV + NOW + Nowa TV + Now Rock + NTV 101 Sanski most + Number One Türk + NUTA.TV HD + NUTA GOLD + OBN + oe24.TV + O Kanal + Ozone Network + Pickbox TV RS + Pikaboo + Pink Action + Pink and Roll + Pink BH + Pink Classic + Pink Comedy + Pink Crime & Mystery + Pink Erotic 1 + Pink Erotic 2 + Pink Erotic 3 + Pink Erotic 4 + Pink Erotic 5 + Pink Erotic 6 + Pink Erotic 7 + Pink Erotic 8 + PINK Family + Pink Fashion + Pink Fight Network + PINK Film + Pink Folk 1 + Pink Folk 2 + Pink Ha Ha + Pink Hits 2 + Pink Hits + Pink Horror + Pink Kids + Pink Koncert + Pink Kuvar + Pink LOL + Pink M + Pink Movies + Pink Music 1 + Pink Pedia + Pink Premium + Pink Reality + Pink Romance + Pink SCI FI & Fantasy + Pink Serije + Pink Show + Pink Soap + Pink Style + Pink Super Kids + Pink Thriller + Pink Timeout + Pink Western + PINK World + Pink World Cinema + PINK Zabava + Planete+ PL + Planet TV SI + Playboy TV + Polonia1 + POLO TV + Polsat 2 + Polsat Cafe + Polsat Comedy Central Extra + Polsat Doku + Polsat Film + Polsat Music + Polsat News 2 + Polsat News + Polsat Play + Polsat Rodzina + Polsat Seriale + Polsat Viasat Explore + Polsat Viasat History + Polsat Viasat Nature + Power TV PL + Private TV + PRO 7 Österreich + ProSieben + ProSieben MAXX + Prva FILES + Prva KICK + Prva LIFE + Prva MAX + Prva Plus + Prva Srpska TV + Prva TV Crna Gora + Prva World + Puls 4 + QVC Deutschland + Radio Televizija BN + Radio Televizija Federacije BIH + RAI DUE + RAI Education + RAI News 24 + RAI Storia + RAI TRE + RAI UNO + RAI World Premium + Rai Yoyo + Reality Kings + Red Carpet TV PL + RedLight HD + RED tv + România TV + RT Documentary + RTL + RTL Croatia World + RTL Gold + RTL Három + RTL Nitro + RTLup + RTRS + RTRS PLUS + RTS 1 + RTS 2 + RTS 3 + RTS Drama + RTSH 3 + RTSH 24 + RTSH Plus + RTSH Shkollë + RTS Klasika + RTS Kolo + RTS Muzika + RTS Nauka + RTS Poletarac + RTS SVET + RTS Trezor + RTS Život + RTV HIT Brčko + RTV Most + RTV Novi Pazar + RT Vojvodina 1 + RT Vojvodina 2 + RTV Pančevo + RTV PINK + RTVS Dvojka + RTVS Jednotka + RTV Slovenija 1 + RTV Slovenija 2 + RTV Slovenija 3 + Russia Today + SAT.1 + Sat.1 Gold + Sat.1 Österreich + SAT TV + SciFi + Show TV + SinemaTV 2 + SinemaTV 1001 + SinemaTV 1002 + SinemaTV + SinemaTV Aile 2 + SinemaTV Aile + SinemaTV Aksiyon 2 + SinemaTV Aksiyon + SinemaTV Komedi 2 + SinemaTV Komedi + SinemaTV Yerli 2 + SinemaTV Yerli + Sixx + Sixx AT + SK Fight + SK Golf + Sky News + Sorozat+ + SOS Plus + Spektrum Home HU + Sport 1 DE + Sremska TV + STAR Channel + STAR Crime + STAR Life + STAR Movies + Stars TV PL + Star TV TR + Stingray Classica + Stingray iConcerts + Stopklatka + Studio B + Sundance TV PL + Super Polsat + Super RTL + SuperSat TV + Superstar 2 + Superstar 3 + Superstar TV + Super TV2 + TA3 SK + Tanjug Tačno + TAY TV + TBN Polska HD + TELE 1 + Tele 5 DE + Televizija 5 + Televizija 24 + Televizija Alfa + Televizija Crne Gore MNE + Televizija Doktor + Televiziunea Româna International + Telewizja 13 + teve2 + teve2 TR + TGRT Belgesel + The History Channel + Tivibu Spor + TLC + TLC DE + TOGGO plus + Top TV + Toxic Folk + Toxic Rap + Toxic TV + Trace Urban + Travel Channel + Travelxp + TRT 4K + TRT EBA TV İlkokul + TRT EBA TV Lise + TRT EBA TV Ortaokul + TTV + TV2 Klub + TV2 Séf + TV 4 + TV4 HU + TV5 + TV5 Monde + TV5Monde Europe + TV 8.5 + TV 8 TR + TV100 TR + TV Belle Amie + TV Duga + SAT + TV Galaksija + TV K23 + TV Markíza SK + TVN 7 + TV Net + TVN Style + TVN Turbo + TVP 2 + TVP 3 + TVP ABC + TV Paprika HU + TVP HD + TVP Historia + TVP Info + TV PINK EXTRA + TV PINK PLUS + TVP Kobieta + TVP Kultura + TVP Rozrywka + TVP Seriale + TV Ras + TVR Cluj RO + TVR Craiova RO + TVR Iasi RO + TVR PL + TVR Tg-Mures RO + TVR Timisoara RO + TV Silesia + TVT PL + TV Trwam + TV Vijesti + Uçankuş TV + UNA TV + Vavoom + Vesti + Viasat Explore CEE + Viasat History + Viasat Kino Balkan + Viasat Nature CEE + Viasat True Crime + VOX + VOX up + wPolsce PL + Wydarzenia 24 + Xtreme TV + XXL + Yaban TV + Zagrebačka Televizija + ZDF + ZDFinfo + ZDFneo + Первый + Россиᴙ 24 + Телевизија Храм + 2X2 + 4Fun Dance + 4Fun Kids + 4FunTV + 13 Ulica + 24 Kitchen + 360 TV + A2TV TR + Active Family + Adult Channel 2 + Adult Channel + Adventure HD + a Haber + ALFA TVP + Al Jazeera + Al Jazeera Arabic English + AMC PL + a News + A Para + ATV2 + ATV TR + AXN PL + AXN Spin PL + BabyTV TR + BBC First + BBC World News + BBN Türk + beIN Box Office 1 TR + beIN Box Office 2 TR + beIN Box Office 3 TR + beIN GURME + beIN HOME & ENTERTAINMENT + beIN iZ + beIN Movies Premiere 2 + beIN Movies Premiere TR + beIN Movies Stars + beIN Movies Turk + beIN Series 1 TR + beIN Series 2 TR + beIN Series 3 TR + beIN Series 4 TR + beIN Sports 1 TR + beIN Sports 2 TR + beIN Sports 3 TR + beIN Sports 4 TR + beIN Sports 5 TR + beIN Sports Haber + beIN Sports MAX 1 TR + beIN Sports MAX 2 TR + beIN TR + Beyaz TV + Bloomberg HT + Bloomberg TV + Bloomberg TV MENA + Body in Balance + CANAL+ Kuchnia + Cartoonito CEE + Cartoonito TR + Cartoon Network TR + Cbeebies + CGTN + CGTN Documentary + CNBC Europe + CNN Europe + CNN Türk TV + Comedy Central PL + Crime+Investigation PL + Da Vinci Learning + Da Vinci Learning TR + Deutsche Welle English + Disco Polo Music PL + Discovery Animal Planet + Discovery Channel + Discovery Historia + Discovery Science + Disney Junior + Dizi Smart Max + Dizi Smart Premium + DMAX TR + Dream Türk + Ekotürk + Epic Drama (CEE) + Erox HD + Eroxxx HD + Eska Rock TV + Eska TV + Eska TV Extra + Euro D + EuroNews + European League of Football Channel + Eurosport 2 + Euro Star + EWTN PL + Fashion TV + Fast&FunBox HD + FENERBAHÇE TV + Fight Klub HD + Filmax + FilmBox Family PL + Film Cafe PL + Fokus TV + Folx TV + France24 + France 24 French + FX Comedy PL + FX PL + FX Turkey + Haber Global + Habertürk + Habitat TV + Hustler HD + HUSTLER TV + Insight TV + Kanal 7 + KANAL 24 TR + Kanal B + Kanal D + Kanal D TR + Karadeniz TV + Kino Polska + Kino Polska Muzyka + Kral Pop TV + krone.tv + Love Nature US + LUXE TV + MCM FR + MCM Top + Melodie TV + Mezzo + Mezzo Live HD + Minika Çocuk + MinikaGO TR + Minimini+ + Motorvision Plus International + Motowizja + MovieSmart Classic + MovieSmart Türk + MTV 00s + MTV Hits International + MTV Live HD + National Geographic Turkey + National Geographic Wild TR + NBA TV + Nick Junior + Nicktoons + Novelas+ + Novelas Plus 1 + NOW + Nowa TV + NTV TR + Number One Türk + NUTA.TV HD + NUTA GOLD + oe24.TV + Planete+ PL + Playboy TV + Polonia1 + POLO TV + Polsat 2 + Polsat Cafe + Polsat Comedy Central Extra + Polsat Doku + Polsat Film + Polsat Music + Polsat News 2 + Polsat News + Polsat Play + Polsat Rodzina + Polsat Seriale + Polsat Viasat Explore + Polsat Viasat History + Power Türk TV + Power TV + Power TV PL + Private TV + PRO 7 Österreich + Rai Yoyo + Red Carpet TV PL + RedLight HD + Sat.1 Österreich + Show Türk + Show TV + SinemaTV 2 + SinemaTV 1001 + SinemaTV 1002 + SinemaTV + SinemaTV Aile 2 + SinemaTV Aile + SinemaTV Aksiyon 2 + SinemaTV Aksiyon + SinemaTV Komedi 2 + SinemaTV Komedi + SinemaTV Yerli 2 + SinemaTV Yerli + Sixx AT + Spor Smart 2 + Spor Smart + Sports TV TR + S Sport 2 + S Sport + Stars TV PL + Star TV TR + Stopklatka + Sundance TV PL + Super Polsat + TAY TV + TBN Polska HD + TELE 1 + Telewizja 13 + teve2 + teve2 TR + TGRT Belgesel + TGRT EU + TGRT HABER + The History Channel + Tivibu Spor 1 + Tivibu Spor 2 + Tivibu Spor 3 + Tivibu Spor 4 + Tivibu Spor 5 + Tivibu Spor + TLC + TLC TR + Trace Urban + TRT 1 + TRT 2 + TRT 4K + TRT Arapça + TRT Avaz + TRT Belgesel + TRT Çocuk + TRT Diyanet + TRT EBA TV İlkokul + TRT EBA TV Lise + TRT EBA TV Ortaokul + TRT Haber + TRT Kurdî + TRT Müzik + TRT Spor + TRT Spor Yıldız + TRT Turk + TRT World + TTV + TV 4 + TV5 + TV 8.5 + TV 8 TR + TV100 TR + TVE Internacional + TVN 7 + TV Net + TVN Style + TVN Turbo + TVP 2 + TVP 3 + TVP ABC + TVP HD + TVP Historia + TVP Info + TVP Kobieta + TVP Kultura + TVP Rozrywka + TVP Seriale + TVR PL + TVS + TV Silesia + TVT PL + Uçankuş TV + Ülke TV + Viasat Explore CEE + Viasat History + Viasat Nature CEE + Vivid Red + Vivid TV + wPolsce PL + Wydarzenia 24 + Xtreme TV + Yaban TV + diff --git a/sites/tvprofil.com/tvprofil.com.config.js b/sites/tvprofil.com/tvprofil.com.config.js index d0d42e17..cf6714fb 100644 --- a/sites/tvprofil.com/tvprofil.com.config.js +++ b/sites/tvprofil.com/tvprofil.com.config.js @@ -1,187 +1,187 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') - -module.exports = { - site: 'tvprofil.com', - days: 2, - url: function ({ channel, date }) { - const parts = channel.site_id.split('#') - const query = buildQuery(parts[1], date) - - return `https://tvprofil.com/${parts[0]}/program/?${query}` - }, - request: { - headers: { - 'x-requested-with': 'XMLHttpRequest', - 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36', - 'referer': 'https://tvprofil.com/tvprogram/', - 'accept': 'text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01' - } - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const $ = cheerio.load(item) - $('div.row').each((_, el) => { - const $item = $(el) - const title = parseTitle($item) - const category = parseCategory($item) - const start = parseStart($item) - const duration = parseDuration($item) - const stop = start.add(duration, 's') - const icon = parseImage($item) - - programs.push({ title, category, start, stop, icon }) - }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - - // prettier-ignore - const countries = { - al: { channelsPath: '/al', progsPath: 'al/programacioni', lang: 'sq' }, - at: { channelsPath: '/at', progsPath: 'at/tvprogramm', lang: 'de' }, - ba: { channelsPath: '/ba', progsPath: 'ba/tvprogram', lang: 'bs' }, - bg: { channelsPath: '/bg', progsPath: 'bg/tv-programa', lang: 'bg' }, - ch: { channelsPath: '/ch', progsPath: 'ch/tv-programm', lang: 'de' }, - de: { channelsPath: '/de', progsPath: 'de/tvprogramm', lang: 'de' }, - es: { channelsPath: '/es', progsPath: 'es/programacion-tv', lang: 'es' }, - fr: { channelsPath: '/fr', progsPath: 'fr/programme-tv', lang: 'fr' }, - hr: { channelsPath: '', progsPath: 'tvprogram', lang: 'hr' }, - hu: { channelsPath: '/hu', progsPath: 'hu/tvmusor', lang: 'hu' }, - ie: { channelsPath: '/ie', progsPath: 'ie/tvschedule', lang: 'en' }, - it: { channelsPath: '/it', progsPath: 'it/guida-tv', lang: 'it' }, - ks: { channelsPath: '/ks', progsPath: 'ks/programacioni', lang: 'sq' }, - me: { channelsPath: '/me', progsPath: 'me/tvprogram', lang: 'en' }, - mk: { channelsPath: '/mk', progsPath: 'mk/tv-raspored', lang: 'mk' }, - pl: { channelsPath: '/pl', progsPath: 'pl/program', lang: 'pl' }, - pt: { channelsPath: '/pt', progsPath: 'pt/programacao', lang: 'pt' }, - ro: { channelsPath: '/ro', progsPath: 'ro/program-tv', lang: 'ro' }, - rs: { channelsPath: '/rs', progsPath: 'rs/tvprogram', lang: 'sr' }, - si: { channelsPath: '/si', progsPath: 'si/tvspored', lang: 'sl' }, - tr: { channelsPath: '/tr', progsPath: 'tr/tv-rehberi', lang: 'tr' }, - uk: { channelsPath: '/gb', progsPath: 'gb/tvschedule', lang: 'en' }, - } - - let channels = [] - for (let country in countries) { - const config = countries[country] - const lang = config.lang - - const url = `https://tvprofil.com${config.channelsPath}/channels/getChannels/` - - console.log(url) - - const cb = await axios - .get(url, { - params: { - callback: 'cb' - }, - headers: { - 'x-requested-with': 'XMLHttpRequest', - 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36', - 'referer': 'https://tvprofil.com/programtv/', - 'accept': 'text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01', - } - }) - .then(r => r.data) - .catch(err => { - console.error(err.message) - }) - - if (!cb) continue - - const [, json] = cb.match(/^cb\((.*)\)$/i) - const data = JSON.parse(json) - - data.data.forEach(group => { - group.channels.forEach(item => { - channels.push({ - lang, - site_id: `${config.progsPath}#${item.urlID}`, - xmltv_id: `${item.title.replaceAll(/[ '&]/g, '')}.${country}`, - name: item.title - }) - }) - }) - } - - return channels - } -} - -function parseImage($item) { - return $item.attr('data-image') || null -} - -function parseDuration($item) { - return parseInt($item.attr('data-len')) -} - -function parseStart($item) { - const timestamp = parseInt($item.attr('data-ts')) - return dayjs.unix(timestamp) -} - -function parseCategory($item) { - return $item.find('.col:nth-child(2) > small').text() || null -} - -function parseTitle($item) { - let title = $item.find('.col:nth-child(2) > a').text() - title += $item.find('.col:nth-child(2)').clone().children().remove().end().text() - - return title.replace('®', '').trim().replace(/,$/, '') -} - -function parseItems(content) { - let data = (content.match(/^[^(]+\(([\s\S]*)\)$/) || [null, null])[1] - if (!data) return [] - let json = JSON.parse(data) - if (!json || !json.data || !json.data.program) return [] - - return [json.data.program] -} - -function buildQuery(site_id, date) { - const query = { - datum: date.format('YYYY-MM-DD'), - kanal: site_id - // callback: 'cb' // possibly still working - } - - let c = 4 - let a = query.datum + query.kanal + c - let ua = query.kanal + query.datum - - if ( - typeof ua === 'undefined' || - ua === null || - ua === '' || - ua === 0 || - ua === '0' || - ua !== ua - ) { - ua = 'none' - } - - for (let j = 0; j < ua.length; j++) c += ua.charCodeAt(j) - - let i = a.length - let b = 2 - while (i--) { - b += (a.charCodeAt(i) + c * 2) * i - } - - b = b.toString() - const lastCharCode = b.charCodeAt(b.length - 1) - const key = 'b' + lastCharCode - query['callback'] = `tvprogramit${lastCharCode}` - query[key] = b - - return new URLSearchParams(query).toString() +const cheerio = require('cheerio') +const dayjs = require('dayjs') + +module.exports = { + site: 'tvprofil.com', + days: 2, + url: function ({ channel, date }) { + const parts = channel.site_id.split('#') + const query = buildQuery(parts[1], date) + + return `https://tvprofil.com/${parts[0]}/program/?${query}` + }, + request: { + headers: { + 'x-requested-with': 'XMLHttpRequest', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36', + 'referer': 'https://tvprofil.com/tvprogram/', + 'accept': 'text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01' + } + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const $ = cheerio.load(item) + $('div.row').each((_, el) => { + const $item = $(el) + const title = parseTitle($item) + const category = parseCategory($item) + const start = parseStart($item) + const duration = parseDuration($item) + const stop = start.add(duration, 's') + const icon = parseImage($item) + + programs.push({ title, category, start, stop, icon }) + }) + }) + + return programs + }, + async channels() { + const axios = require('axios') + + // prettier-ignore + const countries = { + al: { channelsPath: '/al', progsPath: 'al/programacioni', lang: 'sq' }, + at: { channelsPath: '/at', progsPath: 'at/tvprogramm', lang: 'de' }, + ba: { channelsPath: '/ba', progsPath: 'ba/tvprogram', lang: 'bs' }, + bg: { channelsPath: '/bg', progsPath: 'bg/tv-programa', lang: 'bg' }, + ch: { channelsPath: '/ch', progsPath: 'ch/tv-programm', lang: 'de' }, + de: { channelsPath: '/de', progsPath: 'de/tvprogramm', lang: 'de' }, + es: { channelsPath: '/es', progsPath: 'es/programacion-tv', lang: 'es' }, + fr: { channelsPath: '/fr', progsPath: 'fr/programme-tv', lang: 'fr' }, + hr: { channelsPath: '', progsPath: 'tvprogram', lang: 'hr' }, + hu: { channelsPath: '/hu', progsPath: 'hu/tvmusor', lang: 'hu' }, + ie: { channelsPath: '/ie', progsPath: 'ie/tvschedule', lang: 'en' }, + it: { channelsPath: '/it', progsPath: 'it/guida-tv', lang: 'it' }, + ks: { channelsPath: '/ks', progsPath: 'ks/programacioni', lang: 'sq' }, + me: { channelsPath: '/me', progsPath: 'me/tvprogram', lang: 'en' }, + mk: { channelsPath: '/mk', progsPath: 'mk/tv-raspored', lang: 'mk' }, + pl: { channelsPath: '/pl', progsPath: 'pl/program', lang: 'pl' }, + pt: { channelsPath: '/pt', progsPath: 'pt/programacao', lang: 'pt' }, + ro: { channelsPath: '/ro', progsPath: 'ro/program-tv', lang: 'ro' }, + rs: { channelsPath: '/rs', progsPath: 'rs/tvprogram', lang: 'sr' }, + si: { channelsPath: '/si', progsPath: 'si/tvspored', lang: 'sl' }, + tr: { channelsPath: '/tr', progsPath: 'tr/tv-rehberi', lang: 'tr' }, + uk: { channelsPath: '/gb', progsPath: 'gb/tvschedule', lang: 'en' }, + } + + let channels = [] + for (let country in countries) { + const config = countries[country] + const lang = config.lang + + const url = `https://tvprofil.com${config.channelsPath}/channels/getChannels/` + + console.log(url) + + const cb = await axios + .get(url, { + params: { + callback: 'cb' + }, + headers: { + 'x-requested-with': 'XMLHttpRequest', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36', + 'referer': 'https://tvprofil.com/programtv/', + 'accept': 'text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01', + } + }) + .then(r => r.data) + .catch(err => { + console.error(err.message) + }) + + if (!cb) continue + + const [, json] = cb.match(/^cb\((.*)\)$/i) + const data = JSON.parse(json) + + data.data.forEach(group => { + group.channels.forEach(item => { + channels.push({ + lang, + site_id: `${config.progsPath}#${item.urlID}`, + xmltv_id: `${item.title.replaceAll(/[ '&]/g, '')}.${country}`, + name: item.title + }) + }) + }) + } + + return channels + } +} + +function parseImage($item) { + return $item.attr('data-image') || null +} + +function parseDuration($item) { + return parseInt($item.attr('data-len')) +} + +function parseStart($item) { + const timestamp = parseInt($item.attr('data-ts')) + return dayjs.unix(timestamp) +} + +function parseCategory($item) { + return $item.find('.col:nth-child(2) > small').text() || null +} + +function parseTitle($item) { + let title = $item.find('.col:nth-child(2) > a').text() + title += $item.find('.col:nth-child(2)').clone().children().remove().end().text() + + return title.replace('®', '').trim().replace(/,$/, '') +} + +function parseItems(content) { + let data = (content.match(/^[^(]+\(([\s\S]*)\)$/) || [null, null])[1] + if (!data) return [] + let json = JSON.parse(data) + if (!json || !json.data || !json.data.program) return [] + + return [json.data.program] +} + +function buildQuery(site_id, date) { + const query = { + datum: date.format('YYYY-MM-DD'), + kanal: site_id + // callback: 'cb' // possibly still working + } + + let c = 4 + let a = query.datum + query.kanal + c + let ua = query.kanal + query.datum + + if ( + typeof ua === 'undefined' || + ua === null || + ua === '' || + ua === 0 || + ua === '0' || + ua !== ua + ) { + ua = 'none' + } + + for (let j = 0; j < ua.length; j++) c += ua.charCodeAt(j) + + let i = a.length + let b = 2 + while (i--) { + b += (a.charCodeAt(i) + c * 2) * i + } + + b = b.toString() + const lastCharCode = b.charCodeAt(b.length - 1) + const key = 'b' + lastCharCode + query['callback'] = `tvprogramit${lastCharCode}` + query[key] = b + + return new URLSearchParams(query).toString() } \ No newline at end of file diff --git a/sites/tvprofil.com/tvprofil.com.test.js b/sites/tvprofil.com/tvprofil.com.test.js index e25bd71a..44dd4790 100644 --- a/sites/tvprofil.com/tvprofil.com.test.js +++ b/sites/tvprofil.com/tvprofil.com.test.js @@ -1,48 +1,48 @@ -const { parser, url, request } = require('./tvprofil.com.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-07-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'bg/tv-programa#24kitchen-bg', - xmltv_id: '24KitchenBulgaria.bg' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://tvprofil.com/bg/tv-programa/program/?datum=2025-07-29&kanal=24kitchen-bg&callback=tvprogramit48&b48=827670' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'x-requested-with': 'XMLHttpRequest', - 'referer': 'https://tvprofil.com/tvprogram/', - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.txt'), 'utf8') - const results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - title: 'Save with Jamie 1, ep. 2', - start: '2025-07-29T05:00:00.000Z', - stop: '2025-07-29T06:00:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.txt'), 'utf8') - - expect(parser({ content })).toMatchObject([]) -}) +const { parser, url, request } = require('./tvprofil.com.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-07-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'bg/tv-programa#24kitchen-bg', + xmltv_id: '24KitchenBulgaria.bg' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://tvprofil.com/bg/tv-programa/program/?datum=2025-07-29&kanal=24kitchen-bg&callback=tvprogramit48&b48=827670' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'x-requested-with': 'XMLHttpRequest', + 'referer': 'https://tvprofil.com/tvprogram/', + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.txt'), 'utf8') + const results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + title: 'Save with Jamie 1, ep. 2', + start: '2025-07-29T05:00:00.000Z', + stop: '2025-07-29T06:00:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.txt'), 'utf8') + + expect(parser({ content })).toMatchObject([]) +}) diff --git a/sites/tvtv.us/tvtv.us.config.js b/sites/tvtv.us/tvtv.us.config.js index eef953fc..ccb667be 100644 --- a/sites/tvtv.us/tvtv.us.config.js +++ b/sites/tvtv.us/tvtv.us.config.js @@ -1,151 +1,151 @@ -const dayjs = require('dayjs') - -let cachedPrograms = {} - -module.exports = { - site: 'tvtv.us', - days: 2, - url({ date, channel }) { - return `https://www.tvtv.us/api/v1/lineup/USA-NY71652-X/grid/${date.toJSON()}/${date - .add(1, 'day') - .toJSON()}/${channel.site_id}` - }, - request: { - headers: { - Accept: '*/*', - Connection: 'keep-alive', - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', - 'sec-ch-ua': '"Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"', - 'sec-ch-ua-mobile': '?0', - 'sec-ch-ua-platform': '"Windows"' - } - }, - async parser(ctx) { - let programs = [] - let queue = [] - - const items = parseItems(ctx.content) - for (const item of items) { - const start = dayjs(item.startTime) - const stop = start.add(item.duration, 'minute') - - programs.push({ - id: item.programId, - title: item.title, - subtitle: item.subtitle || null, - start, - stop - }) - - // NOTE: This part of the code is commented out because loading additional data leads either to error 429 Too Many Requests or to even greater delays between requests. - // if (item.programId && !cachedPrograms[item.programId]) { - // queue.push({ - // programId: item.programId, - // url: `https://tvtv.us/api/v1/programs/${item.programId}`, - // httpAgent: ctx.request.agent, - // httpsAgent: ctx.request.agent, - // headers: module.exports.request.headers - // }) - // } - } - - const axios = require('axios') - for (const req of queue) { - await wait(5000) - - const data = await axios(req) - .then(r => r.data) - .catch(console.error) - - if (!data || !data.title) continue - - cachedPrograms[req.programId] = data - } - - programs.forEach(program => { - const data = cachedPrograms[program.id] - - if (!data) return - - program.description = data.description || null - program.image = data.image ? `https://tvtv.us${data.image}` : null - program.date = data.releaseYear ? data.releaseYear.toString() : null - program.directors = data.directors - program.categories = data.genres - program.actors = parseActors(data) - program.writers = parseWriters(data) - program.producers = parseProducers(data) - program.ratings = parseRatings(data) - program.season = parseSeason(data) - program.episode = parseEpisode(data) - }) - - return programs - } -} - -function parseEpisode(data) { - if (!data?.seriesEpisode?.seasonEpisode) return null - - const [, episode] = data.seriesEpisode.seasonEpisode.match(/Episode (\d+)/) || [null, null] - - return episode ? parseInt(episode) : null -} - -function parseSeason(data) { - if (!data?.seriesEpisode?.seasonEpisode) return null - - const [, season] = data.seriesEpisode.seasonEpisode.match(/Season (\d+);/) || [null, null] - - return season ? parseInt(season) : null -} - -function parseRatings(data) { - return Array.isArray(data.ratings) - ? data.ratings.map(rating => ({ - value: rating.code, - system: rating.body - })) - : [] -} - -function parseWriters(data) { - return data.crew.filter(member => member.role.includes('Writer')).map(member => member.name) -} - -function parseProducers(data) { - return data.crew.filter(member => member.role.includes('Producer')).map(member => member.name) -} - -function parseActors(data) { - return data.cast.map(actor => { - const guest = actor.role.includes('Guest Star') ? 'yes' : undefined - const role = actor.role.replace(' - Guest Star', '') - - return { - value: actor.name, - role, - guest - } - }) -} - -function parseItems(content) { - try { - const json = JSON.parse(content) - if (!json.length) return [] - - return json[0] - } catch { - return [] - } -} - -function wait(ms) { - if (process.env.NODE_ENV === 'test') return - - return new Promise(resolve => { - setTimeout(resolve, ms) - }) -} +const dayjs = require('dayjs') + +let cachedPrograms = {} + +module.exports = { + site: 'tvtv.us', + days: 2, + url({ date, channel }) { + return `https://www.tvtv.us/api/v1/lineup/USA-NY71652-X/grid/${date.toJSON()}/${date + .add(1, 'day') + .toJSON()}/${channel.site_id}` + }, + request: { + headers: { + Accept: '*/*', + Connection: 'keep-alive', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', + 'sec-ch-ua': '"Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"Windows"' + } + }, + async parser(ctx) { + let programs = [] + let queue = [] + + const items = parseItems(ctx.content) + for (const item of items) { + const start = dayjs(item.startTime) + const stop = start.add(item.duration, 'minute') + + programs.push({ + id: item.programId, + title: item.title, + subtitle: item.subtitle || null, + start, + stop + }) + + // NOTE: This part of the code is commented out because loading additional data leads either to error 429 Too Many Requests or to even greater delays between requests. + // if (item.programId && !cachedPrograms[item.programId]) { + // queue.push({ + // programId: item.programId, + // url: `https://tvtv.us/api/v1/programs/${item.programId}`, + // httpAgent: ctx.request.agent, + // httpsAgent: ctx.request.agent, + // headers: module.exports.request.headers + // }) + // } + } + + const axios = require('axios') + for (const req of queue) { + await wait(5000) + + const data = await axios(req) + .then(r => r.data) + .catch(console.error) + + if (!data || !data.title) continue + + cachedPrograms[req.programId] = data + } + + programs.forEach(program => { + const data = cachedPrograms[program.id] + + if (!data) return + + program.description = data.description || null + program.image = data.image ? `https://tvtv.us${data.image}` : null + program.date = data.releaseYear ? data.releaseYear.toString() : null + program.directors = data.directors + program.categories = data.genres + program.actors = parseActors(data) + program.writers = parseWriters(data) + program.producers = parseProducers(data) + program.ratings = parseRatings(data) + program.season = parseSeason(data) + program.episode = parseEpisode(data) + }) + + return programs + } +} + +function parseEpisode(data) { + if (!data?.seriesEpisode?.seasonEpisode) return null + + const [, episode] = data.seriesEpisode.seasonEpisode.match(/Episode (\d+)/) || [null, null] + + return episode ? parseInt(episode) : null +} + +function parseSeason(data) { + if (!data?.seriesEpisode?.seasonEpisode) return null + + const [, season] = data.seriesEpisode.seasonEpisode.match(/Season (\d+);/) || [null, null] + + return season ? parseInt(season) : null +} + +function parseRatings(data) { + return Array.isArray(data.ratings) + ? data.ratings.map(rating => ({ + value: rating.code, + system: rating.body + })) + : [] +} + +function parseWriters(data) { + return data.crew.filter(member => member.role.includes('Writer')).map(member => member.name) +} + +function parseProducers(data) { + return data.crew.filter(member => member.role.includes('Producer')).map(member => member.name) +} + +function parseActors(data) { + return data.cast.map(actor => { + const guest = actor.role.includes('Guest Star') ? 'yes' : undefined + const role = actor.role.replace(' - Guest Star', '') + + return { + value: actor.name, + role, + guest + } + }) +} + +function parseItems(content) { + try { + const json = JSON.parse(content) + if (!json.length) return [] + + return json[0] + } catch { + return [] + } +} + +function wait(ms) { + if (process.env.NODE_ENV === 'test') return + + return new Promise(resolve => { + setTimeout(resolve, ms) + }) +} diff --git a/sites/tvtv.us/tvtv.us.test.js b/sites/tvtv.us/tvtv.us.test.js index 2a5e6c9a..85b1ddda 100644 --- a/sites/tvtv.us/tvtv.us.test.js +++ b/sites/tvtv.us/tvtv.us.test.js @@ -1,172 +1,172 @@ -const { parser, url } = require('./tvtv.us.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') - -axios.mockImplementation(req => { - if (req.url === 'https://tvtv.us/api/v1/programs/EP009311820269') { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_1.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } -}) - -const date = dayjs.utc('2025-01-30', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: '20373' } - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.tvtv.us/api/v1/lineup/USA-NY71652-X/grid/2025-01-30T00:00:00.000Z/2025-01-31T00:00:00.000Z/20373' - ) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - - let results = await parser({ content, request: { agent: null } }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(33) - expect(results[0]).toMatchObject({ - start: '2025-01-30T00:00:00.000Z', - stop: '2025-01-30T00:30:00.000Z', - title: 'NY Sports Nation Nightly', - subtitle: null - }) - expect(results[1]).toMatchObject({ - start: '2025-01-30T00:30:00.000Z', - stop: '2025-01-30T01:00:00.000Z', - title: 'The Big Bang Theory', - subtitle: 'The Bow Tie Asymmetry' - // description: - // "When Amy's parents and Sheldon's family arrive, everybody is focused on making sure the wedding arrangements go according to plan -- everyone except the bride and groom.", - // image: 'https://tvtv.us/gn/pi/assets/p185554_b_v11_az.jpg?w=240&h=360', - // date: '2018', - // season: 11, - // episode: 24, - // actors: [ - // { - // value: 'Johnny Galecki', - // role: 'Leonard Hofstadter' - // }, - // { - // value: 'Jim Parsons', - // role: 'Sheldon Cooper' - // }, - // { - // value: 'Kaley Cuoco', - // role: 'Penny' - // }, - // { - // value: 'Simon Helberg', - // role: 'Howard Wolowitz' - // }, - // { - // value: 'Kunal Nayyar', - // role: 'Raj Koothrappali' - // }, - // { - // value: 'Mayim Bialik', - // role: 'Amy Farrah Fowler' - // }, - // { - // value: 'Melissa Rauch', - // role: 'Bernadette Rostenkowski' - // }, - // { - // value: 'Kevin Sussman', - // role: 'Stuart', - // guest: 'yes' - // }, - // { - // value: 'Laurie Metcalf', - // role: 'Mary', - // guest: 'yes' - // }, - // { - // value: 'John Ross Bowie', - // role: 'Kripke', - // guest: 'yes' - // }, - // { - // value: 'Wil Wheaton', - // role: 'Himself', - // guest: 'yes' - // }, - // { - // value: 'Brian Posehn', - // role: 'Bert', - // guest: 'yes' - // }, - // { - // value: "Jerry O'Connell", - // role: 'George', - // guest: 'yes' - // }, - // { - // value: 'Courtney Henggeler', - // role: 'Missy', - // guest: 'yes' - // }, - // { - // value: 'Lauren Lapkus', - // role: 'Denise', - // guest: 'yes' - // }, - // { - // value: 'Teller', - // role: 'Mr. Fowler', - // guest: 'yes' - // }, - // { - // value: 'Kathy Bates', - // role: 'Mrs. Fowler', - // guest: 'yes' - // }, - // { - // value: 'Mark Hamill', - // role: 'Himself', - // guest: 'yes' - // } - // ], - // directors: ['Mark Cendrowski'], - // producers: ['Chuck Lorre', 'Bill Prady', 'Steven Molaro'], - // writers: [ - // 'Chuck Lorre', - // 'Steven Molaro', - // 'Maria Ferrari', - // 'Steve Holland', - // 'Eric Kaplan', - // 'Tara Hernandez' - // ], - // categories: ['Sitcom'], - // ratings: [ - // { - // value: 'TVPG', - // system: 'USA Parental Rating' - // } - // ] - }) -}) - -it('can handle empty guide', async () => { - const results = await parser({ - content: '[]', - request: { agent: null } - }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./tvtv.us.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') + +axios.mockImplementation(req => { + if (req.url === 'https://tvtv.us/api/v1/programs/EP009311820269') { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_1.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } +}) + +const date = dayjs.utc('2025-01-30', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: '20373' } + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.tvtv.us/api/v1/lineup/USA-NY71652-X/grid/2025-01-30T00:00:00.000Z/2025-01-31T00:00:00.000Z/20373' + ) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + + let results = await parser({ content, request: { agent: null } }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(33) + expect(results[0]).toMatchObject({ + start: '2025-01-30T00:00:00.000Z', + stop: '2025-01-30T00:30:00.000Z', + title: 'NY Sports Nation Nightly', + subtitle: null + }) + expect(results[1]).toMatchObject({ + start: '2025-01-30T00:30:00.000Z', + stop: '2025-01-30T01:00:00.000Z', + title: 'The Big Bang Theory', + subtitle: 'The Bow Tie Asymmetry' + // description: + // "When Amy's parents and Sheldon's family arrive, everybody is focused on making sure the wedding arrangements go according to plan -- everyone except the bride and groom.", + // image: 'https://tvtv.us/gn/pi/assets/p185554_b_v11_az.jpg?w=240&h=360', + // date: '2018', + // season: 11, + // episode: 24, + // actors: [ + // { + // value: 'Johnny Galecki', + // role: 'Leonard Hofstadter' + // }, + // { + // value: 'Jim Parsons', + // role: 'Sheldon Cooper' + // }, + // { + // value: 'Kaley Cuoco', + // role: 'Penny' + // }, + // { + // value: 'Simon Helberg', + // role: 'Howard Wolowitz' + // }, + // { + // value: 'Kunal Nayyar', + // role: 'Raj Koothrappali' + // }, + // { + // value: 'Mayim Bialik', + // role: 'Amy Farrah Fowler' + // }, + // { + // value: 'Melissa Rauch', + // role: 'Bernadette Rostenkowski' + // }, + // { + // value: 'Kevin Sussman', + // role: 'Stuart', + // guest: 'yes' + // }, + // { + // value: 'Laurie Metcalf', + // role: 'Mary', + // guest: 'yes' + // }, + // { + // value: 'John Ross Bowie', + // role: 'Kripke', + // guest: 'yes' + // }, + // { + // value: 'Wil Wheaton', + // role: 'Himself', + // guest: 'yes' + // }, + // { + // value: 'Brian Posehn', + // role: 'Bert', + // guest: 'yes' + // }, + // { + // value: "Jerry O'Connell", + // role: 'George', + // guest: 'yes' + // }, + // { + // value: 'Courtney Henggeler', + // role: 'Missy', + // guest: 'yes' + // }, + // { + // value: 'Lauren Lapkus', + // role: 'Denise', + // guest: 'yes' + // }, + // { + // value: 'Teller', + // role: 'Mr. Fowler', + // guest: 'yes' + // }, + // { + // value: 'Kathy Bates', + // role: 'Mrs. Fowler', + // guest: 'yes' + // }, + // { + // value: 'Mark Hamill', + // role: 'Himself', + // guest: 'yes' + // } + // ], + // directors: ['Mark Cendrowski'], + // producers: ['Chuck Lorre', 'Bill Prady', 'Steven Molaro'], + // writers: [ + // 'Chuck Lorre', + // 'Steven Molaro', + // 'Maria Ferrari', + // 'Steve Holland', + // 'Eric Kaplan', + // 'Tara Hernandez' + // ], + // categories: ['Sitcom'], + // ratings: [ + // { + // value: 'TVPG', + // system: 'USA Parental Rating' + // } + // ] + }) +}) + +it('can handle empty guide', async () => { + const results = await parser({ + content: '[]', + request: { agent: null } + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/v3.myafn.dodmedia.osd.mil/v3.myafn.dodmedia.osd.mil.config.js b/sites/v3.myafn.dodmedia.osd.mil/v3.myafn.dodmedia.osd.mil.config.js index 3622e954..137b8ac6 100644 --- a/sites/v3.myafn.dodmedia.osd.mil/v3.myafn.dodmedia.osd.mil.config.js +++ b/sites/v3.myafn.dodmedia.osd.mil/v3.myafn.dodmedia.osd.mil.config.js @@ -1,76 +1,76 @@ -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) - -module.exports = { - site: 'v3.myafn.dodmedia.osd.mil', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: function ({ date }) { - return `https://v3.myafn.dodmedia.osd.mil/api/json/32/${date.format( - 'YYYY-MM-DD' - )}@0000/${date.format('YYYY-MM-DD')}@2359/schedule.json` - }, - parser: function ({ content, date, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const start = parseStart(item, date) - const stop = start.add(item.o, 'm') - programs.push({ - title: item.h, - sub_title: item.i, - description: item.l, - rating: parseRating(item), - category: parseCategory(item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://v3.myafn.dodmedia.osd.mil/api/json/32/channels.json') - .then(r => r.data) - .catch(console.log) - - return data.map(item => ({ - site_id: item.Channel, - name: item.Title - })) - } -} - -function parseStart(item) { - return dayjs.utc(item.e, 'YYYY,M,D,H,m,s,0').add(1, 'month') -} - -function parseCategory(item) { - return item.m ? item.m.split(',') : [] -} - -function parseRating(item) { - return item.j - ? { - system: 'MPA', - value: item.j - } - : null -} - -function parseItems(content, channel) { - const items = JSON.parse(content) - if (!Array.isArray(items)) return [] - - return items.filter(i => i.b == channel.site_id) -} +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) + +module.exports = { + site: 'v3.myafn.dodmedia.osd.mil', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: function ({ date }) { + return `https://v3.myafn.dodmedia.osd.mil/api/json/32/${date.format( + 'YYYY-MM-DD' + )}@0000/${date.format('YYYY-MM-DD')}@2359/schedule.json` + }, + parser: function ({ content, date, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const start = parseStart(item, date) + const stop = start.add(item.o, 'm') + programs.push({ + title: item.h, + sub_title: item.i, + description: item.l, + rating: parseRating(item), + category: parseCategory(item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://v3.myafn.dodmedia.osd.mil/api/json/32/channels.json') + .then(r => r.data) + .catch(console.log) + + return data.map(item => ({ + site_id: item.Channel, + name: item.Title + })) + } +} + +function parseStart(item) { + return dayjs.utc(item.e, 'YYYY,M,D,H,m,s,0').add(1, 'month') +} + +function parseCategory(item) { + return item.m ? item.m.split(',') : [] +} + +function parseRating(item) { + return item.j + ? { + system: 'MPA', + value: item.j + } + : null +} + +function parseItems(content, channel) { + const items = JSON.parse(content) + if (!Array.isArray(items)) return [] + + return items.filter(i => i.b == channel.site_id) +} diff --git a/sites/v3.myafn.dodmedia.osd.mil/v3.myafn.dodmedia.osd.mil.test.js b/sites/v3.myafn.dodmedia.osd.mil/v3.myafn.dodmedia.osd.mil.test.js index 21b668fd..988e100e 100644 --- a/sites/v3.myafn.dodmedia.osd.mil/v3.myafn.dodmedia.osd.mil.test.js +++ b/sites/v3.myafn.dodmedia.osd.mil/v3.myafn.dodmedia.osd.mil.test.js @@ -1,55 +1,55 @@ -const { parser, url } = require('./v3.myafn.dodmedia.osd.mil.config.js') -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('2022-10-03', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2', - xmltv_id: 'AFNPrimeAtlantic.us' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://v3.myafn.dodmedia.osd.mil/api/json/32/2022-10-03@0000/2022-10-03@2359/schedule.json' - ) -}) - -it('can parse response', () => { - const content = - '[{"a":566,"b":2,"c":"2022,9,3,3,0,0,0","d":"2022,9,3,4,0,0,0","e":"2022,9,3,3,0,0,0","f":"2022,9,3,4,0,0,0","g":60,"h":"This Week with George Stephanopoulos (ABC)","i":"Episode Title","j":"TV-14","k":false,"l":"Former Clinton White House staffer and current co-anchor of ABC\'s weekday morning news show \\"\\"Good Morning America,\\"\\" George Stephanopoulos and co-anchors Martha Raddatz and Jonathan Karl offer a look at current events with a focus on the politics of the day. Each week\'s show includes interviews with top newsmakers (including some of the nation\'s top political leaders) as well as a roundtable discussion, usually featuring journalists from ABC and other news organizations, of the week\'s happenings. Since 2008, the program has broadcast from a studio at the Newseum in Washington, D.C.","m":"News,Politics,Public affairs,Talk","n":694284445,"o":60,"p":20,"q":true,"r":694285705,"s":null}]' - const result = parser({ content, date, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-10-03T03:00:00.000Z', - stop: '2022-10-03T04:00:00.000Z', - title: 'This Week with George Stephanopoulos (ABC)', - sub_title: 'Episode Title', - description: - 'Former Clinton White House staffer and current co-anchor of ABC\'s weekday morning news show ""Good Morning America,"" George Stephanopoulos and co-anchors Martha Raddatz and Jonathan Karl offer a look at current events with a focus on the politics of the day. Each week\'s show includes interviews with top newsmakers (including some of the nation\'s top political leaders) as well as a roundtable discussion, usually featuring journalists from ABC and other news organizations, of the week\'s happenings. Since 2008, the program has broadcast from a studio at the Newseum in Washington, D.C.', - category: ['News', 'Politics', 'Public affairs', 'Talk'], - rating: { - system: 'MPA', - value: 'TV-14' - } - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: `{ - "Message": "An error has occurred." - }`, - date, - channel - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./v3.myafn.dodmedia.osd.mil.config.js') +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('2022-10-03', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2', + xmltv_id: 'AFNPrimeAtlantic.us' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://v3.myafn.dodmedia.osd.mil/api/json/32/2022-10-03@0000/2022-10-03@2359/schedule.json' + ) +}) + +it('can parse response', () => { + const content = + '[{"a":566,"b":2,"c":"2022,9,3,3,0,0,0","d":"2022,9,3,4,0,0,0","e":"2022,9,3,3,0,0,0","f":"2022,9,3,4,0,0,0","g":60,"h":"This Week with George Stephanopoulos (ABC)","i":"Episode Title","j":"TV-14","k":false,"l":"Former Clinton White House staffer and current co-anchor of ABC\'s weekday morning news show \\"\\"Good Morning America,\\"\\" George Stephanopoulos and co-anchors Martha Raddatz and Jonathan Karl offer a look at current events with a focus on the politics of the day. Each week\'s show includes interviews with top newsmakers (including some of the nation\'s top political leaders) as well as a roundtable discussion, usually featuring journalists from ABC and other news organizations, of the week\'s happenings. Since 2008, the program has broadcast from a studio at the Newseum in Washington, D.C.","m":"News,Politics,Public affairs,Talk","n":694284445,"o":60,"p":20,"q":true,"r":694285705,"s":null}]' + const result = parser({ content, date, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-10-03T03:00:00.000Z', + stop: '2022-10-03T04:00:00.000Z', + title: 'This Week with George Stephanopoulos (ABC)', + sub_title: 'Episode Title', + description: + 'Former Clinton White House staffer and current co-anchor of ABC\'s weekday morning news show ""Good Morning America,"" George Stephanopoulos and co-anchors Martha Raddatz and Jonathan Karl offer a look at current events with a focus on the politics of the day. Each week\'s show includes interviews with top newsmakers (including some of the nation\'s top political leaders) as well as a roundtable discussion, usually featuring journalists from ABC and other news organizations, of the week\'s happenings. Since 2008, the program has broadcast from a studio at the Newseum in Washington, D.C.', + category: ['News', 'Politics', 'Public affairs', 'Talk'], + rating: { + system: 'MPA', + value: 'TV-14' + } + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: `{ + "Message": "An error has occurred." + }`, + date, + channel + }) + expect(result).toMatchObject([]) +}) 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/sites/virginmediatelevision.ie/virginmediatelevision.ie.config.js b/sites/virginmediatelevision.ie/virginmediatelevision.ie.config.js index ad225963..95c544a6 100644 --- a/sites/virginmediatelevision.ie/virginmediatelevision.ie.config.js +++ b/sites/virginmediatelevision.ie/virginmediatelevision.ie.config.js @@ -1,82 +1,82 @@ -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: 'virginmediatelevision.ie', - days: 2, - url({ date }) { - return `https://www.virginmediatelevision.ie/includes/ajax/tv_guide.php?date=${date.format( - 'YYYY-MM-DD' - )}` - }, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1h - } - }, - parser({ content, channel, date }) { - const programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const $item = cheerio.load(item) - let start = parseStart($item, date) - let duration = parseDuration($item) - let stop = start.add(duration, 'm') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - sub_title: parseSubTitle($item), - image: parseImage($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('.info > h2').text().trim() -} - -function parseDescription($item) { - return $item('.info').data('description') -} - -function parseSubTitle($item) { - return $item('.info').data('subtitle') -} - -function parseImage($item) { - return $item('.info').data('image') -} - -function parseStart($item, date) { - const [time] = $item('.info') - .data('time') - .match(/^\d{1,2}\.\d{2}(am|pm)/) || [null] - - if (!time) return null - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD h.mma', 'Europe/London') -} - -function parseDuration($item) { - const duration = $item('.info > .time').data('minutes') - - return duration ? parseInt(duration) : 30 -} - -function parseItems(content, channel) { - const $ = cheerio.load(content) - - return $(`.programs_parent > .programs[data-channel='${channel.site_id}'] > .program`).toArray() -} +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: 'virginmediatelevision.ie', + days: 2, + url({ date }) { + return `https://www.virginmediatelevision.ie/includes/ajax/tv_guide.php?date=${date.format( + 'YYYY-MM-DD' + )}` + }, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1h + } + }, + parser({ content, channel, date }) { + const programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const $item = cheerio.load(item) + let start = parseStart($item, date) + let duration = parseDuration($item) + let stop = start.add(duration, 'm') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + sub_title: parseSubTitle($item), + image: parseImage($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('.info > h2').text().trim() +} + +function parseDescription($item) { + return $item('.info').data('description') +} + +function parseSubTitle($item) { + return $item('.info').data('subtitle') +} + +function parseImage($item) { + return $item('.info').data('image') +} + +function parseStart($item, date) { + const [time] = $item('.info') + .data('time') + .match(/^\d{1,2}\.\d{2}(am|pm)/) || [null] + + if (!time) return null + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD h.mma', 'Europe/London') +} + +function parseDuration($item) { + const duration = $item('.info > .time').data('minutes') + + return duration ? parseInt(duration) : 30 +} + +function parseItems(content, channel) { + const $ = cheerio.load(content) + + return $(`.programs_parent > .programs[data-channel='${channel.site_id}'] > .program`).toArray() +} diff --git a/sites/virginmediatelevision.ie/virginmediatelevision.ie.test.js b/sites/virginmediatelevision.ie/virginmediatelevision.ie.test.js index ad5c067a..9d151415 100644 --- a/sites/virginmediatelevision.ie/virginmediatelevision.ie.test.js +++ b/sites/virginmediatelevision.ie/virginmediatelevision.ie.test.js @@ -1,51 +1,51 @@ -const { parser, url } = require('./virginmediatelevision.ie.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('2023-01-31', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'one', - xmltv_id: 'VirginMediaOne.ie' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://www.virginmediatelevision.ie/includes/ajax/tv_guide.php?date=2023-01-31' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = parser({ content, channel, date }) - 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: '2023-01-31T00:00:00.000Z', - stop: '2023-01-31T01:00:00.000Z', - title: 'Chasing Shadows', - sub_title: '', - description: - 'A detective sergeant and expert in the field of serial killers working for the Missing Persons Bureau tries to protect the general public from evil.', - image: 'https://bcboltvirgin.akamaized.net/player/shows/1498_517x291_1528141264.jpg' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - channel, - content: '' - }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./virginmediatelevision.ie.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('2023-01-31', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'one', + xmltv_id: 'VirginMediaOne.ie' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://www.virginmediatelevision.ie/includes/ajax/tv_guide.php?date=2023-01-31' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = parser({ content, channel, date }) + 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: '2023-01-31T00:00:00.000Z', + stop: '2023-01-31T01:00:00.000Z', + title: 'Chasing Shadows', + sub_title: '', + description: + 'A detective sergeant and expert in the field of serial killers working for the Missing Persons Bureau tries to protect the general public from evil.', + image: 'https://bcboltvirgin.akamaized.net/player/shows/1498_517x291_1528141264.jpg' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + channel, + content: '' + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/virgintvgo.virginmedia.com/virgintvgo.virginmedia.com.config.js b/sites/virgintvgo.virginmedia.com/virgintvgo.virginmedia.com.config.js index dba45f8d..1b38da43 100644 --- a/sites/virgintvgo.virginmedia.com/virgintvgo.virginmedia.com.config.js +++ b/sites/virgintvgo.virginmedia.com/virgintvgo.virginmedia.com.config.js @@ -1,114 +1,114 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const doFetch = require('@ntlab/sfetch') -const debug = require('debug')('site:virgintvgo.virginmedia.com') - -dayjs.extend(utc) - -doFetch.setDebugger(debug) - -const detailedGuide = true - -module.exports = { - site: 'virgintvgo.virginmedia.com', - days: 2, - request: { - cache: { - ttl: 24 * 60 * 60 * 1000 // 1 day - } - }, - url({ date, segment = 0 }) { - return `https://staticqbr-prod-gb.gnp.cloud.virgintvgo.virginmedia.com/eng/web/epg-service-lite/gb/en/events/segments/${date.format( - 'YYYYMMDD' - )}${segment.toString().padStart(2, '0')}0000` - }, - async parser({ content, channel, date }) { - const programs = [] - if (content) { - const items = typeof content === 'string' ? JSON.parse(content) : content - if (Array.isArray(items.entries)) { - // fetch other segments - const queues = [ - module.exports.url({ date, segment: 6 }), - module.exports.url({ date, segment: 12 }), - module.exports.url({ date, segment: 18 }) - ] - await doFetch(queues, (url, res) => { - if (Array.isArray(res.entries)) { - items.entries.push(...res.entries) - } - }) - items.entries - .filter(item => item.channelId === channel.site_id) - .forEach(item => { - if (Array.isArray(item.events)) { - if (detailedGuide) { - queues.push( - ...item.events.map( - event => - `https://spark-prod-gb.gnp.cloud.virgintvgo.virginmedia.com/eng/web/linear-service/v2/replayEvent/${event.id}?returnLinearContent=true&forceLinearResponse=true&language=en` - ) - ) - } else { - item.events.forEach(event => { - programs.push({ - title: event.title, - start: dayjs.utc(event.startTime * 1000), - stop: dayjs.utc(event.endTime * 1000) - }) - }) - } - } - }) - // fetch detailed guide - if (queues.length) { - await doFetch(queues, (url, res) => { - programs.push({ - title: res.title, - subTitle: res.episodeName, - description: res.longDescription ? res.longDescription : res.shortDescription, - category: res.genres, - season: res.seasonNumber, - episode: res.episodeNumber, - country: res.countryOfOrigin, - actor: res.actors, - director: res.directors, - producer: res.producers, - date: res.productionDate, - start: dayjs.utc(res.startTime * 1000), - stop: dayjs.utc(res.endTime * 1000) - }) - }) - } - } - } - - return programs - }, - async channels() { - const channels = [] - const axios = require('axios') - const res = await axios - .get( - 'https://spark-prod-gb.gnp.cloud.virgintvgo.virginmedia.com/eng/web/linear-service/v2/channels?cityId=40980&language=en&productClass=Orion-DASH&platform=web' - ) - .then(r => r.data) - .catch(console.error) - - if (Array.isArray(res)) { - channels.push( - ...res - .filter(item => !item.isHidden) - .map(item => { - return { - lang: 'en', - site_id: item.id, - name: item.name - } - }) - ) - } - - return channels - } -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const doFetch = require('@ntlab/sfetch') +const debug = require('debug')('site:virgintvgo.virginmedia.com') + +dayjs.extend(utc) + +doFetch.setDebugger(debug) + +const detailedGuide = true + +module.exports = { + site: 'virgintvgo.virginmedia.com', + days: 2, + request: { + cache: { + ttl: 24 * 60 * 60 * 1000 // 1 day + } + }, + url({ date, segment = 0 }) { + return `https://staticqbr-prod-gb.gnp.cloud.virgintvgo.virginmedia.com/eng/web/epg-service-lite/gb/en/events/segments/${date.format( + 'YYYYMMDD' + )}${segment.toString().padStart(2, '0')}0000` + }, + async parser({ content, channel, date }) { + const programs = [] + if (content) { + const items = typeof content === 'string' ? JSON.parse(content) : content + if (Array.isArray(items.entries)) { + // fetch other segments + const queues = [ + module.exports.url({ date, segment: 6 }), + module.exports.url({ date, segment: 12 }), + module.exports.url({ date, segment: 18 }) + ] + await doFetch(queues, (url, res) => { + if (Array.isArray(res.entries)) { + items.entries.push(...res.entries) + } + }) + items.entries + .filter(item => item.channelId === channel.site_id) + .forEach(item => { + if (Array.isArray(item.events)) { + if (detailedGuide) { + queues.push( + ...item.events.map( + event => + `https://spark-prod-gb.gnp.cloud.virgintvgo.virginmedia.com/eng/web/linear-service/v2/replayEvent/${event.id}?returnLinearContent=true&forceLinearResponse=true&language=en` + ) + ) + } else { + item.events.forEach(event => { + programs.push({ + title: event.title, + start: dayjs.utc(event.startTime * 1000), + stop: dayjs.utc(event.endTime * 1000) + }) + }) + } + } + }) + // fetch detailed guide + if (queues.length) { + await doFetch(queues, (url, res) => { + programs.push({ + title: res.title, + subTitle: res.episodeName, + description: res.longDescription ? res.longDescription : res.shortDescription, + category: res.genres, + season: res.seasonNumber, + episode: res.episodeNumber, + country: res.countryOfOrigin, + actor: res.actors, + director: res.directors, + producer: res.producers, + date: res.productionDate, + start: dayjs.utc(res.startTime * 1000), + stop: dayjs.utc(res.endTime * 1000) + }) + }) + } + } + } + + return programs + }, + async channels() { + const channels = [] + const axios = require('axios') + const res = await axios + .get( + 'https://spark-prod-gb.gnp.cloud.virgintvgo.virginmedia.com/eng/web/linear-service/v2/channels?cityId=40980&language=en&productClass=Orion-DASH&platform=web' + ) + .then(r => r.data) + .catch(console.error) + + if (Array.isArray(res)) { + channels.push( + ...res + .filter(item => !item.isHidden) + .map(item => { + return { + lang: 'en', + site_id: item.id, + name: item.name + } + }) + ) + } + + return channels + } +} diff --git a/sites/virgintvgo.virginmedia.com/virgintvgo.virginmedia.com.test.js b/sites/virgintvgo.virginmedia.com/virgintvgo.virginmedia.com.test.js index 1fbc3400..683a9266 100644 --- a/sites/virgintvgo.virginmedia.com/virgintvgo.virginmedia.com.test.js +++ b/sites/virgintvgo.virginmedia.com/virgintvgo.virginmedia.com.test.js @@ -1,95 +1,95 @@ -const { parser, url } = require('./virgintvgo.virginmedia.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('2024-12-14', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1958', - xmltv_id: '5ActionHD.uk' -} - -axios.get.mockImplementation(url => { - const urls = { - 'https://staticqbr-prod-gb.gnp.cloud.virgintvgo.virginmedia.com/eng/web/epg-service-lite/gb/en/events/segments/20241214000000': - 'content00.json', - 'https://staticqbr-prod-gb.gnp.cloud.virgintvgo.virginmedia.com/eng/web/epg-service-lite/gb/en/events/segments/20241214060000': - 'content06.json', - 'https://staticqbr-prod-gb.gnp.cloud.virgintvgo.virginmedia.com/eng/web/epg-service-lite/gb/en/events/segments/20241214120000': - 'content12.json', - 'https://staticqbr-prod-gb.gnp.cloud.virgintvgo.virginmedia.com/eng/web/epg-service-lite/gb/en/events/segments/20241214180000': - 'content18.json', - 'https://spark-prod-gb.gnp.cloud.virgintvgo.virginmedia.com/eng/web/linear-service/v2/replayEvent/crid:~~2F~~2Fgn.tv~~2F16647964~~2FEP012911720228,imi:74a552c465e11e5fe6ed7bfae7aeda5b639322ff?returnLinearContent=true&forceLinearResponse=true&language=en': - 'program01.json', - 'https://spark-prod-gb.gnp.cloud.virgintvgo.virginmedia.com/eng/web/linear-service/v2/replayEvent/crid:~~2F~~2Fgn.tv~~2F17641069~~2FEP026460800059,imi:23c363d12af79f43134f4a15b96dd12df81b19ab?returnLinearContent=true&forceLinearResponse=true&language=en': - 'program02.json', - 'https://spark-prod-gb.gnp.cloud.virgintvgo.virginmedia.com/eng/web/linear-service/v2/replayEvent/crid:~~2F~~2Fgn.tv~~2F19221598~~2FSH037146530000~~2F333458689,imi:f1060b3f63cd5399e0f97901b25a85ef71097891?returnLinearContent=true&forceLinearResponse=true&language=en': - 'program03.json' - } - let data = '' - if (urls[url] !== undefined) { - data = fs.readFileSync(path.join(__dirname, '__data__', urls[url])).toString() - if (!urls[url].startsWith('content00')) { - data = JSON.parse(data) - } - } - return Promise.resolve({ data }) -}) - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://staticqbr-prod-gb.gnp.cloud.virgintvgo.virginmedia.com/eng/web/epg-service-lite/gb/en/events/segments/20241214000000' - ) -}) - -it('can parse response', async () => { - const content = await axios - .get(url({ date })) - .then(response => response.data) - .catch(console.error) - const result = (await parser({ content, channel, date })).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result.length).toBe(3) - expect(result[0]).toMatchObject({ - start: '2024-12-14T00:00:00.000Z', - stop: '2024-12-14T01:05:00.000Z', - title: 'Police Interceptors', - description: - 'Eight police cars and the eye in the sky hunt down a high powered Porsche Cayenne that is causing carnage. Undertaking at high speeds and goading the interceptors, the driver even manages to take out several police cars.', - category: ['Reality', 'Crime'], - season: 16, - episode: 1 - }) - expect(result[2]).toMatchObject({ - start: '2024-12-14T22:00:00.000Z', - stop: '2024-12-14T22:05:00.000Z', - title: 'Entertainment News On 5', - description: - 'A daily round-up of showbiz news and gossip from around the world, focusing on celebrities, movies, music and entertainment.', - category: ['News', 'Entertainment'], - season: 46530000, - episode: 333458689, - actor: ['Jamie Burton'] - }) -}) - -it('can handle empty guide', async () => { - const result = await parser({ - content: '', - channel, - date - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./virgintvgo.virginmedia.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('2024-12-14', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1958', + xmltv_id: '5ActionHD.uk' +} + +axios.get.mockImplementation(url => { + const urls = { + 'https://staticqbr-prod-gb.gnp.cloud.virgintvgo.virginmedia.com/eng/web/epg-service-lite/gb/en/events/segments/20241214000000': + 'content00.json', + 'https://staticqbr-prod-gb.gnp.cloud.virgintvgo.virginmedia.com/eng/web/epg-service-lite/gb/en/events/segments/20241214060000': + 'content06.json', + 'https://staticqbr-prod-gb.gnp.cloud.virgintvgo.virginmedia.com/eng/web/epg-service-lite/gb/en/events/segments/20241214120000': + 'content12.json', + 'https://staticqbr-prod-gb.gnp.cloud.virgintvgo.virginmedia.com/eng/web/epg-service-lite/gb/en/events/segments/20241214180000': + 'content18.json', + 'https://spark-prod-gb.gnp.cloud.virgintvgo.virginmedia.com/eng/web/linear-service/v2/replayEvent/crid:~~2F~~2Fgn.tv~~2F16647964~~2FEP012911720228,imi:74a552c465e11e5fe6ed7bfae7aeda5b639322ff?returnLinearContent=true&forceLinearResponse=true&language=en': + 'program01.json', + 'https://spark-prod-gb.gnp.cloud.virgintvgo.virginmedia.com/eng/web/linear-service/v2/replayEvent/crid:~~2F~~2Fgn.tv~~2F17641069~~2FEP026460800059,imi:23c363d12af79f43134f4a15b96dd12df81b19ab?returnLinearContent=true&forceLinearResponse=true&language=en': + 'program02.json', + 'https://spark-prod-gb.gnp.cloud.virgintvgo.virginmedia.com/eng/web/linear-service/v2/replayEvent/crid:~~2F~~2Fgn.tv~~2F19221598~~2FSH037146530000~~2F333458689,imi:f1060b3f63cd5399e0f97901b25a85ef71097891?returnLinearContent=true&forceLinearResponse=true&language=en': + 'program03.json' + } + let data = '' + if (urls[url] !== undefined) { + data = fs.readFileSync(path.join(__dirname, '__data__', urls[url])).toString() + if (!urls[url].startsWith('content00')) { + data = JSON.parse(data) + } + } + return Promise.resolve({ data }) +}) + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://staticqbr-prod-gb.gnp.cloud.virgintvgo.virginmedia.com/eng/web/epg-service-lite/gb/en/events/segments/20241214000000' + ) +}) + +it('can parse response', async () => { + const content = await axios + .get(url({ date })) + .then(response => response.data) + .catch(console.error) + const result = (await parser({ content, channel, date })).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result.length).toBe(3) + expect(result[0]).toMatchObject({ + start: '2024-12-14T00:00:00.000Z', + stop: '2024-12-14T01:05:00.000Z', + title: 'Police Interceptors', + description: + 'Eight police cars and the eye in the sky hunt down a high powered Porsche Cayenne that is causing carnage. Undertaking at high speeds and goading the interceptors, the driver even manages to take out several police cars.', + category: ['Reality', 'Crime'], + season: 16, + episode: 1 + }) + expect(result[2]).toMatchObject({ + start: '2024-12-14T22:00:00.000Z', + stop: '2024-12-14T22:05:00.000Z', + title: 'Entertainment News On 5', + description: + 'A daily round-up of showbiz news and gossip from around the world, focusing on celebrities, movies, music and entertainment.', + category: ['News', 'Entertainment'], + season: 46530000, + episode: 333458689, + actor: ['Jamie Burton'] + }) +}) + +it('can handle empty guide', async () => { + const result = await parser({ + content: '', + channel, + date + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/visionplus.id/visionplus.id.config.js b/sites/visionplus.id/visionplus.id.config.js index e4220ca4..9ad5e31b 100644 --- a/sites/visionplus.id/visionplus.id.config.js +++ b/sites/visionplus.id/visionplus.id.config.js @@ -1,71 +1,71 @@ -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) - -const languages = { en: 'ENG', id: 'IND' } - -module.exports = { - site: 'visionplus.id', - days: 2, - url({ date, channel }) { - return `https://www.visionplus.id/managetv/tvinfo/events/schedule?language=${ - languages[channel.lang] - }&serviceId=${channel.site_id}&start=${date.format('YYYY-MM-DD')}T00%3A00%3A00Z&end=${date - .add(1, 'd') - .format('YYYY-MM-DD')}T00%3A00%3A00Z&view=cd-events-grid-view` - }, - parser({ content, channel }) { - const programs = [] - const json = JSON.parse(content) - if (Array.isArray(json.evs)) { - for (const ev of json.evs) { - if (ev.sid === channel.site_id) { - const title = ev.con && ev.con.loc ? ev.con.loc[0].tit : ev.con.oti - const [, , season, , episode] = title.match(/( S(\d+))?(, Ep (\d+))/) || [ - null, - null, - null, - null, - null - ] - programs.push({ - title, - description: ev.con && ev.con.loc ? ev.con.loc[0].syn : null, - categories: ev.con ? ev.con.categories : null, - season: season ? parseInt(season) : season, - episode: episode ? parseInt(episode) : episode, - start: dayjs(ev.sta), - stop: dayjs(ev.end) - }) - } - } - } - - return programs - }, - async channels({ lang = 'id' }) { - const result = [] - const axios = require('axios') - const json = await axios - .get(`https://www.visionplus.id/managetv/tvinfo/channels/get?language=${languages[lang]}`) - .then(response => response.data) - .catch(console.error) - - if (Array.isArray(json?.chs)) { - for (const ch of json.chs) { - result.push({ - lang, - site_id: ch.sid, - name: ch.loc[0].nam - }) - } - } - - return result - } -} +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) + +const languages = { en: 'ENG', id: 'IND' } + +module.exports = { + site: 'visionplus.id', + days: 2, + url({ date, channel }) { + return `https://www.visionplus.id/managetv/tvinfo/events/schedule?language=${ + languages[channel.lang] + }&serviceId=${channel.site_id}&start=${date.format('YYYY-MM-DD')}T00%3A00%3A00Z&end=${date + .add(1, 'd') + .format('YYYY-MM-DD')}T00%3A00%3A00Z&view=cd-events-grid-view` + }, + parser({ content, channel }) { + const programs = [] + const json = JSON.parse(content) + if (Array.isArray(json.evs)) { + for (const ev of json.evs) { + if (ev.sid === channel.site_id) { + const title = ev.con && ev.con.loc ? ev.con.loc[0].tit : ev.con.oti + const [, , season, , episode] = title.match(/( S(\d+))?(, Ep (\d+))/) || [ + null, + null, + null, + null, + null + ] + programs.push({ + title, + description: ev.con && ev.con.loc ? ev.con.loc[0].syn : null, + categories: ev.con ? ev.con.categories : null, + season: season ? parseInt(season) : season, + episode: episode ? parseInt(episode) : episode, + start: dayjs(ev.sta), + stop: dayjs(ev.end) + }) + } + } + } + + return programs + }, + async channels({ lang = 'id' }) { + const result = [] + const axios = require('axios') + const json = await axios + .get(`https://www.visionplus.id/managetv/tvinfo/channels/get?language=${languages[lang]}`) + .then(response => response.data) + .catch(console.error) + + if (Array.isArray(json?.chs)) { + for (const ch of json.chs) { + result.push({ + lang, + site_id: ch.sid, + name: ch.loc[0].nam + }) + } + } + + return result + } +} diff --git a/sites/visionplus.id/visionplus.id.test.js b/sites/visionplus.id/visionplus.id.test.js index f71f7c6f..0579b669 100644 --- a/sites/visionplus.id/visionplus.id.test.js +++ b/sites/visionplus.id/visionplus.id.test.js @@ -1,73 +1,73 @@ -const { parser, url } = require('./visionplus.id.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) - -jest.mock('axios') - -const date = dayjs.utc('2024-11-24', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '00000000000000000079', - xmltv_id: 'AXN.id', - lang: 'en' -} -const channelId = { ...channel, lang: 'id' } - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.visionplus.id/managetv/tvinfo/events/schedule?language=ENG&serviceId=00000000000000000079&start=2024-11-24T00%3A00%3A00Z&end=2024-11-25T00%3A00%3A00Z&view=cd-events-grid-view' - ) - expect(url({ channel: channelId, date })).toBe( - 'https://www.visionplus.id/managetv/tvinfo/events/schedule?language=IND&serviceId=00000000000000000079&start=2024-11-24T00%3A00%3A00Z&end=2024-11-25T00%3A00%3A00Z&view=cd-events-grid-view' - ) -}) - -it('can parse response', () => { - let content = fs.readFileSync(path.resolve(__dirname, '__data__/content_en.json')) - let results = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(1) - expect(results[0]).toMatchObject({ - start: '2024-11-23T23:30:00.000Z', - stop: '2024-11-24T00:15:00.000Z', - title: 'FBI: Most Wanted S4, Ep 18', - description: - 'After two agents from the Bureau of Land Management go missing while executing a land seizure warrant in Wyoming, the Fugitive Task Force heads west to track them down in an unwelcoming county.', - season: 4, - episode: 18 - }) - - content = fs.readFileSync(path.resolve(__dirname, '__data__/content_id.json')) - results = parser({ content, channel: channelId, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(1) - expect(results[0]).toMatchObject({ - start: '2024-11-23T23:30:00.000Z', - stop: '2024-11-24T00:15:00.000Z', - title: 'FBI: Most Wanted S4, Ep 18', - description: - 'Satgas Buronan pergi ke wilayah barat untuk melacak keberadaan dua petugas Biro Pengelolaan Lahan yang menghilang saat menjalankan perintah penyitaan lahan di negara bagian yang tak ramah, Wyoming.', - season: 4, - episode: 18 - }) -}) - -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 } = require('./visionplus.id.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) + +jest.mock('axios') + +const date = dayjs.utc('2024-11-24', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '00000000000000000079', + xmltv_id: 'AXN.id', + lang: 'en' +} +const channelId = { ...channel, lang: 'id' } + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.visionplus.id/managetv/tvinfo/events/schedule?language=ENG&serviceId=00000000000000000079&start=2024-11-24T00%3A00%3A00Z&end=2024-11-25T00%3A00%3A00Z&view=cd-events-grid-view' + ) + expect(url({ channel: channelId, date })).toBe( + 'https://www.visionplus.id/managetv/tvinfo/events/schedule?language=IND&serviceId=00000000000000000079&start=2024-11-24T00%3A00%3A00Z&end=2024-11-25T00%3A00%3A00Z&view=cd-events-grid-view' + ) +}) + +it('can parse response', () => { + let content = fs.readFileSync(path.resolve(__dirname, '__data__/content_en.json')) + let results = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(1) + expect(results[0]).toMatchObject({ + start: '2024-11-23T23:30:00.000Z', + stop: '2024-11-24T00:15:00.000Z', + title: 'FBI: Most Wanted S4, Ep 18', + description: + 'After two agents from the Bureau of Land Management go missing while executing a land seizure warrant in Wyoming, the Fugitive Task Force heads west to track them down in an unwelcoming county.', + season: 4, + episode: 18 + }) + + content = fs.readFileSync(path.resolve(__dirname, '__data__/content_id.json')) + results = parser({ content, channel: channelId, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(1) + expect(results[0]).toMatchObject({ + start: '2024-11-23T23:30:00.000Z', + stop: '2024-11-24T00:15:00.000Z', + title: 'FBI: Most Wanted S4, Ep 18', + description: + 'Satgas Buronan pergi ke wilayah barat untuk melacak keberadaan dua petugas Biro Pengelolaan Lahan yang menghilang saat menjalankan perintah penyitaan lahan di negara bagian yang tak ramah, Wyoming.', + season: 4, + episode: 18 + }) +}) + +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/sites/vivoplay.com.br/vivoplay.com.br.config.js b/sites/vivoplay.com.br/vivoplay.com.br.config.js index 97ee36db..a6f5b8a1 100644 --- a/sites/vivoplay.com.br/vivoplay.com.br.config.js +++ b/sites/vivoplay.com.br/vivoplay.com.br.config.js @@ -1,68 +1,68 @@ -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') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'vivoplay.com.br', - days: 2, - url({ channel, date }) { - const starttime = date.unix() - const endtime = date.add(1, 'd').unix() - - return `https://contentapi-br.cdn.telefonica.com/25/default/pt-BR/schedules?ca_deviceTypes=null%7C401&ca_channelmaps=779%7Cnull&fields=Pid,Title,Description,ChannelName,ChannelNumber,CallLetter,Start,End,LiveChannelPid,LiveProgramPid,EpgSerieId,SeriesPid,SeriesId,SeasonPid,SeasonNumber,EpisodeNumber,images.videoFrame,images.banner,LiveToVod,AgeRatingPid,forbiddenTechnology,IsSoDisabled&includeRelations=Genre&orderBy=START_TIME%3Aa&filteravailability=false&includeAttributes=ca_cpvrDisable,ca_descriptors,ca_blackout_target,ca_blackout_areas&starttime=${starttime}&endtime=${endtime}&livechannelpids=${channel.site_id}&offset=0&limit=1000` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item['Title'], - description: item['Description'], - season: item['SeasonNumber'] > 0 ? item['SeasonNumber'] : null, - episode: item['EpisodeNumber'] > 0 ? item['EpisodeNumber'] : null, - images: parseImages(item), - start: dayjs.unix(item['Start']), - stop: dayjs.unix(item['End']) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get( - 'https://contentapi-br.cdn.telefonica.com/25/default/pt-BR/contents/all?ca_deviceTypes=401&contentTypes=LCH&ca_active=true&ca_requiresPin=false&includeAttributes=ca_channelmapnumber,ca_devicetypes_qualities,ca_deviceTypes_isPlayback,ca_deviceTypes_isStartOverEnabled,ca_deviceTypes_isPvrPlayback,ca_deviceTypes_isPvrManageable,ca_deviceTypes_isCatchup,ca_channelmaps&includeRelations=ProductDependencies,Media&fields=Pid,Name,ChannelNumber,Dvr,EpgLiveChannelReferenceId,CallLetter,ProviderChannel,LXDChannel,AdvancedCDNServices,CdnBuffer,DefaultLanguageOrders,DistributorId,IsLatencyKey,images.logo,images.icon,UxReference,HasPlaylistExperience,IsHomeBlocked,IsStoverFfwdDisabled,IsStoverRwdDisabled,IsCpvrFfwdDisabled,IsCpvrRwdDisabled,IsCatchupFfwdDisabled,IsCatchupRwdDisabled,IsCowatchEnabled,IsFastChannel,MaxLiveNowGap&orderBy=contentOrder&offset=0&limit=1000' - ) - .then(r => r.data) - .catch(console.error) - - return data['Content']['List'].map(channel => ({ - lang: 'pt', - name: channel['Name'], - site_id: channel['Pid'].toLowerCase() - })) - } -} - -function parseImages(item) { - return item['Images'] && Array.isArray(item['Images']['VideoFrame']) - ? item['Images']['VideoFrame'].map(vf => vf['Url']) - : [] -} - -function parseItems(content) { - try { - const data = JSON.parse(content) - if (!data || !Array.isArray(data['Content'])) return [] - - return data['Content'] - } catch { - return [] - } -} +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') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'vivoplay.com.br', + days: 2, + url({ channel, date }) { + const starttime = date.unix() + const endtime = date.add(1, 'd').unix() + + return `https://contentapi-br.cdn.telefonica.com/25/default/pt-BR/schedules?ca_deviceTypes=null%7C401&ca_channelmaps=779%7Cnull&fields=Pid,Title,Description,ChannelName,ChannelNumber,CallLetter,Start,End,LiveChannelPid,LiveProgramPid,EpgSerieId,SeriesPid,SeriesId,SeasonPid,SeasonNumber,EpisodeNumber,images.videoFrame,images.banner,LiveToVod,AgeRatingPid,forbiddenTechnology,IsSoDisabled&includeRelations=Genre&orderBy=START_TIME%3Aa&filteravailability=false&includeAttributes=ca_cpvrDisable,ca_descriptors,ca_blackout_target,ca_blackout_areas&starttime=${starttime}&endtime=${endtime}&livechannelpids=${channel.site_id}&offset=0&limit=1000` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item['Title'], + description: item['Description'], + season: item['SeasonNumber'] > 0 ? item['SeasonNumber'] : null, + episode: item['EpisodeNumber'] > 0 ? item['EpisodeNumber'] : null, + images: parseImages(item), + start: dayjs.unix(item['Start']), + stop: dayjs.unix(item['End']) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get( + 'https://contentapi-br.cdn.telefonica.com/25/default/pt-BR/contents/all?ca_deviceTypes=401&contentTypes=LCH&ca_active=true&ca_requiresPin=false&includeAttributes=ca_channelmapnumber,ca_devicetypes_qualities,ca_deviceTypes_isPlayback,ca_deviceTypes_isStartOverEnabled,ca_deviceTypes_isPvrPlayback,ca_deviceTypes_isPvrManageable,ca_deviceTypes_isCatchup,ca_channelmaps&includeRelations=ProductDependencies,Media&fields=Pid,Name,ChannelNumber,Dvr,EpgLiveChannelReferenceId,CallLetter,ProviderChannel,LXDChannel,AdvancedCDNServices,CdnBuffer,DefaultLanguageOrders,DistributorId,IsLatencyKey,images.logo,images.icon,UxReference,HasPlaylistExperience,IsHomeBlocked,IsStoverFfwdDisabled,IsStoverRwdDisabled,IsCpvrFfwdDisabled,IsCpvrRwdDisabled,IsCatchupFfwdDisabled,IsCatchupRwdDisabled,IsCowatchEnabled,IsFastChannel,MaxLiveNowGap&orderBy=contentOrder&offset=0&limit=1000' + ) + .then(r => r.data) + .catch(console.error) + + return data['Content']['List'].map(channel => ({ + lang: 'pt', + name: channel['Name'], + site_id: channel['Pid'].toLowerCase() + })) + } +} + +function parseImages(item) { + return item['Images'] && Array.isArray(item['Images']['VideoFrame']) + ? item['Images']['VideoFrame'].map(vf => vf['Url']) + : [] +} + +function parseItems(content) { + try { + const data = JSON.parse(content) + if (!data || !Array.isArray(data['Content'])) return [] + + return data['Content'] + } catch { + return [] + } +} diff --git a/sites/vivoplay.com.br/vivoplay.com.br.test.js b/sites/vivoplay.com.br/vivoplay.com.br.test.js index 17d607ff..8f4f1f69 100644 --- a/sites/vivoplay.com.br/vivoplay.com.br.test.js +++ b/sites/vivoplay.com.br/vivoplay.com.br.test.js @@ -1,61 +1,61 @@ -const { parser, url } = require('./vivoplay.com.br.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-19', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'lch5554', - xmltv_id: 'TVNovoTempo.br' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://contentapi-br.cdn.telefonica.com/25/default/pt-BR/schedules?ca_deviceTypes=null%7C401&ca_channelmaps=779%7Cnull&fields=Pid,Title,Description,ChannelName,ChannelNumber,CallLetter,Start,End,LiveChannelPid,LiveProgramPid,EpgSerieId,SeriesPid,SeriesId,SeasonPid,SeasonNumber,EpisodeNumber,images.videoFrame,images.banner,LiveToVod,AgeRatingPid,forbiddenTechnology,IsSoDisabled&includeRelations=Genre&orderBy=START_TIME%3Aa&filteravailability=false&includeAttributes=ca_cpvrDisable,ca_descriptors,ca_blackout_target,ca_blackout_areas&starttime=1737244800&endtime=1737331200&livechannelpids=lch5554&offset=0&limit=1000' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(44) - expect(results[0]).toMatchObject({ - start: '2025-01-19T00:00:00.000Z', - stop: '2025-01-19T00:30:00.000Z', - title: 'Reavivados para a Missão', - description: - 'Tudo sobre a missão com o Pastor Ted Wilson, líder mundial da Igreja Adventista do Sétimo Dia.', - season: 25, - episode: 3, - images: [ - 'http://media.gvp.telefonica.com/storageArea0/IMAGES/00/21/19/21190052_46294A7A7B0DF467.jpg' - ] - }) - expect(results[43]).toMatchObject({ - start: '2025-01-19T23:30:00.000Z', - stop: '2025-01-20T00:00:00.000Z', - title: 'A Máquina Humana', - description: - 'O documentário explora a complexidade e a perfeição do corpo humano por meio de uma série de analogias com máquinas, olhando para a questão da saúde através das perspectivas da ciência, tecnologia e espiritualidade.', - season: null, - episode: null, - images: [ - 'http://media.gvp.telefonica.com/storageArea0/IMAGES/00/20/86/20864769_7DD013A4CCCF7899.jpg' - ] - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const results = parser({ content }) - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./vivoplay.com.br.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-19', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'lch5554', + xmltv_id: 'TVNovoTempo.br' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://contentapi-br.cdn.telefonica.com/25/default/pt-BR/schedules?ca_deviceTypes=null%7C401&ca_channelmaps=779%7Cnull&fields=Pid,Title,Description,ChannelName,ChannelNumber,CallLetter,Start,End,LiveChannelPid,LiveProgramPid,EpgSerieId,SeriesPid,SeriesId,SeasonPid,SeasonNumber,EpisodeNumber,images.videoFrame,images.banner,LiveToVod,AgeRatingPid,forbiddenTechnology,IsSoDisabled&includeRelations=Genre&orderBy=START_TIME%3Aa&filteravailability=false&includeAttributes=ca_cpvrDisable,ca_descriptors,ca_blackout_target,ca_blackout_areas&starttime=1737244800&endtime=1737331200&livechannelpids=lch5554&offset=0&limit=1000' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(44) + expect(results[0]).toMatchObject({ + start: '2025-01-19T00:00:00.000Z', + stop: '2025-01-19T00:30:00.000Z', + title: 'Reavivados para a Missão', + description: + 'Tudo sobre a missão com o Pastor Ted Wilson, líder mundial da Igreja Adventista do Sétimo Dia.', + season: 25, + episode: 3, + images: [ + 'http://media.gvp.telefonica.com/storageArea0/IMAGES/00/21/19/21190052_46294A7A7B0DF467.jpg' + ] + }) + expect(results[43]).toMatchObject({ + start: '2025-01-19T23:30:00.000Z', + stop: '2025-01-20T00:00:00.000Z', + title: 'A Máquina Humana', + description: + 'O documentário explora a complexidade e a perfeição do corpo humano por meio de uma série de analogias com máquinas, olhando para a questão da saúde através das perspectivas da ciência, tecnologia e espiritualidade.', + season: null, + episode: null, + images: [ + 'http://media.gvp.telefonica.com/storageArea0/IMAGES/00/20/86/20864769_7DD013A4CCCF7899.jpg' + ] + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const results = parser({ content }) + expect(results).toMatchObject([]) +}) diff --git a/sites/vtm.be/vtm.be.config.js b/sites/vtm.be/vtm.be.config.js index c919c8ad..d4cf37d3 100644 --- a/sites/vtm.be/vtm.be.config.js +++ b/sites/vtm.be/vtm.be.config.js @@ -1,48 +1,48 @@ -const dayjs = require('dayjs') -const isBetween = require('dayjs/plugin/isBetween') - -dayjs.extend(isBetween) - -module.exports = { - site: 'vtm.be', - days: 2, - url: function ({ channel }) { - return `https://vtm.be/tv-gids/${channel.site_id}` - }, - request: { - headers: { - Cookie: - 'ak_bmsc=8103DDA2C2C37ECD922124463C746A4C~000000000000000000000000000000~YAAQNwVJF7ndI+p8AQAAYDkcCg0mQAkQ2jDHjSfnXl9VIGnzditECZ1FDj1Yi72a8rv/Q454lDDY0Dm3TPqxJUuNLzxJGmgkLmei4IIIwzKJWbB6wC/FMQApoI1NbGz+tUErryic1HWdbZ2dz1IX+AkOHJ9RVupYG5GmkSEQdFG1+/dSZoBMWEeb/5VOCLmNXRDP7k8LnSXaIuKqp5c2MQB+uQ9DdHUd6bIje3dzuxbka9+nJZ+eX/pNbgWI41X2tiXLvPZKh91Tk9k98zrK0pwBnGpTJqDVxmafYH/CjkXoLgEUW3loZfgL9SqddG706a4LnRPhyLzW6W6SH7Q0QOFE4g54NKADVttS2gbXgVrICvo0bb0FAESaFjc5uDyOd+fV2XBGzw==; authId=54da9bc2-d387-4923-8773-3d33ec68710e; gtm_session=1; _sp_ses.417f=*; _ga=GA1.2.525677035.1636552212; _gid=GA1.2.386833723.1636552212; tcf20_purposes=functional|analytics|targeted_advertising|non-personalised_ads|personalisation|marketing|social_media|advertising_1|advertising_2|advertising_3|advertising_4|advertising_7|advertising_9|advertising_10; _gcl_au=1.1.112810754.1636552212; _gat_UA-538372-57=1; sp=4a32f074-5526-4654-9389-2516d799ec68; _gat_UA-6602938-21=1; _sp_id.417f=0c81a857-09dc-47c2-8e51-4fed976211c4.1636552212.1.1636552214.1636552212.55934f90-4bad-47ff-8c5e-cf904126dcfb; bm_sv=1A45EF31D80D05B688C17EAD85964E29~hFpINNxpFphfJ2LLPoLQTauvUpyAf3kaTeGZAMfI/UTMlTRFjoAGBQJPEUPvSw3rXw/swqqAICc74l56pEBVSw6aJYqaoRaiRAZXyWZzQ6jAoeP5SMsZwtvNzYQ3aJXVWM8W8a98J0trlnSjIIsRPQ==' - } - }, - parser: function ({ content, date }) { - let programs = [] - const data = parseContent(content) - const items = parseItems(data, date) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.synopsis, - category: item.genre, - image: item.imageUrl, - start: dayjs(item.from).toJSON(), - stop: dayjs(item.to).toJSON() - }) - }) - - return programs - } -} - -function parseContent(content) { - const [, json] = content.match(/window.__EPG_REDUX_DATA__=(.*);\n/i) || [null, null] - const data = JSON.parse(json) - - return data -} - -function parseItems(data, date) { - if (!data || !data.broadcasts) return [] - - return Object.values(data.broadcasts).filter(i => dayjs(i.from).isBetween(date, date.add(1, 'd'))) -} +const dayjs = require('dayjs') +const isBetween = require('dayjs/plugin/isBetween') + +dayjs.extend(isBetween) + +module.exports = { + site: 'vtm.be', + days: 2, + url: function ({ channel }) { + return `https://vtm.be/tv-gids/${channel.site_id}` + }, + request: { + headers: { + Cookie: + 'ak_bmsc=8103DDA2C2C37ECD922124463C746A4C~000000000000000000000000000000~YAAQNwVJF7ndI+p8AQAAYDkcCg0mQAkQ2jDHjSfnXl9VIGnzditECZ1FDj1Yi72a8rv/Q454lDDY0Dm3TPqxJUuNLzxJGmgkLmei4IIIwzKJWbB6wC/FMQApoI1NbGz+tUErryic1HWdbZ2dz1IX+AkOHJ9RVupYG5GmkSEQdFG1+/dSZoBMWEeb/5VOCLmNXRDP7k8LnSXaIuKqp5c2MQB+uQ9DdHUd6bIje3dzuxbka9+nJZ+eX/pNbgWI41X2tiXLvPZKh91Tk9k98zrK0pwBnGpTJqDVxmafYH/CjkXoLgEUW3loZfgL9SqddG706a4LnRPhyLzW6W6SH7Q0QOFE4g54NKADVttS2gbXgVrICvo0bb0FAESaFjc5uDyOd+fV2XBGzw==; authId=54da9bc2-d387-4923-8773-3d33ec68710e; gtm_session=1; _sp_ses.417f=*; _ga=GA1.2.525677035.1636552212; _gid=GA1.2.386833723.1636552212; tcf20_purposes=functional|analytics|targeted_advertising|non-personalised_ads|personalisation|marketing|social_media|advertising_1|advertising_2|advertising_3|advertising_4|advertising_7|advertising_9|advertising_10; _gcl_au=1.1.112810754.1636552212; _gat_UA-538372-57=1; sp=4a32f074-5526-4654-9389-2516d799ec68; _gat_UA-6602938-21=1; _sp_id.417f=0c81a857-09dc-47c2-8e51-4fed976211c4.1636552212.1.1636552214.1636552212.55934f90-4bad-47ff-8c5e-cf904126dcfb; bm_sv=1A45EF31D80D05B688C17EAD85964E29~hFpINNxpFphfJ2LLPoLQTauvUpyAf3kaTeGZAMfI/UTMlTRFjoAGBQJPEUPvSw3rXw/swqqAICc74l56pEBVSw6aJYqaoRaiRAZXyWZzQ6jAoeP5SMsZwtvNzYQ3aJXVWM8W8a98J0trlnSjIIsRPQ==' + } + }, + parser: function ({ content, date }) { + let programs = [] + const data = parseContent(content) + const items = parseItems(data, date) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.synopsis, + category: item.genre, + image: item.imageUrl, + start: dayjs(item.from).toJSON(), + stop: dayjs(item.to).toJSON() + }) + }) + + return programs + } +} + +function parseContent(content) { + const [, json] = content.match(/window.__EPG_REDUX_DATA__=(.*);\n/i) || [null, null] + const data = JSON.parse(json) + + return data +} + +function parseItems(data, date) { + if (!data || !data.broadcasts) return [] + + return Object.values(data.broadcasts).filter(i => dayjs(i.from).isBetween(date, date.add(1, 'd'))) +} diff --git a/sites/vtm.be/vtm.be.test.js b/sites/vtm.be/vtm.be.test.js index 0ea8d2b2..5c2b8c20 100644 --- a/sites/vtm.be/vtm.be.test.js +++ b/sites/vtm.be/vtm.be.test.js @@ -1,44 +1,44 @@ -const { parser, url } = require('./vtm.be.config.js') -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('2021-11-10', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'vtm', - xmltv_id: 'VTM.be' -} -const content = ` ` - -it('can generate valid url', () => { - const result = url({ channel }) - expect(result).toBe('https://vtm.be/tv-gids/vtm') -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: '2021-11-10T23:45:00.000Z', - stop: '2021-11-11T00:20:00.000Z', - title: 'Wooninspiraties', - image: - 'https://images4.persgroep.net/rcs/z5qrZHumkjuN5rWzoaRJ_BTdL7A/diocontent/209688322/_fill/600/400?appId=da11c75db9b73ea0f41f0cd0da631c71', - description: - 'Een team gaat op pad om inspiratie op te doen over alles wat met wonen en leven te maken heeft; Ze trekken heel het land door om de laatste trends en tips op het gebied van wonen te achterhalen.', - category: 'Magazine' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: ' ' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./vtm.be.config.js') +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('2021-11-10', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'vtm', + xmltv_id: 'VTM.be' +} +const content = ` ` + +it('can generate valid url', () => { + const result = url({ channel }) + expect(result).toBe('https://vtm.be/tv-gids/vtm') +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: '2021-11-10T23:45:00.000Z', + stop: '2021-11-11T00:20:00.000Z', + title: 'Wooninspiraties', + image: + 'https://images4.persgroep.net/rcs/z5qrZHumkjuN5rWzoaRJ_BTdL7A/diocontent/209688322/_fill/600/400?appId=da11c75db9b73ea0f41f0cd0da631c71', + description: + 'Een team gaat op pad om inspiratie op te doen over alles wat met wonen en leven te maken heeft; Ze trekken heel het land door om de laatste trends en tips op het gebied van wonen te achterhalen.', + category: 'Magazine' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: ' ' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/walesi.com.fj/walesi.com.fj.config.js b/sites/walesi.com.fj/walesi.com.fj.config.js index 156582ad..4b715edc 100644 --- a/sites/walesi.com.fj/walesi.com.fj.config.js +++ b/sites/walesi.com.fj/walesi.com.fj.config.js @@ -1,91 +1,91 @@ -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') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'walesi.com.fj', - days: 2, - url: 'https://www.walesi.com.fj/wp-admin/admin-ajax.php', - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }, - data({ channel, date }) { - const params = new URLSearchParams() - params.append('chanel', channel.site_id) - params.append('date', date.unix()) - params.append('action', 'extvs_get_schedule_simple') - - return params - } - }, - parser({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - const start = parseStart($item, date) - const stop = start.add(30, 'm') - if (prev) prev.stop = start - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://www.walesi.com.fj/channel-guide/') - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(data) - const channels = $( - 'div.ex-chanel-list > div.extvs-inline-chanel > ul > li.extvs-inline-select' - ).toArray() - return channels.map(item => { - const $item = cheerio.load(item) - const [, name] = $item('span') - .text() - .trim() - .match(/\d+\. (.*)/) || [null, null] - return { - lang: 'fj', - site_id: $item('*').data('value'), - name - } - }) - } -} - -function parseTitle($item) { - return $item('td.extvs-table1-programme > div > div > figure > h3').text() -} - -function parseStart($item, date) { - let time = $item('td.extvs-table1-time > span').text().trim() - if (!time) return null - time = `${date.format('YYYY-MM-DD')} ${time}` - - return dayjs.tz(time, 'YYYY-MM-DD H:mm a', 'Pacific/Fiji') -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data.html) return [] - const $ = cheerio.load(data.html) - - return $('table > tbody > tr').toArray() -} +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') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'walesi.com.fj', + days: 2, + url: 'https://www.walesi.com.fj/wp-admin/admin-ajax.php', + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }, + data({ channel, date }) { + const params = new URLSearchParams() + params.append('chanel', channel.site_id) + params.append('date', date.unix()) + params.append('action', 'extvs_get_schedule_simple') + + return params + } + }, + parser({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + const start = parseStart($item, date) + const stop = start.add(30, 'm') + if (prev) prev.stop = start + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://www.walesi.com.fj/channel-guide/') + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(data) + const channels = $( + 'div.ex-chanel-list > div.extvs-inline-chanel > ul > li.extvs-inline-select' + ).toArray() + return channels.map(item => { + const $item = cheerio.load(item) + const [, name] = $item('span') + .text() + .trim() + .match(/\d+\. (.*)/) || [null, null] + return { + lang: 'fj', + site_id: $item('*').data('value'), + name + } + }) + } +} + +function parseTitle($item) { + return $item('td.extvs-table1-programme > div > div > figure > h3').text() +} + +function parseStart($item, date) { + let time = $item('td.extvs-table1-time > span').text().trim() + if (!time) return null + time = `${date.format('YYYY-MM-DD')} ${time}` + + return dayjs.tz(time, 'YYYY-MM-DD H:mm a', 'Pacific/Fiji') +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data.html) return [] + const $ = cheerio.load(data.html) + + return $('table > tbody > tr').toArray() +} diff --git a/sites/walesi.com.fj/walesi.com.fj.test.js b/sites/walesi.com.fj/walesi.com.fj.test.js index 61d18a17..6c7ee103 100644 --- a/sites/walesi.com.fj/walesi.com.fj.test.js +++ b/sites/walesi.com.fj/walesi.com.fj.test.js @@ -1,65 +1,65 @@ -const { parser, url, request } = require('./walesi.com.fj.config.js') -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('2021-11-21', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'fbc-2', - xmltv_id: 'FBCTV.fj' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.walesi.com.fj/wp-admin/admin-ajax.php') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }) -}) - -it('can generate valid request data', () => { - const result = request.data({ date, channel }) - expect(result.has('chanel')).toBe(true) - expect(result.has('date')).toBe(true) - expect(result.has('action')).toBe(true) -}) - -it('can parse response', () => { - const content = - '{"html":"\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\r\\n\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n\\r\\n\\t\\r\\n\\t\\r\\n\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n
      ImageTimeProgramme
      \\r\\n\\t\\t\\t12:00 am\\r\\n\\t\\t
      \\r\\n\\t\\t\\t
      \\r\\n\\t\\t\\t\\t
      \\r\\n\\t\\t\\t\\t\\t
      \\r\\n\\t\\t\\t\\t\\t

      Aljazeera

      \\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t
      \\r\\n\\t\\t\\t
      \\r\\n\\t\\t\\t\\t\\t
      \\r\\n\\t
      6:00 am

      Move Fiji

      \\r\\n\\t\\t\\t\\t"}' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-20T12:00:00.000Z', - stop: '2021-11-20T18:00:00.000Z', - title: 'Aljazeera' - }, - { - start: '2021-11-20T18:00:00.000Z', - stop: '2021-11-20T18:30:00.000Z', - title: 'Move Fiji' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"html":"

      No matching records found

      "}' - }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./walesi.com.fj.config.js') +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('2021-11-21', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'fbc-2', + xmltv_id: 'FBCTV.fj' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.walesi.com.fj/wp-admin/admin-ajax.php') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }) +}) + +it('can generate valid request data', () => { + const result = request.data({ date, channel }) + expect(result.has('chanel')).toBe(true) + expect(result.has('date')).toBe(true) + expect(result.has('action')).toBe(true) +}) + +it('can parse response', () => { + const content = + '{"html":"\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\r\\n\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n\\r\\n\\t\\r\\n\\t\\r\\n\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n
      ImageTimeProgramme
      \\r\\n\\t\\t\\t12:00 am\\r\\n\\t\\t
      \\r\\n\\t\\t\\t
      \\r\\n\\t\\t\\t\\t
      \\r\\n\\t\\t\\t\\t\\t
      \\r\\n\\t\\t\\t\\t\\t

      Aljazeera

      \\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t
      \\r\\n\\t\\t\\t
      \\r\\n\\t\\t\\t\\t\\t
      \\r\\n\\t
      6:00 am

      Move Fiji

      \\r\\n\\t\\t\\t\\t"}' + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-20T12:00:00.000Z', + stop: '2021-11-20T18:00:00.000Z', + title: 'Aljazeera' + }, + { + start: '2021-11-20T18:00:00.000Z', + stop: '2021-11-20T18:30:00.000Z', + title: 'Move Fiji' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"html":"

      No matching records found

      "}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/watch.sportsnet.ca/watch.sportsnet.ca.config.js b/sites/watch.sportsnet.ca/watch.sportsnet.ca.config.js index a3e956e9..b95277eb 100644 --- a/sites/watch.sportsnet.ca/watch.sportsnet.ca.config.js +++ b/sites/watch.sportsnet.ca/watch.sportsnet.ca.config.js @@ -1,69 +1,69 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'watch.sportsnet.ca', - days: 2, - url: function ({ channel, date }) { - return `https://production-cdn.sportsnet.ca/api/schedules?channels=${ - channel.site_id - }&date=${date.format('YYYY-MM-DD')}&duration=24&hour=0` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.item.title, - description: item.item.shortDescription, - image: parseImage(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const axios = require('axios') - const html = await axios - .get('https://watch.sportsnet.ca/schedule/tvlistings') - .then(r => r.data) - .catch(console.log) - - let [, __data] = html.match(/window\.__data = ([^<]+)<\/script>/) - const func = new Function(`"use strict";return ${__data}`) - const data = func() - - return data.cache.list['678|page_size=24'].list.items.map(item => { - return { - lang: 'en', - site_id: item.id, - name: item.title - } - }) - } -} - -function parseImage(item) { - if (!item.item || !item.item.images) return null - - return item.item.images.tile -} - -function parseStart(item) { - return dayjs.utc(item.startDate) -} - -function parseStop(item) { - return dayjs.utc(item.endDate) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!Array.isArray(data) || !Array.isArray(data[0].schedules)) return [] - - return data[0].schedules -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'watch.sportsnet.ca', + days: 2, + url: function ({ channel, date }) { + return `https://production-cdn.sportsnet.ca/api/schedules?channels=${ + channel.site_id + }&date=${date.format('YYYY-MM-DD')}&duration=24&hour=0` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.item.title, + description: item.item.shortDescription, + image: parseImage(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const axios = require('axios') + const html = await axios + .get('https://watch.sportsnet.ca/schedule/tvlistings') + .then(r => r.data) + .catch(console.log) + + let [, __data] = html.match(/window\.__data = ([^<]+)<\/script>/) + const func = new Function(`"use strict";return ${__data}`) + const data = func() + + return data.cache.list['678|page_size=24'].list.items.map(item => { + return { + lang: 'en', + site_id: item.id, + name: item.title + } + }) + } +} + +function parseImage(item) { + if (!item.item || !item.item.images) return null + + return item.item.images.tile +} + +function parseStart(item) { + return dayjs.utc(item.startDate) +} + +function parseStop(item) { + return dayjs.utc(item.endDate) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!Array.isArray(data) || !Array.isArray(data[0].schedules)) return [] + + return data[0].schedules +} diff --git a/sites/watch.sportsnet.ca/watch.sportsnet.ca.test.js b/sites/watch.sportsnet.ca/watch.sportsnet.ca.test.js index 130e27ac..63fe3191 100644 --- a/sites/watch.sportsnet.ca/watch.sportsnet.ca.test.js +++ b/sites/watch.sportsnet.ca/watch.sportsnet.ca.test.js @@ -1,48 +1,48 @@ -const { parser, url } = require('./watch.sportsnet.ca.config.js') -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('2022-03-14', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '24533', - xmltv_id: 'SportsNetOntario.ca' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://production-cdn.sportsnet.ca/api/schedules?channels=24533&date=2022-03-14&duration=24&hour=0' - ) -}) - -it('can parse response', () => { - const content = - '[{"channelId":"24533","startDate":"2022-03-14T00:00:00.000Z","endDate":"2022-03-15T00:00:00.000Z","schedules":[{"channelId":"24533","customFields":{"ContentId":"EP029977175139","Checksum":"2DA90E7E66B9C311F98B186B89C50FAD"},"endDate":"2022-03-14T02:30:00Z","id":"826cb731-9de4-4cf3-bcca-d548d8a33d16","startDate":"2022-03-14T00:00:00Z","item":{"id":"34a028b0-eacf-40f3-9bf9-62ee3330df1b","type":"program","title":"Calgary Flames at Colorado Avalanche","shortDescription":"Johnny Gaudreau and the Flames pay a visit to the Avalanche. Calgary won 4-3 in overtime March 5.","path":"/channel/24533","duration":9000,"images":{"tile":"https://production-static.sportsnet-static.com/shain/v1/dataservice/ResizeImage/$value?Format=\'jpg\'&Quality=85&ImageId=\'785305\'&EntityType=\'LinearSchedule\'&EntityId=\'826cb731-9de4-4cf3-bcca-d548d8a33d16\'&Width=3840&Height=2160","wallpaper":"https://production-static.sportsnet-static.com/shain/v1/dataservice/ResizeImage/$value?Format=\'jpg\'&Quality=85&ImageId=\'785311\'&EntityType=\'LinearSchedule\'&EntityId=\'826cb731-9de4-4cf3-bcca-d548d8a33d16\'&Width=3840&Height=2160"}}}]}]' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-14T00:00:00.000Z', - stop: '2022-03-14T02:30:00.000Z', - title: 'Calgary Flames at Colorado Avalanche', - description: - 'Johnny Gaudreau and the Flames pay a visit to the Avalanche. Calgary won 4-3 in overtime March 5.', - image: - "https://production-static.sportsnet-static.com/shain/v1/dataservice/ResizeImage/$value?Format='jpg'&Quality=85&ImageId='785305'&EntityType='LinearSchedule'&EntityId='826cb731-9de4-4cf3-bcca-d548d8a33d16'&Width=3840&Height=2160" - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: - '[{"channelId":"245321","startDate":"2022-03-14T00:00:00.000Z","endDate":"2022-03-15T00:00:00.000Z","schedules":[]}]' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./watch.sportsnet.ca.config.js') +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('2022-03-14', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '24533', + xmltv_id: 'SportsNetOntario.ca' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://production-cdn.sportsnet.ca/api/schedules?channels=24533&date=2022-03-14&duration=24&hour=0' + ) +}) + +it('can parse response', () => { + const content = + '[{"channelId":"24533","startDate":"2022-03-14T00:00:00.000Z","endDate":"2022-03-15T00:00:00.000Z","schedules":[{"channelId":"24533","customFields":{"ContentId":"EP029977175139","Checksum":"2DA90E7E66B9C311F98B186B89C50FAD"},"endDate":"2022-03-14T02:30:00Z","id":"826cb731-9de4-4cf3-bcca-d548d8a33d16","startDate":"2022-03-14T00:00:00Z","item":{"id":"34a028b0-eacf-40f3-9bf9-62ee3330df1b","type":"program","title":"Calgary Flames at Colorado Avalanche","shortDescription":"Johnny Gaudreau and the Flames pay a visit to the Avalanche. Calgary won 4-3 in overtime March 5.","path":"/channel/24533","duration":9000,"images":{"tile":"https://production-static.sportsnet-static.com/shain/v1/dataservice/ResizeImage/$value?Format=\'jpg\'&Quality=85&ImageId=\'785305\'&EntityType=\'LinearSchedule\'&EntityId=\'826cb731-9de4-4cf3-bcca-d548d8a33d16\'&Width=3840&Height=2160","wallpaper":"https://production-static.sportsnet-static.com/shain/v1/dataservice/ResizeImage/$value?Format=\'jpg\'&Quality=85&ImageId=\'785311\'&EntityType=\'LinearSchedule\'&EntityId=\'826cb731-9de4-4cf3-bcca-d548d8a33d16\'&Width=3840&Height=2160"}}}]}]' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-14T00:00:00.000Z', + stop: '2022-03-14T02:30:00.000Z', + title: 'Calgary Flames at Colorado Avalanche', + description: + 'Johnny Gaudreau and the Flames pay a visit to the Avalanche. Calgary won 4-3 in overtime March 5.', + image: + "https://production-static.sportsnet-static.com/shain/v1/dataservice/ResizeImage/$value?Format='jpg'&Quality=85&ImageId='785305'&EntityType='LinearSchedule'&EntityId='826cb731-9de4-4cf3-bcca-d548d8a33d16'&Width=3840&Height=2160" + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: + '[{"channelId":"245321","startDate":"2022-03-14T00:00:00.000Z","endDate":"2022-03-15T00:00:00.000Z","schedules":[]}]' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/watchyour.tv/watchyour.tv.config.js b/sites/watchyour.tv/watchyour.tv.config.js index 893c8a0b..4236d5c3 100644 --- a/sites/watchyour.tv/watchyour.tv.config.js +++ b/sites/watchyour.tv/watchyour.tv.config.js @@ -1,56 +1,56 @@ -const dayjs = require('dayjs') -const axios = require('axios') - -module.exports = { - site: 'watchyour.tv', - days: 2, - url: 'https://www.watchyour.tv/guide.json', - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - parser: function ({ content, date, channel }) { - let programs = [] - const items = parseItems(content, date, channel) - items.forEach(item => { - const start = parseStart(item) - const stop = start.add(parseInt(item.duration), 'm') - programs.push({ - title: item.name, - icon: item.icon, - category: item.category, - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://www.watchyour.tv/guide.json') - .then(r => r.data) - .catch(console.log) - - return data.map(item => ({ - lang: 'en', - site_id: item.id, - name: item.name - })) - } -} - -function parseStart(item) { - return dayjs.unix(parseInt(item.tms)) -} - -function parseItems(content, date, channel) { - if (!content) return [] - const data = JSON.parse(content) - if (!Array.isArray(data)) return [] - const channelData = data.find(i => i.id == channel.site_id) - if (!channelData || !Array.isArray(channelData.shows)) return [] - - return channelData.shows.filter(i => i.start_day === date.format('YYYY-MM-DD')) -} +const dayjs = require('dayjs') +const axios = require('axios') + +module.exports = { + site: 'watchyour.tv', + days: 2, + url: 'https://www.watchyour.tv/guide.json', + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + parser: function ({ content, date, channel }) { + let programs = [] + const items = parseItems(content, date, channel) + items.forEach(item => { + const start = parseStart(item) + const stop = start.add(parseInt(item.duration), 'm') + programs.push({ + title: item.name, + icon: item.icon, + category: item.category, + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://www.watchyour.tv/guide.json') + .then(r => r.data) + .catch(console.log) + + return data.map(item => ({ + lang: 'en', + site_id: item.id, + name: item.name + })) + } +} + +function parseStart(item) { + return dayjs.unix(parseInt(item.tms)) +} + +function parseItems(content, date, channel) { + if (!content) return [] + const data = JSON.parse(content) + if (!Array.isArray(data)) return [] + const channelData = data.find(i => i.id == channel.site_id) + if (!channelData || !Array.isArray(channelData.shows)) return [] + + return channelData.shows.filter(i => i.start_day === date.format('YYYY-MM-DD')) +} diff --git a/sites/watchyour.tv/watchyour.tv.test.js b/sites/watchyour.tv/watchyour.tv.test.js index ef1dd98b..79fb077c 100644 --- a/sites/watchyour.tv/watchyour.tv.test.js +++ b/sites/watchyour.tv/watchyour.tv.test.js @@ -1,45 +1,45 @@ -const { parser, url } = require('./watchyour.tv.config.js') -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('2022-10-03', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '735', - xmltv_id: 'TVSClassicSports.us' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.watchyour.tv/guide.json') -}) - -it('can parse response', () => { - const content = - '[{"name":"TVS Classic Sports","icon":"https://www.watchyour.tv/epg/channellogos/tvs-classic-sports.png","language":"English","id":"735","shows":[{"name":"1979 WVU vs Penn State","category":"Sports","start_day":"2022-10-03","start":"04:00:00","end_day":"2022-10-03","end":"06:00:45","duration":"121","url":"http://rpn1.bozztv.com/36bay2/gusa-tvs/index-1664769600-7245.m3u8?token=f7410a9414f61579dced17ac1bbdb971","icon":"https://example.com/icon.png","timezone":"+0000","tms":"1664769600"},{"name":"1958 NCAA University of Kentucky vs Seattle U","category":"Sports","start_day":"2022-10-04","start":"00:58:50","end_day":"2022-10-04","end":"01:44:11","duration":"46","url":"http://rpn1.bozztv.com/36bay2/gusa-tvs/index.m3u8?token=93e7b201f544c87296076b73f9d880ae","icon":"","timezone":"+0000","tms":"1664845130"}]}]' - const result = parser({ content, date, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-10-03T04:00:00.000Z', - stop: '2022-10-03T06:01:00.000Z', - title: '1979 WVU vs Penn State', - icon: 'https://example.com/icon.png', - category: 'Sports' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '', - date, - channel - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./watchyour.tv.config.js') +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('2022-10-03', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '735', + xmltv_id: 'TVSClassicSports.us' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.watchyour.tv/guide.json') +}) + +it('can parse response', () => { + const content = + '[{"name":"TVS Classic Sports","icon":"https://www.watchyour.tv/epg/channellogos/tvs-classic-sports.png","language":"English","id":"735","shows":[{"name":"1979 WVU vs Penn State","category":"Sports","start_day":"2022-10-03","start":"04:00:00","end_day":"2022-10-03","end":"06:00:45","duration":"121","url":"http://rpn1.bozztv.com/36bay2/gusa-tvs/index-1664769600-7245.m3u8?token=f7410a9414f61579dced17ac1bbdb971","icon":"https://example.com/icon.png","timezone":"+0000","tms":"1664769600"},{"name":"1958 NCAA University of Kentucky vs Seattle U","category":"Sports","start_day":"2022-10-04","start":"00:58:50","end_day":"2022-10-04","end":"01:44:11","duration":"46","url":"http://rpn1.bozztv.com/36bay2/gusa-tvs/index.m3u8?token=93e7b201f544c87296076b73f9d880ae","icon":"","timezone":"+0000","tms":"1664845130"}]}]' + const result = parser({ content, date, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-10-03T04:00:00.000Z', + stop: '2022-10-03T06:01:00.000Z', + title: '1979 WVU vs Penn State', + icon: 'https://example.com/icon.png', + category: 'Sports' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '', + date, + channel + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/wavve.com/wavve.com.config.js b/sites/wavve.com/wavve.com.config.js index 59755129..347852ed 100644 --- a/sites/wavve.com/wavve.com.config.js +++ b/sites/wavve.com/wavve.com.config.js @@ -1,62 +1,62 @@ -const axios = require('axios') -const { DateTime } = require('luxon') - -module.exports = { - site: 'wavve.com', - days: 2, - url: function ({ channel, date }) { - return `https://apis.pooq.co.kr/live/epgs/channels/${ - channel.site_id - }?startdatetime=${date.format('YYYY-MM-DD')}%2000%3A00&enddatetime=${date - .add(1, 'd') - .format('YYYY-MM-DD')}%2000%3A00&apikey=E5F3E0D30947AA5440556471321BB6D9&limit=500` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const channels = [] - - const data = await axios - .get( - 'https://apis.pooq.co.kr/live/epgs?enddatetime=2022-04-17%2019%3A00&genre=all&limit=500&startdatetime=2022-04-17%2016%3A00&apikey=E5F3E0D30947AA5440556471321BB6D9' - ) - .then(r => r.data) - .catch(console.log) - - data.list.forEach(i => { - channels.push({ - name: i.channelname, - site_id: i.channelid, - lang: 'ko' - }) - }) - - return channels - } -} - -function parseStart(item) { - return DateTime.fromFormat(item.starttime, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Seoul' }).toUTC() -} - -function parseStop(item) { - return DateTime.fromFormat(item.endtime, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Seoul' }).toUTC() -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.list)) return [] - - return data.list -} +const axios = require('axios') +const { DateTime } = require('luxon') + +module.exports = { + site: 'wavve.com', + days: 2, + url: function ({ channel, date }) { + return `https://apis.pooq.co.kr/live/epgs/channels/${ + channel.site_id + }?startdatetime=${date.format('YYYY-MM-DD')}%2000%3A00&enddatetime=${date + .add(1, 'd') + .format('YYYY-MM-DD')}%2000%3A00&apikey=E5F3E0D30947AA5440556471321BB6D9&limit=500` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const channels = [] + + const data = await axios + .get( + 'https://apis.pooq.co.kr/live/epgs?enddatetime=2022-04-17%2019%3A00&genre=all&limit=500&startdatetime=2022-04-17%2016%3A00&apikey=E5F3E0D30947AA5440556471321BB6D9' + ) + .then(r => r.data) + .catch(console.log) + + data.list.forEach(i => { + channels.push({ + name: i.channelname, + site_id: i.channelid, + lang: 'ko' + }) + }) + + return channels + } +} + +function parseStart(item) { + return DateTime.fromFormat(item.starttime, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Seoul' }).toUTC() +} + +function parseStop(item) { + return DateTime.fromFormat(item.endtime, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Seoul' }).toUTC() +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.list)) return [] + + return data.list +} diff --git a/sites/wavve.com/wavve.com.test.js b/sites/wavve.com/wavve.com.test.js index ce23e537..7dd9b545 100644 --- a/sites/wavve.com/wavve.com.test.js +++ b/sites/wavve.com/wavve.com.test.js @@ -1,43 +1,43 @@ -const { parser, url } = require('./wavve.com.config.js') -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('2022-04-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'K01', - xmltv_id: 'KBS1TV.kr' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://apis.pooq.co.kr/live/epgs/channels/K01?startdatetime=2022-04-17%2000%3A00&enddatetime=2022-04-18%2000%3A00&apikey=E5F3E0D30947AA5440556471321BB6D9&limit=500' - ) -}) - -it('can parse response', () => { - const content = - '{"pagecount":"37","count":"37","list":[{"cpid":"C3","channelid":"K01","channelname":"KBS 1TV","channelimage":"img.pooq.co.kr/BMS/Channelimage30/image/KBS-1TV-1.jpg","scheduleid":"K01_20220416223000","programid":"","title":"특파원 보고 세계는 지금","image":"wchimg.wavve.com/live/thumbnail/K01.jpg","starttime":"2022-04-16 22:30","endtime":"2022-04-16 23:15","timemachine":"Y","license":"y","livemarks":[],"targetage":"0","tvimage":"img.pooq.co.kr/BMS/Channelimage30/image/KBS 1TV-2.png","ispreorder":"n","preorderlink":"n","alarm":"n"}]}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-04-16T13:30:00.000Z', - stop: '2022-04-16T14:15:00.000Z', - title: '특파원 보고 세계는 지금' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '{"pagecount":"0","count":"0","list":[]}' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./wavve.com.config.js') +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('2022-04-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'K01', + xmltv_id: 'KBS1TV.kr' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://apis.pooq.co.kr/live/epgs/channels/K01?startdatetime=2022-04-17%2000%3A00&enddatetime=2022-04-18%2000%3A00&apikey=E5F3E0D30947AA5440556471321BB6D9&limit=500' + ) +}) + +it('can parse response', () => { + const content = + '{"pagecount":"37","count":"37","list":[{"cpid":"C3","channelid":"K01","channelname":"KBS 1TV","channelimage":"img.pooq.co.kr/BMS/Channelimage30/image/KBS-1TV-1.jpg","scheduleid":"K01_20220416223000","programid":"","title":"특파원 보고 세계는 지금","image":"wchimg.wavve.com/live/thumbnail/K01.jpg","starttime":"2022-04-16 22:30","endtime":"2022-04-16 23:15","timemachine":"Y","license":"y","livemarks":[],"targetage":"0","tvimage":"img.pooq.co.kr/BMS/Channelimage30/image/KBS 1TV-2.png","ispreorder":"n","preorderlink":"n","alarm":"n"}]}' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-04-16T13:30:00.000Z', + stop: '2022-04-16T14:15:00.000Z', + title: '특파원 보고 세계는 지금' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '{"pagecount":"0","count":"0","list":[]}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/web.magentatv.de/web.magentatv.de.config.js b/sites/web.magentatv.de/web.magentatv.de.config.js index 371df0c1..68864329 100644 --- a/sites/web.magentatv.de/web.magentatv.de.config.js +++ b/sites/web.magentatv.de/web.magentatv.de.config.js @@ -1,214 +1,213 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const { upperCase } = require('lodash') - -let X_CSRFTOKEN -let Cookie -const cookiesToExtract = ['JSESSIONID', 'CSESSIONID', 'CSRFSESSION'] - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'web.magentatv.de', - days: 2, - url: 'https://api.prod.sngtv.magentatv.de/EPG/JSON/PlayBillList', - request: { - method: 'POST', - async headers() { - return await setHeaders() - }, - data({ channel, date }) { - return { - count: -1, - isFillProgram: 1, - offset: 0, - properties: [ - { - include: - 'endtime,genres,id,name,starttime,channelid,pictures,introduce,subName,seasonNum,subNum,cast,country,producedate,externalIds', - name: 'playbill' - } - ], - type: 2, - begintime: date.format('YYYYMMDD000000'), - channelid: channel.site_id, - endtime: date.add(1, 'd').format('YYYYMMDD000000') - } - } - }, - parser({ content }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.name, - description: item.introduce, - image: parseImage(item), - category: parseCategory(item), - start: parseStart(item), - stop: parseStop(item), - sub_title: item.subName, - season: item.seasonNum, - episode: item.subNum, - directors: parseDirectors(item), - producers: parseProducers(item), - adapters: parseAdapters(item), - country: upperCase(item.country), - date: item.producedate, - urls: parseUrls(item) - }) - }) - return programs - }, - async channels() { - const url = 'https://api.prod.sngtv.magentatv.de/EPG/JSON/AllChannel' - const body = { - channelNamespace: 2, - filterlist: [ - { - key: 'IsHide', - value: '-1' - } - ], - metaDataVer: 'Channel/1.1', - properties: [ - { - include: '/channellist/logicalChannel/contentId,/channellist/logicalChannel/name', - name: 'logicalChannel' - } - ], - returnSatChannel: 0 - } - const params = { - headers: await setHeaders() - } - - const data = await axios - .post(url, body, params) - .then(r => r.data) - .catch(console.log) - - return data.channellist.map(item => { - return { - lang: 'de', - site_id: item.contentId, - name: item.name - } - }) - } -} - -function parseCategory(item) { - return item.genres - ? item.genres - .replace('und', ',') - .split(',') - .map(i => i.trim()) - : [] -} - -function parseDirectors(item) { - if (!item.cast || !item.cast.director) return [] - return item.cast.director - .replace('und', ',') - .split(',') - .map(i => i.trim()) -} - -function parseProducers(item) { - if (!item.cast || !item.cast.producer) return [] - return item.cast.producer - .replace('und', ',') - .split(',') - .map(i => i.trim()) -} - -function parseAdapters(item) { - if (!item.cast || !item.cast.adaptor) return [] - return item.cast.adaptor - .replace('und', ',') - .split(',') - .map(i => i.trim()) -} - -function parseUrls(item) { - // currently only a imdb id is returned by the api, thus we can construct the url here - if (!item.externalIds) return [] - return JSON.parse(item.externalIds) - .filter(externalId => externalId.type === 'imdb' && externalId.id) - .map(externalId => ({ system: 'imdb', value: `https://www.imdb.com/title/${externalId.id}` })) -} - -function parseImage(item) { - if (!Array.isArray(item.pictures) || !item.pictures.length) return null - - return item.pictures[0].href -} - -function parseStart(item) { - return dayjs.utc(item.starttime, 'YYYY-MM-DD HH:mm:ss') -} - -function parseStop(item) { - return dayjs.utc(item.endtime, 'YYYY-MM-DD HH:mm:ss') -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.playbilllist)) return [] - - return data.playbilllist -} - -async function fetchCookieAndToken() { - // Only fetch the cookies and csrfToken if they are not already set - if (X_CSRFTOKEN && Cookie) { - return - } - - try { - const response = await axios.request({ - url: 'https://api.prod.sngtv.magentatv.de/EPG/JSON/Authenticate', - params: { - SID: 'firstup', - T: 'Windows_chrome_118' - }, - method: 'POST', - data: '{"terminalid":"00:00:00:00:00:00","mac":"00:00:00:00:00:00","terminaltype":"WEBTV","utcEnable":1,"timezone":"Etc/GMT0","userType":3,"terminalvendor":"Unknown"}', - }) - - // Extract the cookies specified in cookiesToExtract - const setCookieHeader = response.headers['set-cookie'] || [] - const extractedCookies = [] - cookiesToExtract.forEach(cookieName => { - const regex = new RegExp(`${cookieName}=(.+?)(;|$)`) - const match = setCookieHeader.find(header => regex.test(header)) - - if (match) { - const cookieString = regex.exec(match)[0] - extractedCookies.push(cookieString) - } - }) - - // check if we recieved a csrfToken only then store the values - if (!response.data.csrfToken) { - console.log('csrfToken not found in the response.') - return - } - - X_CSRFTOKEN = response.data.csrfToken - Cookie = extractedCookies.join(' ') - - } catch(error) { - console.error(error) - } -} - -async function setHeaders() { - await fetchCookieAndToken() - - return { X_CSRFTOKEN, Cookie } -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +let X_CSRFTOKEN +let Cookie +const cookiesToExtract = ['JSESSIONID', 'CSESSIONID', 'CSRFSESSION'] + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'web.magentatv.de', + days: 2, + url: 'https://api.prod.sngtv.magentatv.de/EPG/JSON/PlayBillList', + request: { + method: 'POST', + async headers() { + return await setHeaders() + }, + data({ channel, date }) { + return { + count: -1, + isFillProgram: 1, + offset: 0, + properties: [ + { + include: + 'endtime,genres,id,name,starttime,channelid,pictures,introduce,subName,seasonNum,subNum,cast,country,producedate,externalIds', + name: 'playbill' + } + ], + type: 2, + begintime: date.format('YYYYMMDD000000'), + channelid: channel.site_id, + endtime: date.add(1, 'd').format('YYYYMMDD000000') + } + } + }, + parser({ content }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.name, + description: item.introduce, + image: parseImage(item), + category: parseCategory(item), + start: parseStart(item), + stop: parseStop(item), + sub_title: item.subName, + season: item.seasonNum, + episode: item.subNum, + directors: parseDirectors(item), + producers: parseProducers(item), + adapters: parseAdapters(item), + country: item.country?.toUpperCase(), + date: item.producedate, + urls: parseUrls(item) + }) + }) + return programs + }, + async channels() { + const url = 'https://api.prod.sngtv.magentatv.de/EPG/JSON/AllChannel' + const body = { + channelNamespace: 2, + filterlist: [ + { + key: 'IsHide', + value: '-1' + } + ], + metaDataVer: 'Channel/1.1', + properties: [ + { + include: '/channellist/logicalChannel/contentId,/channellist/logicalChannel/name', + name: 'logicalChannel' + } + ], + returnSatChannel: 0 + } + const params = { + headers: await setHeaders() + } + + const data = await axios + .post(url, body, params) + .then(r => r.data) + .catch(console.log) + + return data.channellist.map(item => { + return { + lang: 'de', + site_id: item.contentId, + name: item.name + } + }) + } +} + +function parseCategory(item) { + return item.genres + ? item.genres + .replace('und', ',') + .split(',') + .map(i => i.trim()) + : [] +} + +function parseDirectors(item) { + if (!item.cast || !item.cast.director) return [] + return item.cast.director + .replace('und', ',') + .split(',') + .map(i => i.trim()) +} + +function parseProducers(item) { + if (!item.cast || !item.cast.producer) return [] + return item.cast.producer + .replace('und', ',') + .split(',') + .map(i => i.trim()) +} + +function parseAdapters(item) { + if (!item.cast || !item.cast.adaptor) return [] + return item.cast.adaptor + .replace('und', ',') + .split(',') + .map(i => i.trim()) +} + +function parseUrls(item) { + // currently only a imdb id is returned by the api, thus we can construct the url here + if (!item.externalIds) return [] + return JSON.parse(item.externalIds) + .filter(externalId => externalId.type === 'imdb' && externalId.id) + .map(externalId => ({ system: 'imdb', value: `https://www.imdb.com/title/${externalId.id}` })) +} + +function parseImage(item) { + if (!Array.isArray(item.pictures) || !item.pictures.length) return null + + return item.pictures[0].href +} + +function parseStart(item) { + return dayjs.utc(item.starttime, 'YYYY-MM-DD HH:mm:ss') +} + +function parseStop(item) { + return dayjs.utc(item.endtime, 'YYYY-MM-DD HH:mm:ss') +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.playbilllist)) return [] + + return data.playbilllist +} + +async function fetchCookieAndToken() { + // Only fetch the cookies and csrfToken if they are not already set + if (X_CSRFTOKEN && Cookie) { + return + } + + try { + const response = await axios.request({ + url: 'https://api.prod.sngtv.magentatv.de/EPG/JSON/Authenticate', + params: { + SID: 'firstup', + T: 'Windows_chrome_118' + }, + method: 'POST', + data: '{"terminalid":"00:00:00:00:00:00","mac":"00:00:00:00:00:00","terminaltype":"WEBTV","utcEnable":1,"timezone":"Etc/GMT0","userType":3,"terminalvendor":"Unknown"}', + }) + + // Extract the cookies specified in cookiesToExtract + const setCookieHeader = response.headers['set-cookie'] || [] + const extractedCookies = [] + cookiesToExtract.forEach(cookieName => { + const regex = new RegExp(`${cookieName}=(.+?)(;|$)`) + const match = setCookieHeader.find(header => regex.test(header)) + + if (match) { + const cookieString = regex.exec(match)[0] + extractedCookies.push(cookieString) + } + }) + + // check if we recieved a csrfToken only then store the values + if (!response.data.csrfToken) { + console.log('csrfToken not found in the response.') + return + } + + X_CSRFTOKEN = response.data.csrfToken + Cookie = extractedCookies.join(' ') + + } catch(error) { + console.error(error) + } +} + +async function setHeaders() { + await fetchCookieAndToken() + + return { X_CSRFTOKEN, Cookie } +} diff --git a/sites/web.magentatv.de/web.magentatv.de.test.js b/sites/web.magentatv.de/web.magentatv.de.test.js index 71f56672..62efef9a 100644 --- a/sites/web.magentatv.de/web.magentatv.de.test.js +++ b/sites/web.magentatv.de/web.magentatv.de.test.js @@ -1,138 +1,138 @@ -const { parser, url, request } = require('./web.magentatv.de.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('2022-03-09', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '255', - xmltv_id: '13thStreet.de' -} - -axios.request.mockImplementation(req => { - const result = {} - if (req.url === 'https://api.prod.sngtv.magentatv.de/EPG/JSON/Authenticate') { - Object.assign(result, { - headers: { - 'set-cookie': [ - 'JSESSIONID=2147EBA9C59BCDC33822CFD2764E5C0B; Path=/EPG; HttpOnly; SameSite=None; Secure', - 'JSESSIONID=2147EBA9C59BCDC33822CFD2764E5C0B; Path=/EPG/; HttpOnly; SameSite=None; Secure', - 'CSESSIONID=1CF187ABCA12ED1B01ADF84C691048ED; Path=/EPG/; Secure; HttpOnly; SameSite=None', - 'CSRFSESSION=ea2329ba213271192bffd77c2fa276086a8e828c1a4ee379; Path=/EPG/; SameSite=None; Secure' - ] - }, - data: { - csrfToken: '6f678415702493d2c28813747c413aa05c87d8f87ecf05fe' - } - }) - } - - return Promise.resolve(result) -}) - -it('can generate valid url', () => { - expect(url).toBe('https://api.prod.sngtv.magentatv.de/EPG/JSON/PlayBillList') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', async () => { - const headers = await request.headers() - - expect(headers).toHaveProperty('Cookie') - expect(headers).toHaveProperty('X_CSRFTOKEN') - - expect(headers.Cookie).toMatch(/JSESSIONID=[\dA-F]+;/i) - expect(headers.Cookie).toMatch(/CSESSIONID=[\dA-F]+;/i) - expect(headers.Cookie).toMatch(/CSRFSESSION=[\dA-F]+;/i) - expect(headers.X_CSRFTOKEN).toMatch(/[\dA-F]/i) -}) - -it('can generate valid request data', () => { - expect(request.data({ channel, date })).toMatchObject({ - count: -1, - isFillProgram: 1, - offset: 0, - properties: [ - { - include: - 'endtime,genres,id,name,starttime,channelid,pictures,introduce,subName,seasonNum,subNum,cast,country,producedate,externalIds', - name: 'playbill' - } - ], - type: 2, - begintime: '20220309000000', - channelid: '255', - endtime: '20220310000000' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2023-10-23T23:58:55.000Z', - stop: '2023-10-24T00:11:05.000Z', - title: 'Twenty Foot Plus', - description: - 'Die besten Big-Wave-Surfer werden bei ihrer Suche nach der nächsten großen Welle begleitet.', - image: - 'http://ngiss.t-online.de/cm1s/media/gracenote/2/4/p24832950_e_h9_aa_2023-06-22T10_12_01.jpg', - category: ['Sport'] - }, - { - start: '2024-11-05T15:37:03.000Z', - stop: '2024-11-05T16:03:48.000Z', - title: 'The Big Bang Theory', - sub_title: 'Tritte unter dem Tisch', - description: - 'Amy arbeitet für eine Weile in Sheldons Universität, er freut sich darüber, doch sie warnt ihn, dass sie sich jetzt häufiger zu Gesicht bekommen. Als Leonard, Sheldon, Raj und Howard zusammen sitzen, diskutieren sie darüber. Sheldon lässt auf sich einreden und informiert Amy, dass er ein Problem mit ihr auf seiner Arbeit hat. Sie ist enttäuscht, während Bernadette mit Howard darüber spricht, warum er auf Sheldon eingeredet hat.', - season: '7', - episode: '5', - image: - 'http://ngiss.t-online.de/cm1s/media/gracenote/1/0/p10262968_e_h9_ah_2021-10-20T07_16_16.jpg', - category: ['Sitcom'], - directors: ['Mark Cendrowski'], - producers: ['Chuck Lorre', 'Bill Prady', 'Steven Molaro'], - adapters: [ - 'Steven Molaro', - 'Steve Holland', - 'Maria Ferrari', - 'Chuck Lorre', - 'Eric Kaplan', - 'Jim Reynolds' - ], - country: 'US', - date: '2013-01-01', - urls: [ - { - system: 'imdb', - value: 'https://www.imdb.com/title/tt0898266' - } - ] - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '{"counttotal":"0"}' - }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./web.magentatv.de.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('2022-03-09', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '255', + xmltv_id: '13thStreet.de' +} + +axios.request.mockImplementation(req => { + const result = {} + if (req.url === 'https://api.prod.sngtv.magentatv.de/EPG/JSON/Authenticate') { + Object.assign(result, { + headers: { + 'set-cookie': [ + 'JSESSIONID=2147EBA9C59BCDC33822CFD2764E5C0B; Path=/EPG; HttpOnly; SameSite=None; Secure', + 'JSESSIONID=2147EBA9C59BCDC33822CFD2764E5C0B; Path=/EPG/; HttpOnly; SameSite=None; Secure', + 'CSESSIONID=1CF187ABCA12ED1B01ADF84C691048ED; Path=/EPG/; Secure; HttpOnly; SameSite=None', + 'CSRFSESSION=ea2329ba213271192bffd77c2fa276086a8e828c1a4ee379; Path=/EPG/; SameSite=None; Secure' + ] + }, + data: { + csrfToken: '6f678415702493d2c28813747c413aa05c87d8f87ecf05fe' + } + }) + } + + return Promise.resolve(result) +}) + +it('can generate valid url', () => { + expect(url).toBe('https://api.prod.sngtv.magentatv.de/EPG/JSON/PlayBillList') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', async () => { + const headers = await request.headers() + + expect(headers).toHaveProperty('Cookie') + expect(headers).toHaveProperty('X_CSRFTOKEN') + + expect(headers.Cookie).toMatch(/JSESSIONID=[\dA-F]+;/i) + expect(headers.Cookie).toMatch(/CSESSIONID=[\dA-F]+;/i) + expect(headers.Cookie).toMatch(/CSRFSESSION=[\dA-F]+;/i) + expect(headers.X_CSRFTOKEN).toMatch(/[\dA-F]/i) +}) + +it('can generate valid request data', () => { + expect(request.data({ channel, date })).toMatchObject({ + count: -1, + isFillProgram: 1, + offset: 0, + properties: [ + { + include: + 'endtime,genres,id,name,starttime,channelid,pictures,introduce,subName,seasonNum,subNum,cast,country,producedate,externalIds', + name: 'playbill' + } + ], + type: 2, + begintime: '20220309000000', + channelid: '255', + endtime: '20220310000000' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2023-10-23T23:58:55.000Z', + stop: '2023-10-24T00:11:05.000Z', + title: 'Twenty Foot Plus', + description: + 'Die besten Big-Wave-Surfer werden bei ihrer Suche nach der nächsten großen Welle begleitet.', + image: + 'http://ngiss.t-online.de/cm1s/media/gracenote/2/4/p24832950_e_h9_aa_2023-06-22T10_12_01.jpg', + category: ['Sport'] + }, + { + start: '2024-11-05T15:37:03.000Z', + stop: '2024-11-05T16:03:48.000Z', + title: 'The Big Bang Theory', + sub_title: 'Tritte unter dem Tisch', + description: + 'Amy arbeitet für eine Weile in Sheldons Universität, er freut sich darüber, doch sie warnt ihn, dass sie sich jetzt häufiger zu Gesicht bekommen. Als Leonard, Sheldon, Raj und Howard zusammen sitzen, diskutieren sie darüber. Sheldon lässt auf sich einreden und informiert Amy, dass er ein Problem mit ihr auf seiner Arbeit hat. Sie ist enttäuscht, während Bernadette mit Howard darüber spricht, warum er auf Sheldon eingeredet hat.', + season: '7', + episode: '5', + image: + 'http://ngiss.t-online.de/cm1s/media/gracenote/1/0/p10262968_e_h9_ah_2021-10-20T07_16_16.jpg', + category: ['Sitcom'], + directors: ['Mark Cendrowski'], + producers: ['Chuck Lorre', 'Bill Prady', 'Steven Molaro'], + adapters: [ + 'Steven Molaro', + 'Steve Holland', + 'Maria Ferrari', + 'Chuck Lorre', + 'Eric Kaplan', + 'Jim Reynolds' + ], + country: 'US', + date: '2013-01-01', + urls: [ + { + system: 'imdb', + value: 'https://www.imdb.com/title/tt0898266' + } + ] + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '{"counttotal":"0"}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/webtv.delta.nl/webtv.delta.nl.config.js b/sites/webtv.delta.nl/webtv.delta.nl.config.js index 961a3706..16f00753 100644 --- a/sites/webtv.delta.nl/webtv.delta.nl.config.js +++ b/sites/webtv.delta.nl/webtv.delta.nl.config.js @@ -1,70 +1,70 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'webtv.delta.nl', - days: 2, - url: function ({ channel, date }) { - return `https://clientapi.tv.delta.nl/guide/channels/list?start=${date.unix()}&end=${date - .add(1, 'd') - .unix()}&includeDetails=true&channels=${channel.site_id}` - }, - async parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - for (let item of items) { - const details = await loadProgramDetails(item) - programs.push({ - title: item.title, - image: item.images.thumbnail.url, - description: details.description, - start: parseStart(item).toJSON(), - stop: parseStop(item).toJSON() - }) - } - - return programs - }, - async channels() { - const items = await axios - .get('https://clientapi.tv.delta.nl/channels/list') - .then(r => r.data) - .catch(console.log) - - return items - .filter(i => i.type === 'TV') - .map(item => { - return { - lang: 'nl', - site_id: item['ID'], - name: item.name - } - }) - } -} - -async function loadProgramDetails(item) { - if (!item.ID) return {} - const url = `https://clientapi.tv.delta.nl/guide/4/details/${item.ID}?X-Response-Version=4.5` - const data = await axios - .get(url) - .then(r => r.data) - .catch(console.log) - - return data || {} -} - -function parseStart(item) { - return dayjs.unix(item.start) -} - -function parseStop(item) { - return dayjs.unix(item.end) -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data) return [] - - return data[channel.site_id] || [] -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'webtv.delta.nl', + days: 2, + url: function ({ channel, date }) { + return `https://clientapi.tv.delta.nl/guide/channels/list?start=${date.unix()}&end=${date + .add(1, 'd') + .unix()}&includeDetails=true&channels=${channel.site_id}` + }, + async parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + for (let item of items) { + const details = await loadProgramDetails(item) + programs.push({ + title: item.title, + image: item.images.thumbnail.url, + description: details.description, + start: parseStart(item).toJSON(), + stop: parseStop(item).toJSON() + }) + } + + return programs + }, + async channels() { + const items = await axios + .get('https://clientapi.tv.delta.nl/channels/list') + .then(r => r.data) + .catch(console.log) + + return items + .filter(i => i.type === 'TV') + .map(item => { + return { + lang: 'nl', + site_id: item['ID'], + name: item.name + } + }) + } +} + +async function loadProgramDetails(item) { + if (!item.ID) return {} + const url = `https://clientapi.tv.delta.nl/guide/4/details/${item.ID}?X-Response-Version=4.5` + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + + return data || {} +} + +function parseStart(item) { + return dayjs.unix(item.start) +} + +function parseStop(item) { + return dayjs.unix(item.end) +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data) return [] + + return data[channel.site_id] || [] +} diff --git a/sites/webtv.delta.nl/webtv.delta.nl.test.js b/sites/webtv.delta.nl/webtv.delta.nl.test.js index 1b95868c..15c40582 100644 --- a/sites/webtv.delta.nl/webtv.delta.nl.test.js +++ b/sites/webtv.delta.nl/webtv.delta.nl.test.js @@ -1,67 +1,67 @@ -const { parser, url } = require('./webtv.delta.nl.config.js') -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('2021-11-12', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1', - xmltv_id: 'NPO1.nl' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://clientapi.tv.delta.nl/guide/channels/list?start=1636675200&end=1636761600&includeDetails=true&channels=1' - ) -}) - -it('can parse response', done => { - axios.get.mockImplementation(() => - Promise.resolve({ - data: JSON.parse( - '{"ID":"P~945cb98e-3d19-11ec-8456-953363d7a344","seriesID":"S~d37c4626-b691-11ea-ba69-255835135f02","channelID":"1","start":1636674960,"end":1636676520,"catchupAvailableUntil":1637279760,"title":"Eigen Huis & Tuin: Lekker Leven","description":"Nederlands lifestyleprogramma uit 2022 (ook in HD) met dagelijkse inspiratie voor een lekker leven in en om het huis.\\nPresentatrice Froukje de Both, kok Hugo Kennis en een team van experts, onder wie tuinman Tom Groot, geven praktische tips op het gebied van wonen, lifestyle, tuinieren en koken. Daarmee kun je zelf direct aan de slag om je leven leuker én gezonder te maken. Afl. 15 van seizoen 4.","images":{"thumbnail":{"url":"https://cdn.gvidi.tv/img/booxmedia/b291/561946.jpg"}},"additionalInformation":{"metadataID":"M~c512c206-95e5-11ec-87d8-494f70130311","externalMetadataID":"E~RTL4-89d99356_6599_4b65_a7a0_a93f39019645"},"parentalGuidance":{"kijkwijzer":["AL"]},"restrictions":{"startoverDisabled":false,"catchupDisabled":false,"recordingDisabled":false},"isFiller":false}' - ) - }) - ) - - const content = - '{"1":[{"ID":"P~945cb98e-3d19-11ec-8456-953363d7a344","seriesID":"S~d37c4626-b691-11ea-ba69-255835135f02","channelID":"1","start":1636674960,"end":1636676520,"catchupAvailableUntil":1637279760,"title":"NOS Journaal","images":{"thumbnail":{"url":"https://cdn.gvidi.tv/img/booxmedia/e19c/static/NOS%20Journaal5.jpg"}},"additionalInformation":{"metadataID":"M~944f3c6e-3d19-11ec-9faf-2735f2e98d2a","externalMetadataID":"E~TV01-2026117420668"},"parentalGuidance":{"kijkwijzer":["AL"]},"restrictions":{"startoverDisabled":false,"catchupDisabled":false,"recordingDisabled":false},"isFiller":false}]}' - - parser({ date, channel, content }) - .then(result => { - expect(result).toMatchObject([ - { - start: '2021-11-11T23:56:00.000Z', - stop: '2021-11-12T00:22:00.000Z', - title: 'NOS Journaal', - description: - 'Nederlands lifestyleprogramma uit 2022 (ook in HD) met dagelijkse inspiratie voor een lekker leven in en om het huis.\nPresentatrice Froukje de Both, kok Hugo Kennis en een team van experts, onder wie tuinman Tom Groot, geven praktische tips op het gebied van wonen, lifestyle, tuinieren en koken. Daarmee kun je zelf direct aan de slag om je leven leuker én gezonder te maken. Afl. 15 van seizoen 4.', - image: 'https://cdn.gvidi.tv/img/booxmedia/e19c/static/NOS%20Journaal5.jpg' - } - ]) - done() - }) - .catch(error => { - done(error) - }) -}) - -it('can handle empty guide', done => { - parser({ - date, - channel, - content: '{"code":500,"message":"Error retrieving guide"}' - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(error => { - done(error) - }) -}) +const { parser, url } = require('./webtv.delta.nl.config.js') +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('2021-11-12', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1', + xmltv_id: 'NPO1.nl' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://clientapi.tv.delta.nl/guide/channels/list?start=1636675200&end=1636761600&includeDetails=true&channels=1' + ) +}) + +it('can parse response', done => { + axios.get.mockImplementation(() => + Promise.resolve({ + data: JSON.parse( + '{"ID":"P~945cb98e-3d19-11ec-8456-953363d7a344","seriesID":"S~d37c4626-b691-11ea-ba69-255835135f02","channelID":"1","start":1636674960,"end":1636676520,"catchupAvailableUntil":1637279760,"title":"Eigen Huis & Tuin: Lekker Leven","description":"Nederlands lifestyleprogramma uit 2022 (ook in HD) met dagelijkse inspiratie voor een lekker leven in en om het huis.\\nPresentatrice Froukje de Both, kok Hugo Kennis en een team van experts, onder wie tuinman Tom Groot, geven praktische tips op het gebied van wonen, lifestyle, tuinieren en koken. Daarmee kun je zelf direct aan de slag om je leven leuker én gezonder te maken. Afl. 15 van seizoen 4.","images":{"thumbnail":{"url":"https://cdn.gvidi.tv/img/booxmedia/b291/561946.jpg"}},"additionalInformation":{"metadataID":"M~c512c206-95e5-11ec-87d8-494f70130311","externalMetadataID":"E~RTL4-89d99356_6599_4b65_a7a0_a93f39019645"},"parentalGuidance":{"kijkwijzer":["AL"]},"restrictions":{"startoverDisabled":false,"catchupDisabled":false,"recordingDisabled":false},"isFiller":false}' + ) + }) + ) + + const content = + '{"1":[{"ID":"P~945cb98e-3d19-11ec-8456-953363d7a344","seriesID":"S~d37c4626-b691-11ea-ba69-255835135f02","channelID":"1","start":1636674960,"end":1636676520,"catchupAvailableUntil":1637279760,"title":"NOS Journaal","images":{"thumbnail":{"url":"https://cdn.gvidi.tv/img/booxmedia/e19c/static/NOS%20Journaal5.jpg"}},"additionalInformation":{"metadataID":"M~944f3c6e-3d19-11ec-9faf-2735f2e98d2a","externalMetadataID":"E~TV01-2026117420668"},"parentalGuidance":{"kijkwijzer":["AL"]},"restrictions":{"startoverDisabled":false,"catchupDisabled":false,"recordingDisabled":false},"isFiller":false}]}' + + parser({ date, channel, content }) + .then(result => { + expect(result).toMatchObject([ + { + start: '2021-11-11T23:56:00.000Z', + stop: '2021-11-12T00:22:00.000Z', + title: 'NOS Journaal', + description: + 'Nederlands lifestyleprogramma uit 2022 (ook in HD) met dagelijkse inspiratie voor een lekker leven in en om het huis.\nPresentatrice Froukje de Both, kok Hugo Kennis en een team van experts, onder wie tuinman Tom Groot, geven praktische tips op het gebied van wonen, lifestyle, tuinieren en koken. Daarmee kun je zelf direct aan de slag om je leven leuker én gezonder te maken. Afl. 15 van seizoen 4.', + image: 'https://cdn.gvidi.tv/img/booxmedia/e19c/static/NOS%20Journaal5.jpg' + } + ]) + done() + }) + .catch(error => { + done(error) + }) +}) + +it('can handle empty guide', done => { + parser({ + date, + channel, + content: '{"code":500,"message":"Error retrieving guide"}' + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(error => { + done(error) + }) +}) diff --git a/sites/winplay.co/winplay.co.config.js b/sites/winplay.co/winplay.co.config.js index 831fecc7..738ddb03 100644 --- a/sites/winplay.co/winplay.co.config.js +++ b/sites/winplay.co/winplay.co.config.js @@ -1,45 +1,45 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'winplay.co', - days: 2, - url: 'https://next.platform.mediastre.am/graphql', - request: { - method: 'POST', - headers: { - accept: 'application/json', - 'x-client-id': 'a084524ea449c15dfe5e75636fb55ce6a9d0d7601aac946daa', - 'x-ott-language': 'es' - }, - data() { - return { - operationName: 'getLivesEpg', - variables: { page: 1, hours: 48 }, - query: - 'query getLivesEpg($page: Int = 1, $hours: Int, $ids: [String]) {\n getLives(ids: $ids) {\n _id\n logo\n name\n schedules(hours: $hours, page: {limit: 0, page: $page}) {\n _id\n name\n date_start\n date_end\n current\n match {\n matchDay\n __typename\n }\n show {\n _id\n title\n __typename\n }\n live {\n _id\n dvr\n type\n purchased\n __typename\n }\n __typename\n }\n __typename\n }\n}\n' - } - } - }, - parser({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel, date) - for (let item of items) { - programs.push({ - title: item.name, - start: dayjs(item.date_start), - stop: dayjs(item.date_end) - }) - } - - return programs - } -} - -function parseItems(content, channel, date) { - const data = JSON.parse(content) - if (!data || !data.data || !data.data.getLives) return [] - const channelData = data.data.getLives.find(i => i._id === channel.site_id) - if (!Array.isArray(channelData.schedules)) return [] - - return channelData.schedules.filter(i => date.isSame(dayjs(i.date_start), 'd')) -} +const dayjs = require('dayjs') + +module.exports = { + site: 'winplay.co', + days: 2, + url: 'https://next.platform.mediastre.am/graphql', + request: { + method: 'POST', + headers: { + accept: 'application/json', + 'x-client-id': 'a084524ea449c15dfe5e75636fb55ce6a9d0d7601aac946daa', + 'x-ott-language': 'es' + }, + data() { + return { + operationName: 'getLivesEpg', + variables: { page: 1, hours: 48 }, + query: + 'query getLivesEpg($page: Int = 1, $hours: Int, $ids: [String]) {\n getLives(ids: $ids) {\n _id\n logo\n name\n schedules(hours: $hours, page: {limit: 0, page: $page}) {\n _id\n name\n date_start\n date_end\n current\n match {\n matchDay\n __typename\n }\n show {\n _id\n title\n __typename\n }\n live {\n _id\n dvr\n type\n purchased\n __typename\n }\n __typename\n }\n __typename\n }\n}\n' + } + } + }, + parser({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel, date) + for (let item of items) { + programs.push({ + title: item.name, + start: dayjs(item.date_start), + stop: dayjs(item.date_end) + }) + } + + return programs + } +} + +function parseItems(content, channel, date) { + const data = JSON.parse(content) + if (!data || !data.data || !data.data.getLives) return [] + const channelData = data.data.getLives.find(i => i._id === channel.site_id) + if (!Array.isArray(channelData.schedules)) return [] + + return channelData.schedules.filter(i => date.isSame(dayjs(i.date_start), 'd')) +} diff --git a/sites/winplay.co/winplay.co.test.js b/sites/winplay.co/winplay.co.test.js index 49238825..b3fe45c2 100644 --- a/sites/winplay.co/winplay.co.test.js +++ b/sites/winplay.co/winplay.co.test.js @@ -1,68 +1,68 @@ -const { parser, url, request } = require('./winplay.co.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('2024-12-24', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '529cff6f6bd2ea6b610000e0', - xmltv_id: 'WinPlusFutbol.co' -} - -it('can generate valid url', () => { - expect(url).toBe('https://next.platform.mediastre.am/graphql') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - accept: 'application/json', - 'x-client-id': 'a084524ea449c15dfe5e75636fb55ce6a9d0d7601aac946daa', - 'x-ott-language': 'es' - }) -}) - -it('can generate valid request data', () => { - expect(request.data()).toMatchObject({ - operationName: 'getLivesEpg', - variables: { page: 1, hours: 48 }, - query: - 'query getLivesEpg($page: Int = 1, $hours: Int, $ids: [String]) {\n getLives(ids: $ids) {\n _id\n logo\n name\n schedules(hours: $hours, page: {limit: 0, page: $page}) {\n _id\n name\n date_start\n date_end\n current\n match {\n matchDay\n __typename\n }\n show {\n _id\n title\n __typename\n }\n live {\n _id\n dvr\n type\n purchased\n __typename\n }\n __typename\n }\n __typename\n }\n}\n' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') - - const results = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2024-12-24T00:30:00.000Z', - stop: '2024-12-24T02:30:00.000Z', - title: 'Los Disruptivos de Win' - }) - - expect(results[1]).toMatchObject({ - start: '2024-12-24T02:30:00.000Z', - stop: '2024-12-24T03:30:00.000Z', - title: 'WIn Noticias' - }) -}) - -it('can handle empty guide', () => { - const content = '{"status":"ERROR","error":"UNAUTHORIZED_REQUEST"}' - const results = parser({ content, channel, date }) - - expect(results).toMatchObject([]) -}) +const { parser, url, request } = require('./winplay.co.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('2024-12-24', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '529cff6f6bd2ea6b610000e0', + xmltv_id: 'WinPlusFutbol.co' +} + +it('can generate valid url', () => { + expect(url).toBe('https://next.platform.mediastre.am/graphql') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + accept: 'application/json', + 'x-client-id': 'a084524ea449c15dfe5e75636fb55ce6a9d0d7601aac946daa', + 'x-ott-language': 'es' + }) +}) + +it('can generate valid request data', () => { + expect(request.data()).toMatchObject({ + operationName: 'getLivesEpg', + variables: { page: 1, hours: 48 }, + query: + 'query getLivesEpg($page: Int = 1, $hours: Int, $ids: [String]) {\n getLives(ids: $ids) {\n _id\n logo\n name\n schedules(hours: $hours, page: {limit: 0, page: $page}) {\n _id\n name\n date_start\n date_end\n current\n match {\n matchDay\n __typename\n }\n show {\n _id\n title\n __typename\n }\n live {\n _id\n dvr\n type\n purchased\n __typename\n }\n __typename\n }\n __typename\n }\n}\n' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') + + const results = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2024-12-24T00:30:00.000Z', + stop: '2024-12-24T02:30:00.000Z', + title: 'Los Disruptivos de Win' + }) + + expect(results[1]).toMatchObject({ + start: '2024-12-24T02:30:00.000Z', + stop: '2024-12-24T03:30:00.000Z', + title: 'WIn Noticias' + }) +}) + +it('can handle empty guide', () => { + const content = '{"status":"ERROR","error":"UNAUTHORIZED_REQUEST"}' + const results = parser({ content, channel, date }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/worldfishingnetwork.com/worldfishingnetwork.com.config.js b/sites/worldfishingnetwork.com/worldfishingnetwork.com.config.js index b50e1f8f..a20635bb 100644 --- a/sites/worldfishingnetwork.com/worldfishingnetwork.com.config.js +++ b/sites/worldfishingnetwork.com/worldfishingnetwork.com.config.js @@ -1,79 +1,79 @@ -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: 'worldfishingnetwork.com', - days: 2, - url({ date }) { - return `https://www.worldfishingnetwork.com/schedule/77420?day=${date.format('ddd')}` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - let $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - sub_title: parseSubTitle($item), - description: parseDescription($item), - image: parseImage($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('.show-title > h3').text().trim() -} - -function parseSubTitle($item) { - return $item('.show-title').clone().children().remove().end().text().trim() -} - -function parseDescription($item) { - return $item('.show-title > p').text().trim() -} - -function parseImage($item) { - const url = $item('.show-img > img').attr('src') - - return url ? `https:${url}` : null -} - -function parseStart($item, date) { - const time = $item('.show-time > h2').clone().children().remove().end().text().trim() - const period = $item('.show-time > h2 > span > strong').text().trim() - - return dayjs.tz( - `${date.format('YYYY-MM-DD')} ${time} ${period}`, - 'YYYY-MM-DD HH:mm A', - 'America/New_York' - ) -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.show-item').toArray() -} +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: 'worldfishingnetwork.com', + days: 2, + url({ date }) { + return `https://www.worldfishingnetwork.com/schedule/77420?day=${date.format('ddd')}` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + let $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + sub_title: parseSubTitle($item), + description: parseDescription($item), + image: parseImage($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('.show-title > h3').text().trim() +} + +function parseSubTitle($item) { + return $item('.show-title').clone().children().remove().end().text().trim() +} + +function parseDescription($item) { + return $item('.show-title > p').text().trim() +} + +function parseImage($item) { + const url = $item('.show-img > img').attr('src') + + return url ? `https:${url}` : null +} + +function parseStart($item, date) { + const time = $item('.show-time > h2').clone().children().remove().end().text().trim() + const period = $item('.show-time > h2 > span > strong').text().trim() + + return dayjs.tz( + `${date.format('YYYY-MM-DD')} ${time} ${period}`, + 'YYYY-MM-DD HH:mm A', + 'America/New_York' + ) +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.show-item').toArray() +} diff --git a/sites/worldfishingnetwork.com/worldfishingnetwork.com.test.js b/sites/worldfishingnetwork.com/worldfishingnetwork.com.test.js index 8629089e..a67c8c95 100644 --- a/sites/worldfishingnetwork.com/worldfishingnetwork.com.test.js +++ b/sites/worldfishingnetwork.com/worldfishingnetwork.com.test.js @@ -1,53 +1,53 @@ -const { parser, url } = require('./worldfishingnetwork.com.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('2023-01-24', 'YYYY-MM-DD').startOf('d') - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://www.worldfishingnetwork.com/schedule/77420?day=Tue') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - 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: '2023-01-24T05:00:00.000Z', - stop: '2023-01-24T07:00:00.000Z', - title: 'Major League Fishing', - sub_title: 'Challenge Cup Sudden Death Round 2', - description: - 'Nine anglers race to a target weight on Lake Wylie in the Lucas Oil Challenge Cup, presented by B&W Trailer Hitches, Rock Hill, South Carolina. Only four will move on to the Championship Round.', - image: 'https://content.osgnetworks.tv/shows/major-league-fishing-thumbnail.jpg' - }) - - expect(results[41]).toMatchObject({ - start: '2023-01-25T04:30:00.000Z', - stop: '2023-01-25T05:00:00.000Z', - title: 'Fishing 411', - sub_title: 'Flint Wilderness Walleye', - description: - 'Mark Romanack and Bryan Darland fish walleye on Klotz Lake in the famed Flint Wilderness of Ontario', - image: 'https://content.osgnetworks.tv/shows/fishin-411-thumbnail.jpg' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./worldfishingnetwork.com.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('2023-01-24', 'YYYY-MM-DD').startOf('d') + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://www.worldfishingnetwork.com/schedule/77420?day=Tue') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + 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: '2023-01-24T05:00:00.000Z', + stop: '2023-01-24T07:00:00.000Z', + title: 'Major League Fishing', + sub_title: 'Challenge Cup Sudden Death Round 2', + description: + 'Nine anglers race to a target weight on Lake Wylie in the Lucas Oil Challenge Cup, presented by B&W Trailer Hitches, Rock Hill, South Carolina. Only four will move on to the Championship Round.', + image: 'https://content.osgnetworks.tv/shows/major-league-fishing-thumbnail.jpg' + }) + + expect(results[41]).toMatchObject({ + start: '2023-01-25T04:30:00.000Z', + stop: '2023-01-25T05:00:00.000Z', + title: 'Fishing 411', + sub_title: 'Flint Wilderness Walleye', + description: + 'Mark Romanack and Bryan Darland fish walleye on Klotz Lake in the famed Flint Wilderness of Ontario', + image: 'https://content.osgnetworks.tv/shows/fishin-411-thumbnail.jpg' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/www3.nhk.or.jp/www3.nhk.or.jp.config.js b/sites/www3.nhk.or.jp/www3.nhk.or.jp.config.js index 4a7f59f0..1d696b3a 100644 --- a/sites/www3.nhk.or.jp/www3.nhk.or.jp.config.js +++ b/sites/www3.nhk.or.jp/www3.nhk.or.jp.config.js @@ -1,68 +1,68 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'www3.nhk.or.jp', - days: 5, - lang: 'en', - delay: 5000, - - url: function ({ date }) { - return `https://nwapi.nhk.jp/nhkworld/epg/v7b/world/s${date.unix() * 1000}-e${ - date.add(1, 'd').unix() * 1000 - }.json` - }, - - request: { - method: 'GET', - timeout: 5000, - cache: { ttl: 60 * 1000 }, - headers: { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36' - } - }, - - logo: function (context) { - return context.channel.logo - }, - - parser: function (context) { - const programs = [] - - const items = parseItems(context.content) - - items.forEach(item => { - programs.push({ - title: item.title, - start: parseStart(item), - stop: parseStop(item), - description: item.description, - image: parseImage(item), - sub_title: item.subtitle - }) - }) - - return programs - } -} - -function parseItems(content) { - if (content != '') { - const data = JSON.parse(content) - return !data || !data.channel || !Array.isArray(data.channel.item) ? [] : data.channel.item - } else { - return [] - } -} - -function parseStart(item) { - return dayjs.unix(parseInt(item.pubDate) / 1000) -} - -function parseStop(item) { - return dayjs.unix(parseInt(item.endDate) / 1000) -} - -function parseImage(item) { - return 'https://www.nhk.or.jp' + item.thumbnail -} +const dayjs = require('dayjs') + +module.exports = { + site: 'www3.nhk.or.jp', + days: 5, + lang: 'en', + delay: 5000, + + url: function ({ date }) { + return `https://nwapi.nhk.jp/nhkworld/epg/v7b/world/s${date.unix() * 1000}-e${ + date.add(1, 'd').unix() * 1000 + }.json` + }, + + request: { + method: 'GET', + timeout: 5000, + cache: { ttl: 60 * 1000 }, + headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36' + } + }, + + logo: function (context) { + return context.channel.logo + }, + + parser: function (context) { + const programs = [] + + const items = parseItems(context.content) + + items.forEach(item => { + programs.push({ + title: item.title, + start: parseStart(item), + stop: parseStop(item), + description: item.description, + image: parseImage(item), + sub_title: item.subtitle + }) + }) + + return programs + } +} + +function parseItems(content) { + if (content != '') { + const data = JSON.parse(content) + return !data || !data.channel || !Array.isArray(data.channel.item) ? [] : data.channel.item + } else { + return [] + } +} + +function parseStart(item) { + return dayjs.unix(parseInt(item.pubDate) / 1000) +} + +function parseStop(item) { + return dayjs.unix(parseInt(item.endDate) / 1000) +} + +function parseImage(item) { + return 'https://www.nhk.or.jp' + item.thumbnail +} diff --git a/sites/www3.nhk.or.jp/www3.nhk.or.jp.test.js b/sites/www3.nhk.or.jp/www3.nhk.or.jp.test.js index ffe55139..ed65daef 100644 --- a/sites/www3.nhk.or.jp/www3.nhk.or.jp.test.js +++ b/sites/www3.nhk.or.jp/www3.nhk.or.jp.test.js @@ -1,43 +1,43 @@ -const { url, parser } = require('./www3.nhk.or.jp.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2023-04-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '0', - xmltv_id: 'NHKWorldJapan.jp', - lang: 'en', - logo: 'https://www3.nhk.or.jp/nhkworld/common/site_images/nw_webapp_1024x1024.png' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://nwapi.nhk.jp/nhkworld/epg/v7b/world/s1682726400000-e1682812800000.json' - ) -}) - -it('can parse response', () => { - const content = - '{"channel":{"item":[{"seriesId":"1007","airingId":"000","title":"NHK NEWSLINE","description":"NHK WORLD-JAPAN\'s flagship hourly news program delivers the latest world news, business and weather, with a focus on Japan and the rest of Asia.","link":"/nhkworld/en/news/","pubDate":"1682726400000","endDate":"1682727000000","vodReserved":false,"jstrm":"1","wstrm":"1","subtitle":"","content":"","content_clean":"","pgm_gr_id":"","thumbnail":"/nhkworld/upld/thumbnails/en/tv/regular_program/340aed63308aafd1178172abf6325231_large.jpg","thumbnail_s":"/nhkworld/upld/thumbnails/en/tv/regular_program/340aed63308aafd1178172abf6325231_small.jpg","showlist":"0","internal":"0","genre":{"TV":"11","Top":"","LC":""},"vod_id":"","vod_url":"","analytics":"[nhkworld]simul;NHK NEWSLINE;w02,001;1007-000-2023;2023-04-29T09:00:00+09:00"}]}}' - const results = parser({ content }) - - expect(results).toMatchObject([ - { - title: 'NHK NEWSLINE', - start: dayjs(1682726400000), - stop: dayjs(1682727000000), - description: - "NHK WORLD-JAPAN's flagship hourly news program delivers the latest world news, business and weather, with a focus on Japan and the rest of Asia.", - image: - 'https://www.nhk.or.jp/nhkworld/upld/thumbnails/en/tv/regular_program/340aed63308aafd1178172abf6325231_large.jpg', - sub_title: '' - } - ]) -}) - -it('can handle empty guide', () => { - const results = parser({ content: '' }) - - expect(results).toMatchObject([]) -}) +const { url, parser } = require('./www3.nhk.or.jp.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2023-04-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '0', + xmltv_id: 'NHKWorldJapan.jp', + lang: 'en', + logo: 'https://www3.nhk.or.jp/nhkworld/common/site_images/nw_webapp_1024x1024.png' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://nwapi.nhk.jp/nhkworld/epg/v7b/world/s1682726400000-e1682812800000.json' + ) +}) + +it('can parse response', () => { + const content = + '{"channel":{"item":[{"seriesId":"1007","airingId":"000","title":"NHK NEWSLINE","description":"NHK WORLD-JAPAN\'s flagship hourly news program delivers the latest world news, business and weather, with a focus on Japan and the rest of Asia.","link":"/nhkworld/en/news/","pubDate":"1682726400000","endDate":"1682727000000","vodReserved":false,"jstrm":"1","wstrm":"1","subtitle":"","content":"","content_clean":"","pgm_gr_id":"","thumbnail":"/nhkworld/upld/thumbnails/en/tv/regular_program/340aed63308aafd1178172abf6325231_large.jpg","thumbnail_s":"/nhkworld/upld/thumbnails/en/tv/regular_program/340aed63308aafd1178172abf6325231_small.jpg","showlist":"0","internal":"0","genre":{"TV":"11","Top":"","LC":""},"vod_id":"","vod_url":"","analytics":"[nhkworld]simul;NHK NEWSLINE;w02,001;1007-000-2023;2023-04-29T09:00:00+09:00"}]}}' + const results = parser({ content }) + + expect(results).toMatchObject([ + { + title: 'NHK NEWSLINE', + start: dayjs(1682726400000), + stop: dayjs(1682727000000), + description: + "NHK WORLD-JAPAN's flagship hourly news program delivers the latest world news, business and weather, with a focus on Japan and the rest of Asia.", + image: + 'https://www.nhk.or.jp/nhkworld/upld/thumbnails/en/tv/regular_program/340aed63308aafd1178172abf6325231_large.jpg', + sub_title: '' + } + ]) +}) + +it('can handle empty guide', () => { + const results = parser({ content: '' }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/xem.kplus.vn/xem.kplus.vn.config.js b/sites/xem.kplus.vn/xem.kplus.vn.config.js index e648ca2f..c14c11ce 100644 --- a/sites/xem.kplus.vn/xem.kplus.vn.config.js +++ b/sites/xem.kplus.vn/xem.kplus.vn.config.js @@ -1,146 +1,146 @@ -const dayjs = require('dayjs') -const axios = require('axios') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const doFetch = require('@ntlab/sfetch') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -let session - -module.exports = { - site: 'xem.kplus.vn', - days: 2, - url({ channel, date }) { - return `https://tvapi-sgn.solocoo.tv/v1/assets?query=schedule,forrelated,${ - channel.site_id - }&from=${date.format('YYYY-MM-DDTHH:mm:ss[Z]')}&limit=1000` - }, - request: { - async headers() { - if (!session) { - session = await loadSessionDetails() - if (!session || !session.token) return null - } - - return { - authorization: `Bearer ${session.token}` - } - } - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content, date) - items.forEach(item => { - programs.push({ - title: item.title, - categories: parseCategories(item), - images: parseImages(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const session = await loadSessionDetails() - if (!session || !session.token) throw new Error('The session token is missing') - - const groups = [ - 'Channels_Kplus', - 'Channels_VTV', - 'Channels_VTVcab', - 'Channels_Kênh Quốc Tế', - 'Channels_SCTV', - 'Channels_HTV-HTVC', - 'Channels_THVL', - 'Channels_Kênh Thiết Yếu', - 'Channels_Kênh Địa Phương' - ] - - const queue = groups.map(group => ({ - url: `https://tvapi-sgn.solocoo.tv/v1/assets?query=nav,${group}&limit=100`, - params: { - headers: { - authorization: `Bearer ${session.token}` - } - } - })) - - let channels = [] - await doFetch(queue, (url, data) => { - data.assets.forEach(channel => { - if (!channel?.params?.params?.id) return - - channels.push({ - lang: 'vi', - name: channel.params.internalTitle.replace('Channels_', ''), - site_id: channel.params.params.id - }) - }) - }) - - return channels - } -} - -function parseCategories(item) { - return Array.isArray(item?.params?.genres) ? item.params.genres.map(i => i.title) : [] -} - -function parseImages(item) { - return Array.isArray(item?.images) - ? item.images - .filter(i => i.url.indexOf('orientation=landscape') > 0) - .map(i => `${i.url}&w=460&h=260`) - : [] -} - -function parseStart(item) { - return item?.params?.start ? dayjs.utc(item.params.start, 'YYYY-MM-DDTHH:mm:ss[Z]') : null -} - -function parseStop(item) { - return item?.params?.end ? dayjs.utc(item.params.end, 'YYYY-MM-DDTHH:mm:ss[Z]') : null -} - -function parseItems(content, date) { - try { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.assets)) return [] - - return data.assets.filter( - p => p?.params?.start && date.isSame(dayjs.utc(p.params.start, 'YYYY-MM-DDTHH:mm:ss[Z]'), 'd') - ) - } catch (err) { - console.log(err) - return [] - } -} - -function loadSessionDetails() { - return axios - .post('https://tvapi-sgn.solocoo.tv/v1/session', { - ssoToken: - 'eyJhbGciOiJkaXIiLCJrZXkiOiJ2c3R2IiwiZW5jIjoiQTEyOENCQy1IUzI1NiJ9..6jMKWv5bSqODWOWLmeERqw._WcKmMW2ij3yPJkhFllQHgXOkW7powvzT-5p6G4_jjYa8vzJybmHu_1CwIEb_s2hVOyaNDi6M-NVLNY9CaNU3aSC-ojZ4UoQ7QLRTFWP-2uY-mL5IgJtL7Xknus5blHJbR8B-xaOODXIJh8PneZORmPHa5EHhs1vOmqpGb1COZwqlw_WFbGT9EsFq6W8fsYH3O5cUqec608Uad-wK59OQIJyofZJwrb6VTthmwwIDxX6Dn-kyYssfdXvPF_BXu5A-e2MFOsdzvMjENdq0FHCk-b9OojzENR6S-JEtSTrZHrgSfHsqb1DwVbtuaetFlV-A3-gxyqqHH7QIvkRM38StNMAp_q8TUauhluwKK3nuXbgogiQ9d9Kc9s7WGoBPOVHsZ4w6wJ9fDBIyhApOJUAdEINi7dLpe1pTBBk6ZA504PVyQ0d6DtdhJhkbT6I88wwxz2U6sF5tInZBcdyZzCa1KKHWQuonTJ4IPcILGQFuzo.lhVv2QaTOaxTS9F4Ht2L3A', - osVersion: 'Windows 10', - deviceModel: 'Chrome', - deviceType: 'PC', - deviceSerial: 'w408a0eb0-d50f-11ef-affa-af9775b838ad', - deviceOem: 'Chrome', - devicePrettyName: 'Chrome 128.0.0.0', - appVersion: '12.1', - language: 'en_US', - brand: 'vstv', - memberId: '0', - featureLevel: 6, - provisionData: - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MzcxNDU4MjYsImljIjp0cnVlLCJ1cCI6ImNwaSIsImJyIjoidnN0diIsImRzIjoidzQwOGEwZWIwLWQ1MGYtMTFlZi1hZmZhLWFmOTc3NWI4MzhhZCIsImRlIjoiYnJhbmRNYXBwaW5nIn0.Ou6yh5qXtlK4NhyWHciVszARr98PLL1TkaXKpqQtub8' - }) - .then(r => r.data) - .catch(console.log) -} +const dayjs = require('dayjs') +const axios = require('axios') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const doFetch = require('@ntlab/sfetch') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +let session + +module.exports = { + site: 'xem.kplus.vn', + days: 2, + url({ channel, date }) { + return `https://tvapi-sgn.solocoo.tv/v1/assets?query=schedule,forrelated,${ + channel.site_id + }&from=${date.format('YYYY-MM-DDTHH:mm:ss[Z]')}&limit=1000` + }, + request: { + async headers() { + if (!session) { + session = await loadSessionDetails() + if (!session || !session.token) return null + } + + return { + authorization: `Bearer ${session.token}` + } + } + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content, date) + items.forEach(item => { + programs.push({ + title: item.title, + categories: parseCategories(item), + images: parseImages(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const session = await loadSessionDetails() + if (!session || !session.token) throw new Error('The session token is missing') + + const groups = [ + 'Channels_Kplus', + 'Channels_VTV', + 'Channels_VTVcab', + 'Channels_Kênh Quốc Tế', + 'Channels_SCTV', + 'Channels_HTV-HTVC', + 'Channels_THVL', + 'Channels_Kênh Thiết Yếu', + 'Channels_Kênh Địa Phương' + ] + + const queue = groups.map(group => ({ + url: `https://tvapi-sgn.solocoo.tv/v1/assets?query=nav,${group}&limit=100`, + params: { + headers: { + authorization: `Bearer ${session.token}` + } + } + })) + + let channels = [] + await doFetch(queue, (url, data) => { + data.assets.forEach(channel => { + if (!channel?.params?.params?.id) return + + channels.push({ + lang: 'vi', + name: channel.params.internalTitle.replace('Channels_', ''), + site_id: channel.params.params.id + }) + }) + }) + + return channels + } +} + +function parseCategories(item) { + return Array.isArray(item?.params?.genres) ? item.params.genres.map(i => i.title) : [] +} + +function parseImages(item) { + return Array.isArray(item?.images) + ? item.images + .filter(i => i.url.indexOf('orientation=landscape') > 0) + .map(i => `${i.url}&w=460&h=260`) + : [] +} + +function parseStart(item) { + return item?.params?.start ? dayjs.utc(item.params.start, 'YYYY-MM-DDTHH:mm:ss[Z]') : null +} + +function parseStop(item) { + return item?.params?.end ? dayjs.utc(item.params.end, 'YYYY-MM-DDTHH:mm:ss[Z]') : null +} + +function parseItems(content, date) { + try { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.assets)) return [] + + return data.assets.filter( + p => p?.params?.start && date.isSame(dayjs.utc(p.params.start, 'YYYY-MM-DDTHH:mm:ss[Z]'), 'd') + ) + } catch (err) { + console.log(err) + return [] + } +} + +function loadSessionDetails() { + return axios + .post('https://tvapi-sgn.solocoo.tv/v1/session', { + ssoToken: + 'eyJhbGciOiJkaXIiLCJrZXkiOiJ2c3R2IiwiZW5jIjoiQTEyOENCQy1IUzI1NiJ9..6jMKWv5bSqODWOWLmeERqw._WcKmMW2ij3yPJkhFllQHgXOkW7powvzT-5p6G4_jjYa8vzJybmHu_1CwIEb_s2hVOyaNDi6M-NVLNY9CaNU3aSC-ojZ4UoQ7QLRTFWP-2uY-mL5IgJtL7Xknus5blHJbR8B-xaOODXIJh8PneZORmPHa5EHhs1vOmqpGb1COZwqlw_WFbGT9EsFq6W8fsYH3O5cUqec608Uad-wK59OQIJyofZJwrb6VTthmwwIDxX6Dn-kyYssfdXvPF_BXu5A-e2MFOsdzvMjENdq0FHCk-b9OojzENR6S-JEtSTrZHrgSfHsqb1DwVbtuaetFlV-A3-gxyqqHH7QIvkRM38StNMAp_q8TUauhluwKK3nuXbgogiQ9d9Kc9s7WGoBPOVHsZ4w6wJ9fDBIyhApOJUAdEINi7dLpe1pTBBk6ZA504PVyQ0d6DtdhJhkbT6I88wwxz2U6sF5tInZBcdyZzCa1KKHWQuonTJ4IPcILGQFuzo.lhVv2QaTOaxTS9F4Ht2L3A', + osVersion: 'Windows 10', + deviceModel: 'Chrome', + deviceType: 'PC', + deviceSerial: 'w408a0eb0-d50f-11ef-affa-af9775b838ad', + deviceOem: 'Chrome', + devicePrettyName: 'Chrome 128.0.0.0', + appVersion: '12.1', + language: 'en_US', + brand: 'vstv', + memberId: '0', + featureLevel: 6, + provisionData: + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MzcxNDU4MjYsImljIjp0cnVlLCJ1cCI6ImNwaSIsImJyIjoidnN0diIsImRzIjoidzQwOGEwZWIwLWQ1MGYtMTFlZi1hZmZhLWFmOTc3NWI4MzhhZCIsImRlIjoiYnJhbmRNYXBwaW5nIn0.Ou6yh5qXtlK4NhyWHciVszARr98PLL1TkaXKpqQtub8' + }) + .then(r => r.data) + .catch(console.log) +} diff --git a/sites/xem.kplus.vn/xem.kplus.vn.test.js b/sites/xem.kplus.vn/xem.kplus.vn.test.js index d5bd2bfd..451279a4 100644 --- a/sites/xem.kplus.vn/xem.kplus.vn.test.js +++ b/sites/xem.kplus.vn/xem.kplus.vn.test.js @@ -1,77 +1,77 @@ -const { parser, url, request } = require('./xem.kplus.vn.config.js') -const axios = require('axios') -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) - -jest.mock('axios') - -axios.post.mockImplementation(url => { - if (url === 'https://tvapi-sgn.solocoo.tv/v1/session') { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/session.json'))) - }) - } else { - return Promise.resolve({ - data: {} - }) - } -}) - -const date = dayjs.utc('2025-01-18', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'F31WvdXdwYNOInUaTeIC-ixsLQVsrSNgUczDSFCN', - xmltv_id: 'KPlusKids.vn' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://tvapi-sgn.solocoo.tv/v1/assets?query=schedule,forrelated,F31WvdXdwYNOInUaTeIC-ixsLQVsrSNgUczDSFCN&from=2025-01-18T00:00:00Z&limit=1000' - ) -}) - -it('can generate valid request headers', async () => { - expect(await request.headers()).toMatchObject({ - authorization: - 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0di5zb2xvY29vLmF1dGgiOnsicyI6Inc0MDhhMGViMC1kNTBmLTExZWYtYWZmYS1hZjk3NzViODM4YWQiLCJ1IjoiQW8yZXd1S3o3M2tjX2UtOFZmWGVnZyIsImwiOiJlbl9VUyIsImQiOiJQQyIsImRtIjoiQ2hyb21lIiwib20iOiJPIiwiYyI6ImJ6bXZPVEFOM05qdzZadjYtYnZveThwbnMwNHBtbTdxeG9QOUVwaVNQVzAiLCJzdCI6ImZ1bGwiLCJnIjoiZXlKaWNpSTZJblp6ZEhZaUxDSjFjQ0k2SW1Od2FTSXNJbkIwSWpwbVlXeHpaU3dpWkdVaU9pSmljbUZ1WkUxaGNIQnBibWNpTENKa1lpSTZabUZzYzJWOSIsImYiOjYsImIiOiJ2c3R2In0sIm5iZiI6MTczNzE0NTk1NCwiZXhwIjoxNzM3MTYzNzg0LCJpYXQiOjE3MzcxNDU5NTQsImF1ZCI6ImNwaSJ9.25av5gdR38FW0SmnzNiE4EV1D4Gozox2Wgvoh7QKZaM' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(100) - expect(results[0]).toMatchObject({ - start: '2025-01-18T00:03:00.000Z', - stop: '2025-01-18T00:10:00.000Z', - title: 'Masha and the Bear S1, Ep01', - categories: ['Children'], - images: [ - 'https://img.kplus.vn/images?filename=Media/HDVN/2021_11/KID_CAR_21__2660472_0a13b965-0b37-4552-99d5-a5998ca20156.jpg&orientation=landscape&w=460&h=260' - ] - }) - expect(results[99]).toMatchObject({ - start: '2025-01-18T20:59:00.000Z', - stop: '2025-01-18T21:28:00.000Z', - title: 'KID SHOW: BUG SHAPE BOOKMARK - WOODEATER PAPERWEIGHT', - categories: ['Children'], - images: [ - 'https://img.kplus.vn/images?filename=Media/HDVN/2012_02/KID_EDU_HNCP__VN_20200_9d92b5d2-02da-49ac-969e-4b20aad8ccec.jpg&orientation=landscape&w=460&h=260' - ] - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const result = parser({ content, channel, date }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./xem.kplus.vn.config.js') +const axios = require('axios') +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) + +jest.mock('axios') + +axios.post.mockImplementation(url => { + if (url === 'https://tvapi-sgn.solocoo.tv/v1/session') { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/session.json'))) + }) + } else { + return Promise.resolve({ + data: {} + }) + } +}) + +const date = dayjs.utc('2025-01-18', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'F31WvdXdwYNOInUaTeIC-ixsLQVsrSNgUczDSFCN', + xmltv_id: 'KPlusKids.vn' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://tvapi-sgn.solocoo.tv/v1/assets?query=schedule,forrelated,F31WvdXdwYNOInUaTeIC-ixsLQVsrSNgUczDSFCN&from=2025-01-18T00:00:00Z&limit=1000' + ) +}) + +it('can generate valid request headers', async () => { + expect(await request.headers()).toMatchObject({ + authorization: + 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0di5zb2xvY29vLmF1dGgiOnsicyI6Inc0MDhhMGViMC1kNTBmLTExZWYtYWZmYS1hZjk3NzViODM4YWQiLCJ1IjoiQW8yZXd1S3o3M2tjX2UtOFZmWGVnZyIsImwiOiJlbl9VUyIsImQiOiJQQyIsImRtIjoiQ2hyb21lIiwib20iOiJPIiwiYyI6ImJ6bXZPVEFOM05qdzZadjYtYnZveThwbnMwNHBtbTdxeG9QOUVwaVNQVzAiLCJzdCI6ImZ1bGwiLCJnIjoiZXlKaWNpSTZJblp6ZEhZaUxDSjFjQ0k2SW1Od2FTSXNJbkIwSWpwbVlXeHpaU3dpWkdVaU9pSmljbUZ1WkUxaGNIQnBibWNpTENKa1lpSTZabUZzYzJWOSIsImYiOjYsImIiOiJ2c3R2In0sIm5iZiI6MTczNzE0NTk1NCwiZXhwIjoxNzM3MTYzNzg0LCJpYXQiOjE3MzcxNDU5NTQsImF1ZCI6ImNwaSJ9.25av5gdR38FW0SmnzNiE4EV1D4Gozox2Wgvoh7QKZaM' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(100) + expect(results[0]).toMatchObject({ + start: '2025-01-18T00:03:00.000Z', + stop: '2025-01-18T00:10:00.000Z', + title: 'Masha and the Bear S1, Ep01', + categories: ['Children'], + images: [ + 'https://img.kplus.vn/images?filename=Media/HDVN/2021_11/KID_CAR_21__2660472_0a13b965-0b37-4552-99d5-a5998ca20156.jpg&orientation=landscape&w=460&h=260' + ] + }) + expect(results[99]).toMatchObject({ + start: '2025-01-18T20:59:00.000Z', + stop: '2025-01-18T21:28:00.000Z', + title: 'KID SHOW: BUG SHAPE BOOKMARK - WOODEATER PAPERWEIGHT', + categories: ['Children'], + images: [ + 'https://img.kplus.vn/images?filename=Media/HDVN/2012_02/KID_EDU_HNCP__VN_20200_9d92b5d2-02da-49ac-969e-4b20aad8ccec.jpg&orientation=landscape&w=460&h=260' + ] + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const result = parser({ content, channel, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/xumo.tv/xumo.tv.config.js b/sites/xumo.tv/xumo.tv.config.js index 17306c34..6b8353dc 100644 --- a/sites/xumo.tv/xumo.tv.config.js +++ b/sites/xumo.tv/xumo.tv.config.js @@ -1,134 +1,134 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const API_ENDPOINT = 'https://valencia-app-mds.xumo.com/v2' - -const client = axios.create({ - baseURL: API_ENDPOINT, - responseType: 'arraybuffer' -}) - -module.exports = { - site: 'xumo.tv', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: function ({ date, channel }) { - const [offset] = channel.site_id.split('#') - - return `${API_ENDPOINT}/epg/10006/${date.format( - 'YYYYMMDD' - )}/0.json?f=asset.title&f=asset.descriptions&limit=50&offset=${offset}` - }, - async parser({ content, channel, date }) { - let programs = [] - let items = parseItems(content, channel) - if (!items.length) return programs - const d = date.format('YYYYMMDD') - const [offset] = channel.site_id.split('#') - const promises = [ - client.get( - `/epg/10006/${d}/1.json?f=asset.title&f=asset.descriptions&limit=50&offset=${offset}` - ), - client.get( - `/epg/10006/${d}/2.json?f=asset.title&f=asset.descriptions&limit=50&offset=${offset}` - ), - client.get( - `/epg/10006/${d}/3.json?f=asset.title&f=asset.descriptions&limit=50&offset=${offset}` - ) - ] - const results = await Promise.allSettled(promises) - results.forEach(r => { - if (r.status === 'fulfilled') { - items = items.concat(parseItems(r.value.data, channel)) - } - }) - - items.forEach(item => { - programs.push({ - title: item.title, - sub_title: item.episodeTitle, - description: parseDescription(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const channels = await axios - .get( - 'https://valencia-app-mds.xumo.com/v2/channels/list/10006.json?sort=hybrid&geoId=unknown' - ) - .then(r => r.data.channel.item) - .catch(console.log) - - const promises = [ - axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=0`), - axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=50`), - axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=100`), - axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=150`), - axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=200`), - axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=250`), - axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=300`) - ] - - const output = [] - const results = await Promise.allSettled(promises) - results.forEach((r, i) => { - if (r.status !== 'fulfilled') return - - r.value.data.channels.forEach(item => { - const info = channels.find(c => c.guid.value == item.channelId) - - if (!info) { - console.log(item.channelId) - } - - output.push({ - lang: 'en', - site_id: `${i * 50}#${item.channelId}`, - name: info.title - }) - }) - }) - - return output - } -} - -function parseDescription(item) { - if (!item.descriptions) return null - - return item.descriptions.medium || item.descriptions.small || item.descriptions.tiny -} - -function parseStart(item) { - return dayjs(item.start) -} - -function parseStop(item) { - return dayjs(item.end) -} - -function parseItems(content, channel) { - if (!content) return [] - const [, channelId] = channel.site_id.split('#') - const data = JSON.parse(content) - if (!data || !Array.isArray(data.channels)) return [] - const channelData = data.channels.find(c => c.channelId == channelId) - if (!channelData || !Array.isArray(channelData.schedule)) return [] - - return channelData.schedule - .map(item => { - const details = data.assets[item.assetId] - if (!details) return null - - return { ...item, ...details } - }) - .filter(Boolean) -} +const axios = require('axios') +const dayjs = require('dayjs') + +const API_ENDPOINT = 'https://valencia-app-mds.xumo.com/v2' + +const client = axios.create({ + baseURL: API_ENDPOINT, + responseType: 'arraybuffer' +}) + +module.exports = { + site: 'xumo.tv', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: function ({ date, channel }) { + const [offset] = channel.site_id.split('#') + + return `${API_ENDPOINT}/epg/10006/${date.format( + 'YYYYMMDD' + )}/0.json?f=asset.title&f=asset.descriptions&limit=50&offset=${offset}` + }, + async parser({ content, channel, date }) { + let programs = [] + let items = parseItems(content, channel) + if (!items.length) return programs + const d = date.format('YYYYMMDD') + const [offset] = channel.site_id.split('#') + const promises = [ + client.get( + `/epg/10006/${d}/1.json?f=asset.title&f=asset.descriptions&limit=50&offset=${offset}` + ), + client.get( + `/epg/10006/${d}/2.json?f=asset.title&f=asset.descriptions&limit=50&offset=${offset}` + ), + client.get( + `/epg/10006/${d}/3.json?f=asset.title&f=asset.descriptions&limit=50&offset=${offset}` + ) + ] + const results = await Promise.allSettled(promises) + results.forEach(r => { + if (r.status === 'fulfilled') { + items = items.concat(parseItems(r.value.data, channel)) + } + }) + + items.forEach(item => { + programs.push({ + title: item.title, + sub_title: item.episodeTitle, + description: parseDescription(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const channels = await axios + .get( + 'https://valencia-app-mds.xumo.com/v2/channels/list/10006.json?sort=hybrid&geoId=unknown' + ) + .then(r => r.data.channel.item) + .catch(console.log) + + const promises = [ + axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=0`), + axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=50`), + axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=100`), + axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=150`), + axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=200`), + axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=250`), + axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=300`) + ] + + const output = [] + const results = await Promise.allSettled(promises) + results.forEach((r, i) => { + if (r.status !== 'fulfilled') return + + r.value.data.channels.forEach(item => { + const info = channels.find(c => c.guid.value == item.channelId) + + if (!info) { + console.log(item.channelId) + } + + output.push({ + lang: 'en', + site_id: `${i * 50}#${item.channelId}`, + name: info.title + }) + }) + }) + + return output + } +} + +function parseDescription(item) { + if (!item.descriptions) return null + + return item.descriptions.medium || item.descriptions.small || item.descriptions.tiny +} + +function parseStart(item) { + return dayjs(item.start) +} + +function parseStop(item) { + return dayjs(item.end) +} + +function parseItems(content, channel) { + if (!content) return [] + const [, channelId] = channel.site_id.split('#') + const data = JSON.parse(content) + if (!data || !Array.isArray(data.channels)) return [] + const channelData = data.channels.find(c => c.channelId == channelId) + if (!channelData || !Array.isArray(channelData.schedule)) return [] + + return channelData.schedule + .map(item => { + const details = data.assets[item.assetId] + if (!details) return null + + return { ...item, ...details } + }) + .filter(Boolean) +} diff --git a/sites/xumo.tv/xumo.tv.test.js b/sites/xumo.tv/xumo.tv.test.js index 3fc9c9d0..329d2d73 100644 --- a/sites/xumo.tv/xumo.tv.test.js +++ b/sites/xumo.tv/xumo.tv.test.js @@ -1,74 +1,74 @@ -const { parser, url } = require('./xumo.tv.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', () => { - return { - create: jest.fn().mockReturnValue({ - get: jest.fn() - }) - } -}) - -const API_ENDPOINT = 'https://valencia-app-mds.xumo.com/v2' - -const date = dayjs.utc('2022-11-06', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '0#99991247', - xmltv_id: 'NBCNewsNow.us' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - `${API_ENDPOINT}/epg/10006/20221106/0.json?f=asset.title&f=asset.descriptions&limit=50&offset=0` - ) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0.json')) - - axios.create().get.mockImplementation(url => { - if ( - url === - `${API_ENDPOINT}/epg/10006/20221106/1.json?f=asset.title&f=asset.descriptions&limit=50&offset=0` - ) { - return Promise.resolve({ - data: Buffer.from(fs.readFileSync(path.resolve(__dirname, '__data__/content_1.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-05T23:00:00.000Z', - stop: '2022-11-06T01:00:00.000Z', - title: 'Dateline', - sub_title: 'The Disappearance of Laci Peterson', - description: - "After following Laci Peterson's case for more than 15 years, the show delivers a comprehensive report with rarely seen interrogation video, new insight from prosecutors, and surprising details from Amber Frey, who helped uncover the truth." - }) -}) - -it('can handle empty guide', async () => { - const results = await parser({ - content: Buffer.from(fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json'))), - channel, - date - }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./xumo.tv.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', () => { + return { + create: jest.fn().mockReturnValue({ + get: jest.fn() + }) + } +}) + +const API_ENDPOINT = 'https://valencia-app-mds.xumo.com/v2' + +const date = dayjs.utc('2022-11-06', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '0#99991247', + xmltv_id: 'NBCNewsNow.us' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + `${API_ENDPOINT}/epg/10006/20221106/0.json?f=asset.title&f=asset.descriptions&limit=50&offset=0` + ) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0.json')) + + axios.create().get.mockImplementation(url => { + if ( + url === + `${API_ENDPOINT}/epg/10006/20221106/1.json?f=asset.title&f=asset.descriptions&limit=50&offset=0` + ) { + return Promise.resolve({ + data: Buffer.from(fs.readFileSync(path.resolve(__dirname, '__data__/content_1.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-05T23:00:00.000Z', + stop: '2022-11-06T01:00:00.000Z', + title: 'Dateline', + sub_title: 'The Disappearance of Laci Peterson', + description: + "After following Laci Peterson's case for more than 15 years, the show delivers a comprehensive report with rarely seen interrogation video, new insight from prosecutors, and surprising details from Amber Frey, who helped uncover the truth." + }) +}) + +it('can handle empty guide', async () => { + const results = await parser({ + content: Buffer.from(fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json'))), + channel, + date + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/yes.co.il/yes.co.il.config.js b/sites/yes.co.il/yes.co.il.config.js index f60202be..5594815e 100644 --- a/sites/yes.co.il/yes.co.il.config.js +++ b/sites/yes.co.il/yes.co.il.config.js @@ -1,57 +1,57 @@ -const axios = require('axios') - -module.exports = { - site: 'yes.co.il', - days: 2, - url({ channel, date }) { - return `https://svc.yes.co.il/api/content/broadcast-schedule/channels/${ - channel.site_id - }?date=${date.format('YYYY-M-D')}&ignorePastItems=true` - }, - request: { - headers: { - 'user-agent': - 'Mozilla/5.0 (Linux; Linux x86_64) AppleWebKit/600.3 (KHTML, like Gecko) Chrome/48.0.2544.291 Safari/600' - } - }, - parser({ content }) { - const items = parseItems(content) - - return items.map(item => ({ - title: item.title, - description: item.description, - image: item.imageUrl, - start: item.starts, - stop: item.ends - })) - }, - async channels() { - const data = await axios - .get('https://svc.yes.co.il/api/content/broadcast-schedule/channels?page=0&pageSize=1000', { - headers: { - 'accept-language': 'he-IL', - 'user-agent': - 'Mozilla/5.0 (Linux; Linux x86_64) AppleWebKit/600.3 (KHTML, like Gecko) Chrome/48.0.2544.291 Safari/600' - } - }) - .then(r => r.data) - .catch(console.error) - - return data.items.map(channel => ({ - lang: 'he', - name: channel.title, - site_id: channel.channelId - })) - } -} - -function parseItems(content) { - try { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.items)) return [] - - return data.items - } catch { - return [] - } -} +const axios = require('axios') + +module.exports = { + site: 'yes.co.il', + days: 2, + url({ channel, date }) { + return `https://svc.yes.co.il/api/content/broadcast-schedule/channels/${ + channel.site_id + }?date=${date.format('YYYY-M-D')}&ignorePastItems=true` + }, + request: { + headers: { + 'user-agent': + 'Mozilla/5.0 (Linux; Linux x86_64) AppleWebKit/600.3 (KHTML, like Gecko) Chrome/48.0.2544.291 Safari/600' + } + }, + parser({ content }) { + const items = parseItems(content) + + return items.map(item => ({ + title: item.title, + description: item.description, + image: item.imageUrl, + start: item.starts, + stop: item.ends + })) + }, + async channels() { + const data = await axios + .get('https://svc.yes.co.il/api/content/broadcast-schedule/channels?page=0&pageSize=1000', { + headers: { + 'accept-language': 'he-IL', + 'user-agent': + 'Mozilla/5.0 (Linux; Linux x86_64) AppleWebKit/600.3 (KHTML, like Gecko) Chrome/48.0.2544.291 Safari/600' + } + }) + .then(r => r.data) + .catch(console.error) + + return data.items.map(channel => ({ + lang: 'he', + name: channel.title, + site_id: channel.channelId + })) + } +} + +function parseItems(content) { + try { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.items)) return [] + + return data.items + } catch { + return [] + } +} diff --git a/sites/yes.co.il/yes.co.il.test.js b/sites/yes.co.il/yes.co.il.test.js index e6aa8eeb..f0e346fa 100644 --- a/sites/yes.co.il/yes.co.il.test.js +++ b/sites/yes.co.il/yes.co.il.test.js @@ -1,48 +1,48 @@ -const { parser, url, request } = require('./yes.co.il.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-30', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'YSA1' } - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://svc.yes.co.il/api/content/broadcast-schedule/channels/YSA1?date=2025-1-30&ignorePastItems=true' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'user-agent': - 'Mozilla/5.0 (Linux; Linux x86_64) AppleWebKit/600.3 (KHTML, like Gecko) Chrome/48.0.2544.291 Safari/600' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - - let results = parser({ content }) - - expect(results.length).toBe(13) - expect(results[0]).toMatchObject({ - title: 'הבית', - description: - "דרמת מתח סוחפת. זוג צעיר שוכר וילה בכפר רומנטי באיטליה כדי לשפר את הזוגיות שלהם, אך עד מהרה מוצאים עצמם קורבנות בתוכנית זדונית של בעל המקום. עם: ארון פול ('שובר שורות'), אמילי רטאייקאוסקי. במאי: ג'ורג' רטליף. 2018.", - image: 'https://fykswkmjb.filerobot.com/VodAndHomeChan/VP001212610.JPG', - start: '2025-01-29T23:52:00Z', - stop: '2025-01-30T01:31:00Z' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: '{"items":[]}' - }) - - expect(results).toMatchObject([]) -}) +const { parser, url, request } = require('./yes.co.il.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-30', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'YSA1' } + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://svc.yes.co.il/api/content/broadcast-schedule/channels/YSA1?date=2025-1-30&ignorePastItems=true' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'user-agent': + 'Mozilla/5.0 (Linux; Linux x86_64) AppleWebKit/600.3 (KHTML, like Gecko) Chrome/48.0.2544.291 Safari/600' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + + let results = parser({ content }) + + expect(results.length).toBe(13) + expect(results[0]).toMatchObject({ + title: 'הבית', + description: + "דרמת מתח סוחפת. זוג צעיר שוכר וילה בכפר רומנטי באיטליה כדי לשפר את הזוגיות שלהם, אך עד מהרה מוצאים עצמם קורבנות בתוכנית זדונית של בעל המקום. עם: ארון פול ('שובר שורות'), אמילי רטאייקאוסקי. במאי: ג'ורג' רטליף. 2018.", + image: 'https://fykswkmjb.filerobot.com/VodAndHomeChan/VP001212610.JPG', + start: '2025-01-29T23:52:00Z', + stop: '2025-01-30T01:31:00Z' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: '{"items":[]}' + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/zap.co.ao/zap.co.ao.config.js b/sites/zap.co.ao/zap.co.ao.config.js index 267ac859..165900a0 100644 --- a/sites/zap.co.ao/zap.co.ao.config.js +++ b/sites/zap.co.ao/zap.co.ao.config.js @@ -1,48 +1,48 @@ -const { DateTime } = require('luxon') -const axios = require('axios') - -module.exports = { - site: 'zap.co.ao', - days: 2, - url: function ({ date, channel }) { - return `https://zapon.zapsi.net/ao/m/api/epg/events?date=${date.format('YYYYMMDD')}&channel=${ - channel.site_id - }` - }, - parser: function ({ content }) { - const programs = [] - const items = parseItems(content) - if (!items.length) return programs - items.forEach(item => { - programs.push({ - title: item.programName, - description: item.programDescription, - category: item.categoryName, - start: DateTime.fromSeconds(item.utcBeginDate).toUTC(), - stop: DateTime.fromSeconds(item.utcEndDate).toUTC() - }) - }) - - return programs - }, - async channels() { - const channels = await axios - .get('https://zapon.zapsi.net/ao/m/api/epg/channels') - .then(r => r.data.data) - .catch(console.log) - - return channels.map(item => { - return { - lang: 'pt', - site_id: item.id, - name: item.name - } - }) - } -} - -function parseItems(content) { - const data = JSON.parse(content) - - return data.data || [] -} +const { DateTime } = require('luxon') +const axios = require('axios') + +module.exports = { + site: 'zap.co.ao', + days: 2, + url: function ({ date, channel }) { + return `https://zapon.zapsi.net/ao/m/api/epg/events?date=${date.format('YYYYMMDD')}&channel=${ + channel.site_id + }` + }, + parser: function ({ content }) { + const programs = [] + const items = parseItems(content) + if (!items.length) return programs + items.forEach(item => { + programs.push({ + title: item.programName, + description: item.programDescription, + category: item.categoryName, + start: DateTime.fromSeconds(item.utcBeginDate).toUTC(), + stop: DateTime.fromSeconds(item.utcEndDate).toUTC() + }) + }) + + return programs + }, + async channels() { + const channels = await axios + .get('https://zapon.zapsi.net/ao/m/api/epg/channels') + .then(r => r.data.data) + .catch(console.log) + + return channels.map(item => { + return { + lang: 'pt', + site_id: item.id, + name: item.name + } + }) + } +} + +function parseItems(content) { + const data = JSON.parse(content) + + return data.data || [] +} diff --git a/sites/zap.co.ao/zap.co.ao.test.js b/sites/zap.co.ao/zap.co.ao.test.js index 53de59bb..1f04c99d 100644 --- a/sites/zap.co.ao/zap.co.ao.test.js +++ b/sites/zap.co.ao/zap.co.ao.test.js @@ -1,45 +1,45 @@ -const { parser, url } = require('./zap.co.ao.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('2023-05-28', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2275', - xmltv_id: 'TPA1.ao' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://zapon.zapsi.net/ao/m/api/epg/events?date=20230528&channel=2275' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') - const results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-05-27T23:00:00.000Z', - stop: '2023-05-28T00:00:00.000Z', - title: 'Jornal da Meia-Noite', - description: - 'Um jornal diferente do Telejornal, por conter análise, comentários e coluna com jornalistas experientes sobre factos do dia a dia.', - category: 'Noticiário' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: '[]' - }) - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./zap.co.ao.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('2023-05-28', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2275', + xmltv_id: 'TPA1.ao' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://zapon.zapsi.net/ao/m/api/epg/events?date=20230528&channel=2275' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') + const results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-05-27T23:00:00.000Z', + stop: '2023-05-28T00:00:00.000Z', + title: 'Jornal da Meia-Noite', + description: + 'Um jornal diferente do Telejornal, por conter análise, comentários e coluna com jornalistas experientes sobre factos do dia a dia.', + category: 'Noticiário' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: '[]' + }) + expect(results).toMatchObject([]) +}) diff --git a/sites/zap2it.com/zap2it.com.config.js b/sites/zap2it.com/zap2it.com.config.js index 91a2fb86..9cb96a66 100644 --- a/sites/zap2it.com/zap2it.com.config.js +++ b/sites/zap2it.com/zap2it.com.config.js @@ -1,71 +1,71 @@ -const dayjs = require('dayjs') -const timezone = require('dayjs/plugin/timezone') -const utc = require('dayjs/plugin/utc') -const isBetween = require('dayjs/plugin/isBetween') - -dayjs.extend(timezone) -dayjs.extend(utc) -dayjs.extend(isBetween) - -module.exports = { - site: 'zap2it.com', - days: 2, - url: 'https://tvlistings.gracenote.com/api/sslgrid', - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36', - }, - data({ date, channel }) { - const [device, lineupId, headendId, countryCode, postalCode, prgsvcid] = channel.site_id.split('/') - - const timestamp = dayjs(date).unix().toString() - - return { - lineupId, - IsSSLinkNavigation: 'true', - timespan: '336', - timestamp, - prgsvcid, - headendId, - countryCode, - postalCode, - device, - userId: '-', - aid: 'gapzap', - DSTUTCOffset: '-240', - STDUTCOffset: '-300', - DSTStart: '2025-03-09T02:00Z', - DSTEnd: '2025-11-02T02:00Z', - languagecode: 'en-us', - } - }, - }, - parser: function ({ content, date }) { - const data = JSON.parse(content) - const programs = [] - - Object.keys(data).forEach(dateKey => { - data[dateKey].forEach(item => { - programs.push({ - title: item.program.title, - subTitle: item.program.episodeTitle || '', - description: item.program.shortDesc || '', - genres: item.program.genres ? item.program.genres.map(genre => genre.name) : [], - start: dayjs.unix(item.startTime).utc().format('YYYY-MM-DD HH:mm:ss'), - stop: dayjs.unix(item.endTime).utc().format('YYYY-MM-DD HH:mm:ss'), - icon: item.thumbnail ? `https://zap2it.tmsimg.com/assets/${item.thumbnail}.jpg` : '', - rating: item.rating || '', - season: item.program.season || '', - episode: item.program.episode || '', - date: item.program.releaseYear || '', - }) - }) - }) - - return programs.filter(p => dayjs(p.start).add(dayjs(p.start).utcOffset(), 'minute').isBetween(date.startOf('day').subtract(dayjs().utcOffset(), 'minute').utc(), - date.endOf('day').subtract(dayjs().utcOffset(), 'minute').utc(), 'second', '[]')) - } -} +const dayjs = require('dayjs') +const timezone = require('dayjs/plugin/timezone') +const utc = require('dayjs/plugin/utc') +const isBetween = require('dayjs/plugin/isBetween') + +dayjs.extend(timezone) +dayjs.extend(utc) +dayjs.extend(isBetween) + +module.exports = { + site: 'zap2it.com', + days: 2, + url: 'https://tvlistings.gracenote.com/api/sslgrid', + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36', + }, + data({ date, channel }) { + const [device, lineupId, headendId, countryCode, postalCode, prgsvcid] = channel.site_id.split('/') + + const timestamp = dayjs(date).unix().toString() + + return { + lineupId, + IsSSLinkNavigation: 'true', + timespan: '336', + timestamp, + prgsvcid, + headendId, + countryCode, + postalCode, + device, + userId: '-', + aid: 'gapzap', + DSTUTCOffset: '-240', + STDUTCOffset: '-300', + DSTStart: '2025-03-09T02:00Z', + DSTEnd: '2025-11-02T02:00Z', + languagecode: 'en-us', + } + }, + }, + parser: function ({ content, date }) { + const data = JSON.parse(content) + const programs = [] + + Object.keys(data).forEach(dateKey => { + data[dateKey].forEach(item => { + programs.push({ + title: item.program.title, + subTitle: item.program.episodeTitle || '', + description: item.program.shortDesc || '', + genres: item.program.genres ? item.program.genres.map(genre => genre.name) : [], + start: dayjs.unix(item.startTime).utc().format('YYYY-MM-DD HH:mm:ss'), + stop: dayjs.unix(item.endTime).utc().format('YYYY-MM-DD HH:mm:ss'), + icon: item.thumbnail ? `https://zap2it.tmsimg.com/assets/${item.thumbnail}.jpg` : '', + rating: item.rating || '', + season: item.program.season || '', + episode: item.program.episode || '', + date: item.program.releaseYear || '', + }) + }) + }) + + return programs.filter(p => dayjs(p.start).add(dayjs(p.start).utcOffset(), 'minute').isBetween(date.startOf('day').subtract(dayjs().utcOffset(), 'minute').utc(), + date.endOf('day').subtract(dayjs().utcOffset(), 'minute').utc(), 'second', '[]')) + } +} diff --git a/sites/zap2it.com/zap2it.com.test.js b/sites/zap2it.com/zap2it.com.test.js index 0b8b6466..625abac6 100644 --- a/sites/zap2it.com/zap2it.com.test.js +++ b/sites/zap2it.com/zap2it.com.test.js @@ -1,74 +1,74 @@ -const { parser, url } = require('./zap2it.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -// Se cambia la fecha para que, tras aplicar el offset (debido a TZ=Pacific/Nauru), -// el rango filtrado incluya los programas cuyo start en UTC es el 6 de febrero. -const date = dayjs('2025-02-07', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'X/USA-NY31695-DEFAULT/NY31695/USA/13302/49141', xmltv_id: 'Spectrum News 1' } - -it('can generate valid url', () => { - expect(url).toBe('https://tvlistings.gracenote.com/api/sslgrid') -}) - -it('can parse response', () => { - const content = JSON.stringify({ - '2025-02-06': [ - { - program: { - title: 'Your Afternoon on Spectrum News 1 - Central NY', - episodeTitle: '', - shortDesc: '', - genres: [], - season: '', - episode: '', - releaseYear: '' - }, - startTime: 1738868400, - endTime: 1738872000, - thumbnail: '' - }, - { - program: { - title: 'Your Afternoon on Spectrum News 1 - Central NY', - episodeTitle: '', - shortDesc: '', - genres: [], - season: '', - episode: '', - releaseYear: '' - }, - startTime: 1738872000, - endTime: 1738875600, - thumbnail: '' - } - ] - }) - - const results = parser({ content, date }) - - expect(results.length).toBe(2) - expect(results[0]).toMatchObject({ - title: 'Your Afternoon on Spectrum News 1 - Central NY', - start: dayjs.unix(1738868400).utc().format('YYYY-MM-DD HH:mm:ss'), - stop: dayjs.unix(1738872000).utc().format('YYYY-MM-DD HH:mm:ss') - }) - expect(results[1]).toMatchObject({ - title: 'Your Afternoon on Spectrum News 1 - Central NY', - start: dayjs.unix(1738872000).utc().format('YYYY-MM-DD HH:mm:ss'), - stop: dayjs.unix(1738875600).utc().format('YYYY-MM-DD HH:mm:ss') - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{}' - }) - - expect(result).toEqual([]) -}) +const { parser, url } = require('./zap2it.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +// Se cambia la fecha para que, tras aplicar el offset (debido a TZ=Pacific/Nauru), +// el rango filtrado incluya los programas cuyo start en UTC es el 6 de febrero. +const date = dayjs('2025-02-07', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'X/USA-NY31695-DEFAULT/NY31695/USA/13302/49141', xmltv_id: 'Spectrum News 1' } + +it('can generate valid url', () => { + expect(url).toBe('https://tvlistings.gracenote.com/api/sslgrid') +}) + +it('can parse response', () => { + const content = JSON.stringify({ + '2025-02-06': [ + { + program: { + title: 'Your Afternoon on Spectrum News 1 - Central NY', + episodeTitle: '', + shortDesc: '', + genres: [], + season: '', + episode: '', + releaseYear: '' + }, + startTime: 1738868400, + endTime: 1738872000, + thumbnail: '' + }, + { + program: { + title: 'Your Afternoon on Spectrum News 1 - Central NY', + episodeTitle: '', + shortDesc: '', + genres: [], + season: '', + episode: '', + releaseYear: '' + }, + startTime: 1738872000, + endTime: 1738875600, + thumbnail: '' + } + ] + }) + + const results = parser({ content, date }) + + expect(results.length).toBe(2) + expect(results[0]).toMatchObject({ + title: 'Your Afternoon on Spectrum News 1 - Central NY', + start: dayjs.unix(1738868400).utc().format('YYYY-MM-DD HH:mm:ss'), + stop: dayjs.unix(1738872000).utc().format('YYYY-MM-DD HH:mm:ss') + }) + expect(results[1]).toMatchObject({ + title: 'Your Afternoon on Spectrum News 1 - Central NY', + start: dayjs.unix(1738872000).utc().format('YYYY-MM-DD HH:mm:ss'), + stop: dayjs.unix(1738875600).utc().format('YYYY-MM-DD HH:mm:ss') + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{}' + }) + + expect(result).toEqual([]) +}) diff --git a/sites/ziggogo.tv/ziggogo.tv.config.js b/sites/ziggogo.tv/ziggogo.tv.config.js index 480904dc..ecc5ad2b 100644 --- a/sites/ziggogo.tv/ziggogo.tv.config.js +++ b/sites/ziggogo.tv/ziggogo.tv.config.js @@ -1,114 +1,114 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const doFetch = require('@ntlab/sfetch') -const debug = require('debug')('site:ziggogo.tv') - -dayjs.extend(utc) - -doFetch.setDebugger(debug) - -const detailedGuide = true - -module.exports = { - site: 'ziggogo.tv', - days: 2, - request: { - cache: { - ttl: 24 * 60 * 60 * 1000 // 1 day - } - }, - url({ date, segment = 0 }) { - return `https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/en/events/segments/${date.format( - 'YYYYMMDD' - )}${segment.toString().padStart(2, '0')}0000` - }, - async parser({ content, channel, date }) { - const programs = [] - if (content) { - const items = typeof content === 'string' ? JSON.parse(content) : content - if (Array.isArray(items.entries)) { - // fetch other segments - const queues = [ - module.exports.url({ date, segment: 6 }), - module.exports.url({ date, segment: 12 }), - module.exports.url({ date, segment: 18 }) - ] - await doFetch(queues, (url, res) => { - if (Array.isArray(res.entries)) { - items.entries.push(...res.entries) - } - }) - items.entries - .filter(item => item.channelId === channel.site_id) - .forEach(item => { - if (Array.isArray(item.events)) { - if (detailedGuide) { - queues.push( - ...item.events.map( - event => - `https://spark-prod-nl.gnp.cloud.ziggogo.tv/eng/web/linear-service/v2/replayEvent/${event.id}?returnLinearContent=true&forceLinearResponse=true&language=nl` - ) - ) - } else { - item.events.forEach(event => { - programs.push({ - title: event.title, - start: dayjs.utc(event.startTime * 1000), - stop: dayjs.utc(event.endTime * 1000) - }) - }) - } - } - }) - // fetch detailed guide - if (queues.length) { - await doFetch(queues, (url, res) => { - programs.push({ - title: res.title, - subTitle: res.episodeName, - description: res.longDescription ? res.longDescription : res.shortDescription, - category: res.genres, - season: res.seasonNumber, - episode: res.episodeNumber, - country: res.countryOfOrigin, - actor: res.actors, - director: res.directors, - producer: res.producers, - date: res.productionDate, - start: dayjs.utc(res.startTime * 1000), - stop: dayjs.utc(res.endTime * 1000) - }) - }) - } - } - } - - return programs - }, - async channels() { - const channels = [] - const axios = require('axios') - const res = await axios - .get( - 'https://spark-prod-nl.gnp.cloud.ziggogo.tv/eng/web/linear-service/v2/channels?cityId=65535&language=en&productClass=Orion-DASH&platform=web' - ) - .then(r => r.data) - .catch(console.error) - - if (Array.isArray(res)) { - channels.push( - ...res - .filter(item => !item.isHidden) - .map(item => { - return { - lang: 'nl', - site_id: item.id, - name: item.name - } - }) - ) - } - - return channels - } -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const doFetch = require('@ntlab/sfetch') +const debug = require('debug')('site:ziggogo.tv') + +dayjs.extend(utc) + +doFetch.setDebugger(debug) + +const detailedGuide = true + +module.exports = { + site: 'ziggogo.tv', + days: 2, + request: { + cache: { + ttl: 24 * 60 * 60 * 1000 // 1 day + } + }, + url({ date, segment = 0 }) { + return `https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/en/events/segments/${date.format( + 'YYYYMMDD' + )}${segment.toString().padStart(2, '0')}0000` + }, + async parser({ content, channel, date }) { + const programs = [] + if (content) { + const items = typeof content === 'string' ? JSON.parse(content) : content + if (Array.isArray(items.entries)) { + // fetch other segments + const queues = [ + module.exports.url({ date, segment: 6 }), + module.exports.url({ date, segment: 12 }), + module.exports.url({ date, segment: 18 }) + ] + await doFetch(queues, (url, res) => { + if (Array.isArray(res.entries)) { + items.entries.push(...res.entries) + } + }) + items.entries + .filter(item => item.channelId === channel.site_id) + .forEach(item => { + if (Array.isArray(item.events)) { + if (detailedGuide) { + queues.push( + ...item.events.map( + event => + `https://spark-prod-nl.gnp.cloud.ziggogo.tv/eng/web/linear-service/v2/replayEvent/${event.id}?returnLinearContent=true&forceLinearResponse=true&language=nl` + ) + ) + } else { + item.events.forEach(event => { + programs.push({ + title: event.title, + start: dayjs.utc(event.startTime * 1000), + stop: dayjs.utc(event.endTime * 1000) + }) + }) + } + } + }) + // fetch detailed guide + if (queues.length) { + await doFetch(queues, (url, res) => { + programs.push({ + title: res.title, + subTitle: res.episodeName, + description: res.longDescription ? res.longDescription : res.shortDescription, + category: res.genres, + season: res.seasonNumber, + episode: res.episodeNumber, + country: res.countryOfOrigin, + actor: res.actors, + director: res.directors, + producer: res.producers, + date: res.productionDate, + start: dayjs.utc(res.startTime * 1000), + stop: dayjs.utc(res.endTime * 1000) + }) + }) + } + } + } + + return programs + }, + async channels() { + const channels = [] + const axios = require('axios') + const res = await axios + .get( + 'https://spark-prod-nl.gnp.cloud.ziggogo.tv/eng/web/linear-service/v2/channels?cityId=65535&language=en&productClass=Orion-DASH&platform=web' + ) + .then(r => r.data) + .catch(console.error) + + if (Array.isArray(res)) { + channels.push( + ...res + .filter(item => !item.isHidden) + .map(item => { + return { + lang: 'nl', + site_id: item.id, + name: item.name + } + }) + ) + } + + return channels + } +} diff --git a/sites/ziggogo.tv/ziggogo.tv.test.js b/sites/ziggogo.tv/ziggogo.tv.test.js index bfc5b2d4..39a603d1 100644 --- a/sites/ziggogo.tv/ziggogo.tv.test.js +++ b/sites/ziggogo.tv/ziggogo.tv.test.js @@ -1,105 +1,105 @@ -const { parser, url } = require('./ziggogo.tv.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('2024-12-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'NL_000001_019401', - xmltv_id: 'NPO1.nl' -} - -axios.get.mockImplementation(url => { - const urls = { - 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/en/events/segments/20241217000000': - 'content00.json', - 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/en/events/segments/20241217060000': - 'content06.json', - 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/en/events/segments/20241217120000': - 'content12.json', - 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/en/events/segments/20241217180000': - 'content18.json', - 'https://spark-prod-nl.gnp.cloud.ziggogo.tv/eng/web/linear-service/v2/replayEvent/crid:~~2F~~2Fgn.tv~~2F28844562~~2FEP027607161610,imi:1d49feeb2ef4e3db0bde030e7cf6e55e06d56fed?returnLinearContent=true&forceLinearResponse=true&language=nl': - 'program01.json', - 'https://spark-prod-nl.gnp.cloud.ziggogo.tv/eng/web/linear-service/v2/replayEvent/crid:~~2F~~2Fgn.tv~~2F28842707~~2FEP022675661065,imi:33138a61bfa639696f386a5b8da9052e98cffdf8?returnLinearContent=true&forceLinearResponse=true&language=nl': - 'program02.json', - 'https://spark-prod-nl.gnp.cloud.ziggogo.tv/eng/web/linear-service/v2/replayEvent/crid:~~2F~~2Fgn.tv~~2F28728829~~2FEP052397600066,imi:34a0b026912de96e3546b15ad2983070a250dfd5?returnLinearContent=true&forceLinearResponse=true&language=nl': - 'program03.json' - } - let data = '' - if (urls[url] !== undefined) { - data = fs.readFileSync(path.join(__dirname, '__data__', urls[url])).toString() - if (!urls[url].startsWith('content00')) { - data = JSON.parse(data) - } - } - return Promise.resolve({ data }) -}) - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/en/events/segments/20241217000000' - ) -}) - -it('can parse response', async () => { - const content = await axios - .get(url({ date })) - .then(response => response.data) - .catch(console.error) - const result = (await parser({ content, channel, date })).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result.length).toBe(3) - expect(result[0]).toMatchObject({ - start: '2024-12-17T00:10:00.000Z', - stop: '2024-12-17T00:35:00.000Z', - title: 'EenVandaag', - description: - 'Op pad met HTS-rebellen in Syrië. Nieuwe aanpak tegen te veel zitten. VS heeft Tiktok-ban bijna rond. Wat is de rol van Nederland in de onderhandeling rondom Oekraïne?', - category: ['Nieuws', 'Actualiteit'], - season: 11, - episode: 300, - actor: [ - 'Rik van de Westelaken', - 'Roos Moggré', - 'Pieter Jan Hagens', - 'Toine van Peperstraten', - 'Charlotte Nijs', - 'Hila Noorzai', - 'Rob Hadders', - 'Joyce Boverhuis' - ] - }) - expect(result[2]).toMatchObject({ - start: '2024-12-17T14:55:00.000Z', - stop: '2024-12-17T15:58:00.000Z', - title: 'Bar Laat', - description: - 'Bij het Rijnstate Ziekenhuis zijn opnieuw enorme misstanden aan het licht gekomen rond spermadonatie. KRO-NCRV maakte er een docuserie over. Maker Annemieke Ruggenberg schuift aan samen met zaaddonor Peter en donorkinderen Roos en Maria.', - category: ['Talkshow'], - season: 1, - episode: 65, - actor: ['Sophie Hilbrand', 'Jeroen Pauw', 'Tim de Wit'] - }) -}) - -it('can handle empty guide', async () => { - const result = await parser({ - content: '', - channel, - date - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./ziggogo.tv.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('2024-12-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'NL_000001_019401', + xmltv_id: 'NPO1.nl' +} + +axios.get.mockImplementation(url => { + const urls = { + 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/en/events/segments/20241217000000': + 'content00.json', + 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/en/events/segments/20241217060000': + 'content06.json', + 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/en/events/segments/20241217120000': + 'content12.json', + 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/en/events/segments/20241217180000': + 'content18.json', + 'https://spark-prod-nl.gnp.cloud.ziggogo.tv/eng/web/linear-service/v2/replayEvent/crid:~~2F~~2Fgn.tv~~2F28844562~~2FEP027607161610,imi:1d49feeb2ef4e3db0bde030e7cf6e55e06d56fed?returnLinearContent=true&forceLinearResponse=true&language=nl': + 'program01.json', + 'https://spark-prod-nl.gnp.cloud.ziggogo.tv/eng/web/linear-service/v2/replayEvent/crid:~~2F~~2Fgn.tv~~2F28842707~~2FEP022675661065,imi:33138a61bfa639696f386a5b8da9052e98cffdf8?returnLinearContent=true&forceLinearResponse=true&language=nl': + 'program02.json', + 'https://spark-prod-nl.gnp.cloud.ziggogo.tv/eng/web/linear-service/v2/replayEvent/crid:~~2F~~2Fgn.tv~~2F28728829~~2FEP052397600066,imi:34a0b026912de96e3546b15ad2983070a250dfd5?returnLinearContent=true&forceLinearResponse=true&language=nl': + 'program03.json' + } + let data = '' + if (urls[url] !== undefined) { + data = fs.readFileSync(path.join(__dirname, '__data__', urls[url])).toString() + if (!urls[url].startsWith('content00')) { + data = JSON.parse(data) + } + } + return Promise.resolve({ data }) +}) + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/en/events/segments/20241217000000' + ) +}) + +it('can parse response', async () => { + const content = await axios + .get(url({ date })) + .then(response => response.data) + .catch(console.error) + const result = (await parser({ content, channel, date })).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result.length).toBe(3) + expect(result[0]).toMatchObject({ + start: '2024-12-17T00:10:00.000Z', + stop: '2024-12-17T00:35:00.000Z', + title: 'EenVandaag', + description: + 'Op pad met HTS-rebellen in Syrië. Nieuwe aanpak tegen te veel zitten. VS heeft Tiktok-ban bijna rond. Wat is de rol van Nederland in de onderhandeling rondom Oekraïne?', + category: ['Nieuws', 'Actualiteit'], + season: 11, + episode: 300, + actor: [ + 'Rik van de Westelaken', + 'Roos Moggré', + 'Pieter Jan Hagens', + 'Toine van Peperstraten', + 'Charlotte Nijs', + 'Hila Noorzai', + 'Rob Hadders', + 'Joyce Boverhuis' + ] + }) + expect(result[2]).toMatchObject({ + start: '2024-12-17T14:55:00.000Z', + stop: '2024-12-17T15:58:00.000Z', + title: 'Bar Laat', + description: + 'Bij het Rijnstate Ziekenhuis zijn opnieuw enorme misstanden aan het licht gekomen rond spermadonatie. KRO-NCRV maakte er een docuserie over. Maker Annemieke Ruggenberg schuift aan samen met zaaddonor Peter en donorkinderen Roos en Maria.', + category: ['Talkshow'], + season: 1, + episode: 65, + actor: ['Sophie Hilbrand', 'Jeroen Pauw', 'Tim de Wit'] + }) +}) + +it('can handle empty guide', async () => { + const result = await parser({ + content: '', + channel, + date + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/znbc.co.zm/znbc.co.zm.config.js b/sites/znbc.co.zm/znbc.co.zm.config.js index 3d020e88..555d51e0 100644 --- a/sites/znbc.co.zm/znbc.co.zm.config.js +++ b/sites/znbc.co.zm/znbc.co.zm.config.js @@ -1,65 +1,65 @@ -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 tabletojson = require('tabletojson').Tabletojson - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'znbc.co.zm', - days: 2, - url({ channel }) { - return `https://www.znbc.co.zm/${channel.site_id}/` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ title: item.title, start, stop }) - }) - - return programs - } -} - -function parseStart(item, date) { - const dateString = `${date.format('YYYY-MM-DD')} ${item.time}` - - return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Africa/Lusaka') -} - -function parseItems(content, date) { - const dayOfWeek = date.format('dddd').toUpperCase() - const $ = cheerio.load(content) - const table = $(`.elementor-tab-mobile-title:contains("${dayOfWeek}")`).next().html() - if (!table) return [] - const data = tabletojson.convert(table) - if (!Array.isArray(data) || !Array.isArray(data[0])) return [] - - return data[0] - .map(row => { - const [, time, title] = row['0'].replace(/\s\s/g, ' ').match(/^(\d{2}:\d{2}) (.*)/) || [ - null, - null, - null - ] - if (!time || !title.trim()) return null - - return { time, title: title.trim() } - }) - .filter(i => i) -} +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 tabletojson = require('tabletojson').Tabletojson + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'znbc.co.zm', + days: 2, + url({ channel }) { + return `https://www.znbc.co.zm/${channel.site_id}/` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ title: item.title, start, stop }) + }) + + return programs + } +} + +function parseStart(item, date) { + const dateString = `${date.format('YYYY-MM-DD')} ${item.time}` + + return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Africa/Lusaka') +} + +function parseItems(content, date) { + const dayOfWeek = date.format('dddd').toUpperCase() + const $ = cheerio.load(content) + const table = $(`.elementor-tab-mobile-title:contains("${dayOfWeek}")`).next().html() + if (!table) return [] + const data = tabletojson.convert(table) + if (!Array.isArray(data) || !Array.isArray(data[0])) return [] + + return data[0] + .map(row => { + const [, time, title] = row['0'].replace(/\s\s/g, ' ').match(/^(\d{2}:\d{2}) (.*)/) || [ + null, + null, + null + ] + if (!time || !title.trim()) return null + + return { time, title: title.trim() } + }) + .filter(i => i) +} diff --git a/sites/znbc.co.zm/znbc.co.zm.test.js b/sites/znbc.co.zm/znbc.co.zm.test.js index 1bbe0835..e8ddf2be 100644 --- a/sites/znbc.co.zm/znbc.co.zm.test.js +++ b/sites/znbc.co.zm/znbc.co.zm.test.js @@ -1,53 +1,53 @@ -const { parser, url } = require('./znbc.co.zm.config.js') -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('2021-11-25', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'tv1', - xmltv_id: 'ZNBCTV1.zm' -} -const content = - '
      ' - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://www.znbc.co.zm/tv1/') -}) - -it('can parse response', () => { - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-24T22:00:00.000Z', - stop: '2021-11-24T23:00:00.000Z', - title: 'MAIN NEWS – RPT' - }, - { - start: '2021-11-24T23:00:00.000Z', - stop: '2021-11-25T00:00:00.000Z', - title: 'BORN & BRED – Rebroadcast (Tuesday Edition)' - }, - { - start: '2021-11-25T00:00:00.000Z', - stop: '2021-11-25T00:30:00.000Z', - title: 'DOCUMENTARY – DW' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./znbc.co.zm.config.js') +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('2021-11-25', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'tv1', + xmltv_id: 'ZNBCTV1.zm' +} +const content = + '
      ' + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://www.znbc.co.zm/tv1/') +}) + +it('can parse response', () => { + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-24T22:00:00.000Z', + stop: '2021-11-24T23:00:00.000Z', + title: 'MAIN NEWS – RPT' + }, + { + start: '2021-11-24T23:00:00.000Z', + stop: '2021-11-25T00:00:00.000Z', + title: 'BORN & BRED – Rebroadcast (Tuesday Edition)' + }, + { + start: '2021-11-25T00:00:00.000Z', + stop: '2021-11-25T00:30:00.000Z', + title: 'DOCUMENTARY – DW' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/zuragt.mn/zuragt.mn.config.js b/sites/zuragt.mn/zuragt.mn.config.js index e20681b4..ed197342 100644 --- a/sites/zuragt.mn/zuragt.mn.config.js +++ b/sites/zuragt.mn/zuragt.mn.config.js @@ -1,89 +1,89 @@ -const dayjs = require('dayjs') -const axios = require('axios') -const cheerio = require('cheerio') -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: 'zuragt.mn', - days: 2, - url: function ({ channel, date }) { - return `https://m.zuragt.mn/channel/${channel.site_id}/?date=${date.format('YYYY-MM-DD')}` - }, - request: { - maxRedirects: 0, - validateStatus: function (status) { - return status >= 200 && status < 303 - } - }, - parser: async function ({ content, date }) { - let programs = [] - const items = parseItems(content) - for (let item of items) { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - start, - stop - }) - } - - return programs - }, - async channels() { - let html = await axios - .get('https://www.zuragt.mn/') - .then(r => r.data) - .catch(console.log) - let $ = cheerio.load(html) - - const items = $('.tv-box > ul > li').toArray() - return items - .map(item => { - const name = $(item).text().trim() - const link = $(item).find('a').attr('href') - - if (!link) return null - - const [, site_id] = link.match(/\/channel\/(.*)\//) || [null, null] - - return { - lang: 'mn', - site_id, - name - } - }) - .filter(Boolean) - } -} - -function parseTitle($item) { - return $item('.program').text().trim() -} - -function parseStart($item, date) { - const time = $item('.time') - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Ulaanbaatar') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('body > div > div > div > ul > li').toArray() -} +const dayjs = require('dayjs') +const axios = require('axios') +const cheerio = require('cheerio') +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: 'zuragt.mn', + days: 2, + url: function ({ channel, date }) { + return `https://m.zuragt.mn/channel/${channel.site_id}/?date=${date.format('YYYY-MM-DD')}` + }, + request: { + maxRedirects: 0, + validateStatus: function (status) { + return status >= 200 && status < 303 + } + }, + parser: async function ({ content, date }) { + let programs = [] + const items = parseItems(content) + for (let item of items) { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + start, + stop + }) + } + + return programs + }, + async channels() { + let html = await axios + .get('https://www.zuragt.mn/') + .then(r => r.data) + .catch(console.log) + let $ = cheerio.load(html) + + const items = $('.tv-box > ul > li').toArray() + return items + .map(item => { + const name = $(item).text().trim() + const link = $(item).find('a').attr('href') + + if (!link) return null + + const [, site_id] = link.match(/\/channel\/(.*)\//) || [null, null] + + return { + lang: 'mn', + site_id, + name + } + }) + .filter(Boolean) + } +} + +function parseTitle($item) { + return $item('.program').text().trim() +} + +function parseStart($item, date) { + const time = $item('.time') + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Ulaanbaatar') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('body > div > div > div > ul > li').toArray() +} diff --git a/sites/zuragt.mn/zuragt.mn.test.js b/sites/zuragt.mn/zuragt.mn.test.js index a7f9f693..e1d471ea 100644 --- a/sites/zuragt.mn/zuragt.mn.test.js +++ b/sites/zuragt.mn/zuragt.mn.test.js @@ -1,46 +1,46 @@ -const { parser, url, request } = require('./zuragt.mn.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) - -jest.mock('axios') - -const date = dayjs.utc('2023-01-15', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'mnb', - xmltv_id: 'MNB.mn' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://m.zuragt.mn/channel/mnb/?date=2023-01-15') -}) - -it('can generate valid request object', () => { - expect(request.maxRedirects).toBe(0) - expect(request.validateStatus(302)).toBe(true) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - let results = await parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-14T23:00:00.000Z', - stop: '2023-01-15T00:00:00.000Z', - title: '“Цагийн хүрд” мэдээллийн хөтөлбөр' - }) -}) - -it('can handle empty guide', async () => { - const result = await parser({ content: '' }) - expect(result).toMatchObject([]) -}) +const { parser, url, request } = require('./zuragt.mn.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) + +jest.mock('axios') + +const date = dayjs.utc('2023-01-15', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'mnb', + xmltv_id: 'MNB.mn' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://m.zuragt.mn/channel/mnb/?date=2023-01-15') +}) + +it('can generate valid request object', () => { + expect(request.maxRedirects).toBe(0) + expect(request.validateStatus(302)).toBe(true) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + let results = await parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-14T23:00:00.000Z', + stop: '2023-01-15T00:00:00.000Z', + title: '“Цагийн хүрд” мэдээллийн хөтөлбөр' + }) +}) + +it('can handle empty guide', async () => { + const result = await parser({ content: '' }) + expect(result).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__/expected/sites_init/example.com.config.js b/tests/__data__/expected/sites_init/example.com.config.js index 472b22e9..b4178aa6 100644 --- a/tests/__data__/expected/sites_init/example.com.config.js +++ b/tests/__data__/expected/sites_init/example.com.config.js @@ -1,16 +1,16 @@ -module.exports = { - site: 'example.com', - url({ channel, date }) { - return `https://example.com/api/${channel.site_id}/${date.format('YYYY-MM-DD')}` - }, - parser({ content }) { - try { - return JSON.parse(content) - } catch { - return [] - } - }, - channels() { - return [] - } -} +module.exports = { + site: 'example.com', + url({ channel, date }) { + return `https://example.com/api/${channel.site_id}/${date.format('YYYY-MM-DD')}` + }, + parser({ content }) { + try { + return JSON.parse(content) + } catch { + return [] + } + }, + channels() { + return [] + } +} diff --git a/tests/__data__/expected/sites_init/example.com.test.js b/tests/__data__/expected/sites_init/example.com.test.js index f462b6ef..b5162a00 100644 --- a/tests/__data__/expected/sites_init/example.com.test.js +++ b/tests/__data__/expected/sites_init/example.com.test.js @@ -1,38 +1,38 @@ -const { parser, url } = require('./example.com.config.js') -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-12', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'bbc1' } - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://example.com/api/bbc1/2025-01-12') -}) - -it('can parse response', () => { - const content = - '[{"title":"Program 1","start":"2025-01-12T00:00:00.000Z","stop":"2025-01-12T00:30:00.000Z"},{"title":"Program 2","start":"2025-01-12T00:30:00.000Z","stop":"2025-01-12T01:00:00.000Z"}]' - - const results = parser({ content }) - - expect(results.length).toBe(2) - expect(results[0]).toMatchObject({ - title: 'Program 1', - start: '2025-01-12T00:00:00.000Z', - stop: '2025-01-12T00:30:00.000Z' - }) - expect(results[1]).toMatchObject({ - title: 'Program 2', - start: '2025-01-12T00:30:00.000Z', - stop: '2025-01-12T01:00:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ content: '' }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./example.com.config.js') +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-12', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'bbc1' } + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://example.com/api/bbc1/2025-01-12') +}) + +it('can parse response', () => { + const content = + '[{"title":"Program 1","start":"2025-01-12T00:00:00.000Z","stop":"2025-01-12T00:30:00.000Z"},{"title":"Program 2","start":"2025-01-12T00:30:00.000Z","stop":"2025-01-12T01:00:00.000Z"}]' + + const results = parser({ content }) + + expect(results.length).toBe(2) + expect(results[0]).toMatchObject({ + title: 'Program 1', + start: '2025-01-12T00:00:00.000Z', + stop: '2025-01-12T00:30:00.000Z' + }) + expect(results[1]).toMatchObject({ + title: 'Program 2', + start: '2025-01-12T00:30:00.000Z', + stop: '2025-01-12T01:00:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ content: '' }) + + expect(results).toMatchObject([]) +}) 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/channels_parse/example.com.config.js b/tests/__data__/input/channels_parse/example.com.config.js index e1b5c368..4d87c89c 100644 --- a/tests/__data__/input/channels_parse/example.com.config.js +++ b/tests/__data__/input/channels_parse/example.com.config.js @@ -1,21 +1,21 @@ -module.exports = { - site: 'example.com', - url: 'https://example.com', - parser() { - return [] - }, - channels() { - return [ - { - lang: 'en', - site_id: 140, - name: 'CNN International' - }, - { - lang: 'en', - site_id: 240, - name: 'BBC World News' - } - ] - } -} +module.exports = { + site: 'example.com', + url: 'https://example.com', + parser() { + return [] + }, + channels() { + return [ + { + lang: 'en', + site_id: 140, + name: 'CNN International' + }, + { + lang: 'en', + site_id: 240, + name: 'BBC World News' + } + ] + } +} 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/channels/lint.test.ts b/tests/commands/channels/lint.test.ts index 61b9f53b..38b2ba6b 100644 --- a/tests/commands/channels/lint.test.ts +++ b/tests/commands/channels/lint.test.ts @@ -1,83 +1,83 @@ -import { execSync } from 'child_process' - -type ExecError = { - status: number - stdout: string -} - -describe('channels:lint', () => { - it('will show a message if the file contains a syntax error', () => { - try { - const cmd = 'npm run channels:lint --- tests/__data__/input/channels_lint/error.channels.xml' - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - process.exit(1) - } catch (error) { - expect((error as ExecError).status).toBe(1) - expect((error as ExecError).stdout).toContain( - "error.channels.xml\n 3:0 Element 'channel': The attribute 'lang' is required but missing.\n\n1 error(s)\n" - ) - } - }) - - it('will show a message if an error occurred while parsing an xml file', () => { - try { - const cmd = - 'npm run channels:lint --- tests/__data__/input/channels_lint/invalid.channels.xml' - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - process.exit(1) - } catch (error) { - expect((error as ExecError).status).toBe(1) - expect((error as ExecError).stdout).toContain( - 'invalid.channels.xml\n 2:6 XML declaration allowed only at the start of the document\n' - ) - } - }) - - it('can test multiple files at ones', () => { - try { - const cmd = - 'npm run channels:lint --- tests/__data__/input/channels_lint/error.channels.xml tests/__data__/input/channels_lint/invalid.channels.xml' - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - process.exit(1) - } catch (error) { - expect((error as ExecError).status).toBe(1) - expect((error as ExecError).stdout).toContain( - "error.channels.xml\n 3:0 Element 'channel': The attribute 'lang' is required but missing.\n" - ) - expect((error as ExecError).stdout).toContain( - 'invalid.channels.xml\n 2:6 XML declaration allowed only at the start of the document\n' - ) - expect((error as ExecError).stdout).toContain('2 error(s)') - } - }) - - it('will show a message if the file contains single quotes', () => { - try { - const cmd = - 'npm run channels:lint --- tests/__data__/input/channels_lint/single_quotes.channels.xml' - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - process.exit(1) - } catch (error) { - expect((error as ExecError).status).toBe(1) - expect((error as ExecError).stdout).toContain('single_quotes.channels.xml') - expect((error as ExecError).stdout).toContain( - '1:14 Single quotes cannot be used in attributes' - ) - } - }) - - it('does not display errors if there are none', () => { - try { - const cmd = 'npm run channels:lint --- tests/__data__/input/channels_lint/valid.channels.xml' - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - } catch (error) { - if (process.env.DEBUG === 'true') console.log((error as ExecError).stdout) - process.exit(1) - } - }) -}) +import { execSync } from 'child_process' + +interface ExecError { + status: number + stdout: string +} + +describe('channels:lint', () => { + it('will show a message if the file contains a syntax error', () => { + try { + const cmd = 'npm run channels:lint --- tests/__data__/input/channels_lint/error.channels.xml' + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + process.exit(1) + } catch (error) { + expect((error as ExecError).status).toBe(1) + expect((error as ExecError).stdout).toContain( + "error.channels.xml\n 3:0 Element 'channel': The attribute 'lang' is required but missing.\n\n1 error(s)\n" + ) + } + }) + + it('will show a message if an error occurred while parsing an xml file', () => { + try { + const cmd = + 'npm run channels:lint --- tests/__data__/input/channels_lint/invalid.channels.xml' + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + process.exit(1) + } catch (error) { + expect((error as ExecError).status).toBe(1) + expect((error as ExecError).stdout).toContain( + 'invalid.channels.xml\n 2:6 XML declaration allowed only at the start of the document\n' + ) + } + }) + + it('can test multiple files at ones', () => { + try { + const cmd = + 'npm run channels:lint --- tests/__data__/input/channels_lint/error.channels.xml tests/__data__/input/channels_lint/invalid.channels.xml' + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + process.exit(1) + } catch (error) { + expect((error as ExecError).status).toBe(1) + expect((error as ExecError).stdout).toContain( + "error.channels.xml\n 3:0 Element 'channel': The attribute 'lang' is required but missing.\n" + ) + expect((error as ExecError).stdout).toContain( + 'invalid.channels.xml\n 2:6 XML declaration allowed only at the start of the document\n' + ) + expect((error as ExecError).stdout).toContain('2 error(s)') + } + }) + + it('will show a message if the file contains single quotes', () => { + try { + const cmd = + 'npm run channels:lint --- tests/__data__/input/channels_lint/single_quotes.channels.xml' + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + process.exit(1) + } catch (error) { + expect((error as ExecError).status).toBe(1) + expect((error as ExecError).stdout).toContain('single_quotes.channels.xml') + expect((error as ExecError).stdout).toContain( + '1:14 Single quotes cannot be used in attributes' + ) + } + }) + + it('does not display errors if there are none', () => { + try { + const cmd = 'npm run channels:lint --- tests/__data__/input/channels_lint/valid.channels.xml' + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + } catch (error) { + if (process.env.DEBUG === 'true') console.log((error as ExecError).stdout) + process.exit(1) + } + }) +}) diff --git a/tests/commands/channels/parse.test.ts b/tests/commands/channels/parse.test.ts index 3e67da0b..905296da 100644 --- a/tests/commands/channels/parse.test.ts +++ b/tests/commands/channels/parse.test.ts @@ -1,30 +1,30 @@ -import { execSync } from 'child_process' -import fs from 'fs-extra' -import { pathToFileURL } from 'node:url' - -beforeEach(() => { - fs.emptyDirSync('tests/__data__/output') - fs.copySync( - 'tests/__data__/input/channels_parse/example.com.channels.xml', - 'tests/__data__/output/example.com.channels.xml' - ) -}) - -describe('channels:parse', () => { - it('can parse channels', () => { - const cmd = - 'npm run channels:parse --- --config=tests/__data__/input/channels_parse/example.com.config.js --output=tests/__data__/output/example.com.channels.xml' - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - - expect(content('tests/__data__/output/example.com.channels.xml')).toEqual( - content('tests/__data__/expected/channels_parse/example.com.channels.xml') - ) - }) -}) - -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' + +beforeEach(() => { + fs.emptyDirSync('tests/__data__/output') + fs.copySync( + 'tests/__data__/input/channels_parse/example.com.channels.xml', + 'tests/__data__/output/example.com.channels.xml' + ) +}) + +describe('channels:parse', () => { + it('can parse channels', () => { + const cmd = + 'npm run channels:parse --- --config=tests/__data__/input/channels_parse/example.com.config.js --output=tests/__data__/output/example.com.channels.xml' + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + + expect(content('tests/__data__/output/example.com.channels.xml')).toEqual( + content('tests/__data__/expected/channels_parse/example.com.channels.xml') + ) + }) +}) + +function content(filepath: string) { + return fs.readFileSync(pathToFileURL(filepath), { + encoding: 'utf8' + }) +} diff --git a/tests/commands/channels/validate.test.ts b/tests/commands/channels/validate.test.ts index 79e6ce30..e3dc123a 100644 --- a/tests/commands/channels/validate.test.ts +++ b/tests/commands/channels/validate.test.ts @@ -1,58 +1,58 @@ -import { execSync } from 'child_process' - -type ExecError = { - status: number - stdout: string -} - -const ENV_VAR = 'cross-env DATA_DIR=tests/__data__/input/__data__' - -describe('channels:validate', () => { - it('will show a message if the file contains a duplicate', () => { - try { - const cmd = `${ENV_VAR} npm run channels:validate --- tests/__data__/input/channels_validate/duplicate.channels.xml` - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - process.exit(1) - } catch (error) { - expect((error as ExecError).status).toBe(1) - expect((error as ExecError).stdout).toContain(` -┌─────────┬─────────────┬──────┬─────────────────┬─────────┬─────────┐ -│ (index) │ type │ lang │ xmltv_id │ site_id │ name │ -├─────────┼─────────────┼──────┼─────────────────┼─────────┼─────────┤ -│ 0 │ 'duplicate' │ 'en' │ 'Bravo.us@East' │ '140' │ 'Bravo' │ -└─────────┴─────────────┴──────┴─────────────────┴─────────┴─────────┘ - -1 problems (1 errors, 0 warnings) in 1 file(s) -`) - } - }) - - it('will show a message if the file contains a channel with wrong channel id', () => { - const cmd = `${ENV_VAR} npm run channels:validate --- tests/__data__/input/channels_validate/wrong_channel_id.channels.xml` - const stdout = execSync(cmd, { encoding: 'utf8' }) - expect(stdout).toContain(` -┌─────────┬────────────────────┬──────┬────────────────────┬─────────┬─────────────────────┐ -│ (index) │ type │ lang │ xmltv_id │ site_id │ name │ -├─────────┼────────────────────┼──────┼────────────────────┼─────────┼─────────────────────┤ -│ 0 │ 'wrong_channel_id' │ 'en' │ 'CNNInternational' │ '140' │ 'CNN International' │ -└─────────┴────────────────────┴──────┴────────────────────┴─────────┴─────────────────────┘ - -1 problems (0 errors, 1 warnings) in 1 file(s) -`) - }) - - it('will show a message if the file contains a channel with wrong feed id', () => { - const cmd = `${ENV_VAR} npm run channels:validate --- tests/__data__/input/channels_validate/wrong_feed_id.channels.xml` - const stdout = execSync(cmd, { encoding: 'utf8' }) - expect(stdout).toContain(` -┌─────────┬─────────────────┬──────┬─────────────────┬─────────┬─────────┐ -│ (index) │ type │ lang │ xmltv_id │ site_id │ name │ -├─────────┼─────────────────┼──────┼─────────────────┼─────────┼─────────┤ -│ 0 │ 'wrong_feed_id' │ 'en' │ 'Bravo.us@West' │ '150' │ 'Bravo' │ -└─────────┴─────────────────┴──────┴─────────────────┴─────────┴─────────┘ - -1 problems (0 errors, 1 warnings) in 1 file(s) -`) - }) -}) +import { execSync } from 'child_process' + +interface ExecError { + status: number + stdout: string +} + +const ENV_VAR = 'cross-env DATA_DIR=tests/__data__/input/__data__' + +describe('channels:validate', () => { + it('will show a message if the file contains a duplicate', () => { + try { + const cmd = `${ENV_VAR} npm run channels:validate --- tests/__data__/input/channels_validate/duplicate.channels.xml` + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + process.exit(1) + } catch (error) { + expect((error as ExecError).status).toBe(1) + expect((error as ExecError).stdout).toContain(` +┌─────────┬─────────────┬──────┬─────────────────┬─────────┬─────────┐ +│ (index) │ type │ lang │ xmltv_id │ site_id │ name │ +├─────────┼─────────────┼──────┼─────────────────┼─────────┼─────────┤ +│ 0 │ 'duplicate' │ 'en' │ 'Bravo.us@East' │ '140' │ 'Bravo' │ +└─────────┴─────────────┴──────┴─────────────────┴─────────┴─────────┘ + +1 problems (1 errors, 0 warnings) in 1 file(s) +`) + } + }) + + it('will show a message if the file contains a channel with wrong channel id', () => { + const cmd = `${ENV_VAR} npm run channels:validate --- tests/__data__/input/channels_validate/wrong_channel_id.channels.xml` + const stdout = execSync(cmd, { encoding: 'utf8' }) + expect(stdout).toContain(` +┌─────────┬────────────────────┬──────┬────────────────────┬─────────┬─────────────────────┐ +│ (index) │ type │ lang │ xmltv_id │ site_id │ name │ +├─────────┼────────────────────┼──────┼────────────────────┼─────────┼─────────────────────┤ +│ 0 │ 'wrong_channel_id' │ 'en' │ 'CNNInternational' │ '140' │ 'CNN International' │ +└─────────┴────────────────────┴──────┴────────────────────┴─────────┴─────────────────────┘ + +1 problems (0 errors, 1 warnings) in 1 file(s) +`) + }) + + it('will show a message if the file contains a channel with wrong feed id', () => { + const cmd = `${ENV_VAR} npm run channels:validate --- tests/__data__/input/channels_validate/wrong_feed_id.channels.xml` + const stdout = execSync(cmd, { encoding: 'utf8' }) + expect(stdout).toContain(` +┌─────────┬─────────────────┬──────┬─────────────────┬─────────┬─────────┐ +│ (index) │ type │ lang │ xmltv_id │ site_id │ name │ +├─────────┼─────────────────┼──────┼─────────────────┼─────────┼─────────┤ +│ 0 │ 'wrong_feed_id' │ 'en' │ 'Bravo.us@West' │ '150' │ 'Bravo' │ +└─────────┴─────────────────┴──────┴─────────────────┴─────────┴─────────┘ + +1 problems (0 errors, 1 warnings) in 1 file(s) +`) + }) +}) 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) +}