Files
epg/sites/sky.com/sky.com.config.js
T

314 lines
8.6 KiB
JavaScript
Raw Normal View History

const axios = require('axios')
2025-09-28 17:55:05 +03:00
const dayjs = require('dayjs')
const doFetch = require('@ntlab/sfetch')
const debug = require('debug')('site:sky.com')
const fs = require('fs')
const path = require('path')
const utc = require('dayjs/plugin/utc')
2025-09-28 17:55:05 +03:00
dayjs.extend(utc)
2026-06-08 19:04:43 +07:00
doFetch.setCheckResult(false).setDebugger(debug)
2025-09-28 17:55:05 +03:00
const API_ENDPOINT = 'https://awk.epgsky.com/hawk/linear'
const CHANNELS_FILE = path.join(__dirname, 'sky.com.channels.xml')
const LANG_BY_TERRITORY = {
DE: 'de',
GB: 'en',
IT: 'it'
}
const MAX_SIDS_PER_REQUEST = 20
const GRAB_START_DATE = dayjs
.utc(process.env.CURR_DATE || new Date().toISOString())
.startOf('d')
const eventIds = new Set()
const scheduleRequests = new Map()
let scheduleBatches
2026-06-11 18:02:58 +07:00
2025-09-28 17:55:05 +03:00
module.exports = {
site: 'sky.com',
days: 2,
2026-06-08 19:04:43 +07:00
request: {
headers({ channel }) {
return getHeaders(parseSiteId(channel.site_id).territory)
},
2026-06-08 19:04:43 +07:00
cache: {
ttl: 24 * 60 * 60 * 1000, // 1 day
vary: ['X-SkyOTT-Territory']
2026-06-08 19:04:43 +07:00
}
},
2025-09-28 17:55:05 +03:00
url({ date, channel }) {
const { sid } = parseSiteId(channel.site_id)
const sids = getScheduleBatch(channel.site_id) || [sid]
return `${API_ENDPOINT}/schedule/${date.format('YYYYMMDD')}/${sids.join(',')}`
2025-09-28 17:55:05 +03:00
},
async parser({ content, channel, date, config }) {
2025-09-28 17:55:05 +03:00
const programs = []
const events = new Map()
const range = getGrabRange(config)
collectEvents(content, channel, events)
date = date.startOf('d')
if (date.isSame(range.lastDate, 'd')) {
try {
const response = await getSchedule(channel, date.add(1, 'd'))
collectEvents(response, channel, events)
} catch (error) {
debug('Unable to load the final Sky schedule batch: %s', error.message)
2026-06-08 19:04:43 +07:00
}
}
for (const event of events.values()) {
const start = dayjs.utc(event.st * 1000)
const stop = start.add(event.d, 's')
const eventId = getEventId(channel, event)
if (
start.isBefore(range.stop) &&
stop.isAfter(range.start) &&
!eventIds.has(eventId)
) {
eventIds.add(eventId)
const image = event.programmeuuid
? `https://images.metadata.sky.com/pd-image/${event.programmeuuid}/16-9/640`
: null
programs.push({
title: event.t,
description: event.sy,
season: event.seasonnumber,
episode: event.episodenumber,
start,
stop,
icon: image,
image
2026-06-08 19:04:43 +07:00
})
2025-09-28 17:55:05 +03:00
}
}
return programs
},
async channels() {
const territories = Object.entries(LANG_BY_TERRITORY)
const queue = territories.map(([territory, lang]) => ({
type: 'regions',
lang,
territory,
url: `${API_ENDPOINT}/regions`,
params: {
headers: getHeaders(territory)
2025-09-28 17:55:05 +03:00
}
}))
const channels = new Map()
const regions = new Set()
const failures = []
const states = new Map(
territories.map(([territory]) => [
territory,
{
channels: 0,
regionsLoaded: false,
serviceRequests: 0,
serviceResponses: 0
}
])
)
await doFetch(queue, (request, response) => {
const state = states.get(request.territory)
if (request.type === 'regions') {
if (!Array.isArray(response?.regions) || !response.regions.length) {
failures.push(`${request.territory}: ${request.url}`)
return
}
state.regionsLoaded = true
for (const region of response.regions) {
if (region?.bouquetId === undefined || region?.subBouquetId === undefined) {
failures.push(`${request.territory}: ${request.url} (invalid region)`)
continue
2025-09-28 17:55:05 +03:00
}
const regionId = `${region.bouquetId}/${region.subBouquetId}`
const key = `${request.territory}:${regionId}`
if (regions.has(key)) continue
regions.add(key)
state.serviceRequests++
queue.push({
type: 'services',
lang: request.lang,
territory: request.territory,
url: `${API_ENDPOINT}/services/${regionId}`,
params: {
headers: getHeaders(request.territory)
}
})
}
return
}
if (!Array.isArray(response?.services)) {
failures.push(`${request.territory}: ${request.url}`)
return
}
state.serviceResponses++
for (const service of response.services) {
if (service?.sid === undefined) {
failures.push(`${request.territory}: ${request.url} (invalid service)`)
continue
}
if (!service.t) continue
const siteId = `${request.territory}#${service.sid}`
if (!channels.has(siteId)) {
channels.set(siteId, {
lang: request.lang,
site_id: siteId,
name: service.t
})
state.channels++
2025-09-28 17:55:05 +03:00
}
}
})
const incomplete = [...states]
.filter(
([, state]) =>
!state.regionsLoaded ||
!state.serviceRequests ||
state.serviceResponses !== state.serviceRequests ||
!state.channels
)
.map(([territory]) => territory)
if (incomplete.length || failures.length) {
const reasons = [...new Set([...failures, ...incomplete])].join(', ')
throw new Error(
`Unable to load complete Sky channel list: ${reasons || 'no channels found'}`
)
}
return [...channels.values()]
}
}
function getHeaders(territory) {
return {
'X-SkyOTT-Territory': territory
}
}
function getEventId(channel, event) {
return `${channel.site_id}:${event.eid}`
}
function getGrabRange(config) {
const configuredDays = Number(config?.days)
const days = Number.isInteger(configuredDays) && configuredDays > 0
? configuredDays
: module.exports.days
const stop = GRAB_START_DATE.add(days, 'd')
return {
lastDate: stop.subtract(1, 'd'),
start: GRAB_START_DATE,
stop
}
}
function getSchedule(channel, date) {
const { territory } = parseSiteId(channel.site_id)
const url = module.exports.url({ channel, date })
const key = `${territory}:${url}`
if (!scheduleRequests.has(key)) {
const request = axios
.get(url, {
headers: getHeaders(territory)
})
.then(response => response.data)
.catch(error => {
scheduleRequests.delete(key)
throw error
})
scheduleRequests.set(key, request)
}
return scheduleRequests.get(key)
}
function getScheduleBatch(siteId) {
if (!scheduleBatches) scheduleBatches = loadScheduleBatches()
return scheduleBatches.get(siteId)
}
function loadScheduleBatches() {
const channelsXml = fs.readFileSync(CHANNELS_FILE, 'utf8')
const sidsByTerritory = new Map(
Object.keys(LANG_BY_TERRITORY).map(territory => [territory, new Set()])
)
for (const match of channelsXml.matchAll(/\bsite_id="([^"]+)"/g)) {
const { sid, territory } = parseSiteId(match[1])
sidsByTerritory.get(territory).add(sid)
}
const batches = new Map()
for (const [territory, sidSet] of sidsByTerritory) {
const sids = [...sidSet]
for (let index = 0; index < sids.length; index += MAX_SIDS_PER_REQUEST) {
const batch = sids.slice(index, index + MAX_SIDS_PER_REQUEST)
for (const sid of batch) {
batches.set(`${territory}#${sid}`, batch)
}
}
}
return batches
}
function collectEvents(content, channel, events) {
const data = parseContent(content)
if (!Array.isArray(data?.schedule)) return
const { sid } = parseSiteId(channel.site_id)
for (const schedule of data.schedule) {
if (String(schedule.sid) !== sid || !Array.isArray(schedule.events)) continue
for (const event of schedule.events) {
if (event?.eid !== undefined && !events.has(event.eid)) {
events.set(event.eid, event)
}
}
}
}
function parseSiteId(siteId) {
const [territory, sid, ...extra] = String(siteId || '').split('#')
if (!LANG_BY_TERRITORY[territory] || !sid || extra.length) {
throw new Error(
`Invalid Sky site_id "${siteId || ''}". Expected "<territory>#<sid>" with territory "DE", "GB" or "IT".`
)
}
return { sid, territory }
}
function parseContent(content) {
if (!content) return null
try {
if (Buffer.isBuffer(content)) content = content.toString()
return typeof content === 'string' ? JSON.parse(content) : content
} catch {
return null
2025-09-28 17:55:05 +03:00
}
}