Files
epg/sites/ontvtonight.com/ontvtonight.com.config.js
T
Toha dbe2999f0a Update ontvtonight.com guide Api and channels.
This site is protected with AWS WAF, so to be able to scrape the guide
WAF token must be provided in `aws-waf-token` cookie.

Currenlty the token is solved using https://github.com/tohenk/node-awswaf.

Signed-off-by: Toha <tohenk@yahoo.com>
2026-06-23 10:55:19 +07:00

258 lines
7.1 KiB
JavaScript

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')
const AwsWaf = require('@ntlab/awswaf')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
let awsWafToken, awsWafTokenFetching
const cookies = {}
const headers = {
'user-agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36',
}
module.exports = {
site: 'ontvtonight.com',
days: 2,
url({ date, channel }) {
return getSiteUrl(channel, date)
},
request: {
async headers({ date, channel }) {
await fetchCookiesOrToken(getSiteUrl(channel, date))
return getHeaders()
}
},
parser({ content, date, channel }) {
const programs = []
const [$, items] = parseItems(content)
items.forEach(item => {
const prev = programs[programs.length - 1]
const $item = $(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 channels = []
const sUrls = []
let $, providers, regions
const u = (p, r) => {
const res = []
for (const provider of p) {
for (const region of r) {
const url = getScheduleUrl(country, provider, region)
if (!sUrls.includes(url)) {
sUrls.push(url)
res.push(url)
}
}
}
return res
}
const v = items => {
return items
.toArray()
.map(item => $(item).attr('value'))
}
const urls = [`${getBaseUrl(country)}/guide/tvbycity.html`]
while (urls.length) {
const url = urls.shift()
await fetchCookiesOrToken(url)
const data = await axios.request({
url,
method: url.includes('tvbycity') ? 'get' : 'post',
headers: getHeaders()
})
.then(res => res.data)
.catch(() => {
// ignore error
})
if (data) {
$ = cheerio.load(data)
if (url.includes('tvbycity')) {
$('article ul li a')
.toArray()
.forEach(el => {
urls.push(`${getBaseUrl()}${$(el).attr('href')}`)
})
} else if (url.includes('listings')) {
console.log(`Listing ${url}...`)
providers = v($('#guide_provider option'))
if ($('#guide_region').parents('.stream-filter').hasClass('d-none')) {
regions = v($('#provider-zip'))
} else {
regions = v($('#guide_region option'))
}
const _urls = u(providers, regions)
if (_urls.length) {
urls.push(..._urls)
}
} else {
console.log(`${url} (${urls.length} to go)...`)
$('.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.find('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.find('td:nth-child(2) > h5').text().trim()
}
function parseDescription($item) {
return $item.find('td:nth-child(2) > h6').text().trim()
}
function parseItems(content) {
const $ = cheerio.load(content)
return [$, $('table > tbody > tr').toArray()]
}
function getHeaders() {
return {
...headers,
cookie: Object.entries(cookies)
.map(kv => `${kv[0]}=${kv[1]}`)
.join('; ')
}
}
function getBaseUrl(country) {
return `https://www.ontvtonight.com${country && country !== 'us' ? '/' + country : ''}`
}
function getSiteUrl(channel, date) {
const [country, id] = channel.site_id.split('#')
return `${getBaseUrl(country)}/guide/listings/channel/${id}.html?dt=${date.format('YYYY-MM-DD')}`
}
function getScheduleUrl(country, provider, region) {
return `${getBaseUrl(country)}/guide/schedule?provider=${
provider
}&region=${
region
}&TVperiod=Night&date=${dayjs().format('YYYY-MM-DD')}&st=0&u_time=0000&is_mobile=1`
}
async function fetchCookiesOrToken(url) {
const f = async (m = 'head') => {
let waf
await axios[m](url, { headers: getHeaders() })
.then(res => saveCookies(res))
.catch(err => {
if (err instanceof axios.AxiosError && err.status === 405) {
waf = err.response
}
})
if (waf) {
if (m === 'head') {
await f('get')
} else if (waf.data) {
console.log(`WAF triggered from ${url}, fetching token...`)
awsWafTokenFetching = true
try {
await fetchWafToken(waf.data)
} catch (err) {
console.error(err)
}
awsWafTokenFetching = false
}
}
}
await f()
if (awsWafTokenFetching) {
// delay other connection to finish if token is fetching
await new Promise(resolve => {
process.nextTick(() => {
if (!awsWafTokenFetching) {
resolve()
}
})
})
}
}
function saveCookies(res) {
if (res.headers && Array.isArray(res.headers['set-cookie'])) {
res.headers['set-cookie']
.map(cookie => cookie.split(';')[0].trim())
.forEach(cookie => {
const [k, v] = cookie.split('=')
cookies[k] = v
})
}
return res
}
async function fetchWafToken(content) {
const $ = cheerio.load(content)
const challengeJs = $('script[src]')
.toArray()
.map(s => $(s).attr('src'))
.filter(s => s.includes('challenge.js'))
if (challengeJs.length) {
const [, endpoint] = challengeJs[0].match(/(https:\/\/[^"]+)\/challenge\.js/)
const waf = new AwsWaf(endpoint, 'www.ontvtonight.com', headers['user-agent'])
awsWafToken = await waf.getToken()
if (awsWafToken) {
console.log(`Got WAF token ${awsWafToken}...`)
cookies['aws-waf-token'] = awsWafToken
}
}
}