mirror of
https://github.com/iptv-org/epg.git
synced 2026-08-03 10:19:51 +02:00
feat: added sky.com for different europe countries
This commit is contained in:
3
SITES.md
3
SITES.md
@@ -158,8 +158,7 @@
|
||||
<tr><td><a href="sites/singtel.com">singtel.com</a></td><td align="right">137</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/sjonvarp.is">sjonvarp.is</a></td><td align="right">13</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/sky.co.nz">sky.co.nz</a></td><td align="right">91</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/sky.com">sky.com</a></td><td align="right">521</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/sky.de">sky.de</a></td><td align="right">74</td><td align="center">🟢</td><td>https://github.com/iptv-org/epg/issues/3200</td></tr>
|
||||
<tr><td><a href="sites/sky.com">sky.com</a></td><td align="right">1116</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/skylife.co.kr">skylife.co.kr</a></td><td align="right">257</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/skyperfectv.co.jp">skyperfectv.co.jp</a></td><td align="right">196</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/snrt.ma">snrt.ma</a></td><td align="right">11</td><td align="center">🟢</td><td></td></tr>
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
{
|
||||
"regions": [
|
||||
{ "text": "Atherstone HD", "bouquet": 4101, "subBouquet": 19, "value": "4101-19" },
|
||||
{ "text": "Atherstone SD", "bouquet": 4097, "subBouquet": 19, "value": "4097-19" },
|
||||
{ "text": "Border England HD", "bouquet": 4101, "subBouquet": 12, "value": "4101-12" },
|
||||
{ "text": "Border England SD", "bouquet": 4097, "subBouquet": 12, "value": "4097-12" },
|
||||
{ "text": "Border Scotland HD", "bouquet": 4102, "subBouquet": 36, "value": "4102-36" },
|
||||
{ "text": "Border Scotland SD", "bouquet": 4098, "subBouquet": 36, "value": "4098-36" },
|
||||
{ "text": "Brighton HD", "bouquet": 4103, "subBouquet": 65, "value": "4103-65" },
|
||||
{ "text": "Brighton SD", "bouquet": 4099, "subBouquet": 65, "value": "4099-65" },
|
||||
{ "text": "Central Midlands HD", "bouquet": 4101, "subBouquet": 3, "value": "4101-3" },
|
||||
{ "text": "Central Midlands SD", "bouquet": 4097, "subBouquet": 3, "value": "4097-3" },
|
||||
{ "text": "Channel Isles HD", "bouquet": 4104, "subBouquet": 34, "value": "4104-34" },
|
||||
{ "text": "Channel Isles SD", "bouquet": 4100, "subBouquet": 34, "value": "4100-34" },
|
||||
{ "text": "Dundee HD", "bouquet": 4102, "subBouquet": 39, "value": "4102-39" },
|
||||
{ "text": "Dundee SD", "bouquet": 4098, "subBouquet": 39, "value": "4098-39" },
|
||||
{ "text": "East Midlands HD", "bouquet": 4101, "subBouquet": 20, "value": "4101-20" },
|
||||
{ "text": "East Midlands SD", "bouquet": 4097, "subBouquet": 20, "value": "4097-20" },
|
||||
{ "text": "Essex HD", "bouquet": 4101, "subBouquet": 2, "value": "4101-2" },
|
||||
{ "text": "Essex SD", "bouquet": 4097, "subBouquet": 2, "value": "4097-2" },
|
||||
{ "text": "Gloucester HD", "bouquet": 4101, "subBouquet": 24, "value": "4101-24" },
|
||||
{ "text": "Gloucester SD", "bouquet": 4097, "subBouquet": 24, "value": "4097-24" },
|
||||
{ "text": "Grampian HD", "bouquet": 4102, "subBouquet": 35, "value": "4102-35" },
|
||||
{ "text": "Grampian SD", "bouquet": 4098, "subBouquet": 35, "value": "4098-35" },
|
||||
{ "text": "Granada HD", "bouquet": 4101, "subBouquet": 7, "value": "4101-7" },
|
||||
{ "text": "Granada SD", "bouquet": 4097, "subBouquet": 7, "value": "4097-7" },
|
||||
{ "text": "HTV West HD", "bouquet": 4101, "subBouquet": 4, "value": "4101-4" },
|
||||
{ "text": "HTV West SD", "bouquet": 4097, "subBouquet": 4, "value": "4097-4" },
|
||||
{ "text": "HTV West / Thames Valley HD", "bouquet": 4103, "subBouquet": 63, "value": "4103-63" },
|
||||
{ "text": "HTV West / Thames Valley SD", "bouquet": 4099, "subBouquet": 63, "value": "4099-63" },
|
||||
{ "text": "Henley On Thames HD", "bouquet": 4103, "subBouquet": 70, "value": "4103-70" },
|
||||
{ "text": "Henley On Thames SD", "bouquet": 4099, "subBouquet": 70, "value": "4099-70" },
|
||||
{ "text": "Humber HD", "bouquet": 4101, "subBouquet": 29, "value": "4101-29" },
|
||||
{ "text": "Humber SD", "bouquet": 4097, "subBouquet": 29, "value": "4097-29" },
|
||||
{ "text": "London HD", "bouquet": 4101, "subBouquet": 1, "value": "4101-1" },
|
||||
{ "text": "London SD", "bouquet": 4097, "subBouquet": 1, "value": "4097-1" },
|
||||
{ "text": "London / Essex HD", "bouquet": 4101, "subBouquet": 18, "value": "4101-18" },
|
||||
{ "text": "London / Essex SD", "bouquet": 4097, "subBouquet": 18, "value": "4097-18" },
|
||||
{ "text": "London / Thames Valley HD", "bouquet": 4103, "subBouquet": 66, "value": "4103-66" },
|
||||
{ "text": "London / Thames Valley SD", "bouquet": 4099, "subBouquet": 66, "value": "4099-66" },
|
||||
{ "text": "London Kent HD", "bouquet": 4103, "subBouquet": 64, "value": "4103-64" },
|
||||
{ "text": "London Kent SD", "bouquet": 4099, "subBouquet": 64, "value": "4099-64" },
|
||||
{ "text": "Meridian East HD", "bouquet": 4101, "subBouquet": 11, "value": "4101-11" },
|
||||
{ "text": "Meridian East SD", "bouquet": 4097, "subBouquet": 11, "value": "4097-11" },
|
||||
{ "text": "Meridian North HD", "bouquet": 4103, "subBouquet": 68, "value": "4103-68" },
|
||||
{ "text": "Meridian North SD", "bouquet": 4099, "subBouquet": 68, "value": "4099-68" },
|
||||
{ "text": "Meridian South HD", "bouquet": 4101, "subBouquet": 5, "value": "4101-5" },
|
||||
{ "text": "Meridian South SD", "bouquet": 4097, "subBouquet": 5, "value": "4097-5" },
|
||||
{ "text": "Meridian South East HD", "bouquet": 4101, "subBouquet": 10, "value": "4101-10" },
|
||||
{ "text": "Meridian South East SD", "bouquet": 4097, "subBouquet": 10, "value": "4097-10" },
|
||||
{ "text": "Merseyside HD", "bouquet": 4103, "subBouquet": 45, "value": "4103-45" },
|
||||
{ "text": "Merseyside SD", "bouquet": 4099, "subBouquet": 45, "value": "4099-45" },
|
||||
{ "text": "Norfolk HD", "bouquet": 4101, "subBouquet": 21, "value": "4101-21" },
|
||||
{ "text": "Norfolk SD", "bouquet": 4097, "subBouquet": 21, "value": "4097-21" },
|
||||
{ "text": "North East Midlands HD", "bouquet": 4103, "subBouquet": 62, "value": "4103-62" },
|
||||
{ "text": "North East Midlands SD", "bouquet": 4099, "subBouquet": 62, "value": "4099-62" },
|
||||
{ "text": "North West Yorkshire HD", "bouquet": 4101, "subBouquet": 8, "value": "4101-8" },
|
||||
{ "text": "North West Yorkshire SD", "bouquet": 4097, "subBouquet": 8, "value": "4097-8" },
|
||||
{ "text": "North Yorkshire HD", "bouquet": 4101, "subBouquet": 26, "value": "4101-26" },
|
||||
{ "text": "North Yorkshire SD", "bouquet": 4097, "subBouquet": 26, "value": "4097-26" },
|
||||
{ "text": "Northern Ireland HD", "bouquet": 4104, "subBouquet": 33, "value": "4104-33" },
|
||||
{ "text": "Northern Ireland SD", "bouquet": 4100, "subBouquet": 33, "value": "4100-33" },
|
||||
{ "text": "Oxford HD", "bouquet": 4103, "subBouquet": 71, "value": "4103-71" },
|
||||
{ "text": "Oxford SD", "bouquet": 4099, "subBouquet": 71, "value": "4099-71" },
|
||||
{ "text": "Republic of Ireland HD", "bouquet": 4104, "subBouquet": 50, "value": "4104-50" },
|
||||
{ "text": "Republic of Ireland SD", "bouquet": 4100, "subBouquet": 50, "value": "4100-50" },
|
||||
{ "text": "Ridge Hill HD", "bouquet": 4103, "subBouquet": 41, "value": "4103-41" },
|
||||
{ "text": "Ridge Hill SD", "bouquet": 4099, "subBouquet": 41, "value": "4099-41" },
|
||||
{ "text": "Scarborough HD", "bouquet": 4103, "subBouquet": 61, "value": "4103-61" },
|
||||
{ "text": "Scarborough SD", "bouquet": 4099, "subBouquet": 61, "value": "4099-61" },
|
||||
{ "text": "Scottish East HD", "bouquet": 4102, "subBouquet": 38, "value": "4102-38" },
|
||||
{ "text": "Scottish East SD", "bouquet": 4098, "subBouquet": 38, "value": "4098-38" },
|
||||
{ "text": "Scottish West HD", "bouquet": 4102, "subBouquet": 37, "value": "4102-37" },
|
||||
{ "text": "Scottish West SD", "bouquet": 4098, "subBouquet": 37, "value": "4098-37" },
|
||||
{ "text": "Sheffield HD", "bouquet": 4103, "subBouquet": 60, "value": "4103-60" },
|
||||
{ "text": "Sheffield SD", "bouquet": 4099, "subBouquet": 60, "value": "4099-60" },
|
||||
{ "text": "South Lakeland HD", "bouquet": 4101, "subBouquet": 28, "value": "4101-28" },
|
||||
{ "text": "South Lakeland SD", "bouquet": 4097, "subBouquet": 28, "value": "4097-28" },
|
||||
{ "text": "South Yorkshire HD", "bouquet": 4103, "subBouquet": 72, "value": "4103-72" },
|
||||
{ "text": "South Yorkshire SD", "bouquet": 4099, "subBouquet": 72, "value": "4099-72" },
|
||||
{ "text": "Tees HD", "bouquet": 4103, "subBouquet": 69, "value": "4103-69" },
|
||||
{ "text": "Tees SD", "bouquet": 4099, "subBouquet": 69, "value": "4099-69" },
|
||||
{ "text": "Thames Valley HD", "bouquet": 4101, "subBouquet": 9, "value": "4101-9" },
|
||||
{ "text": "Thames Valley SD", "bouquet": 4097, "subBouquet": 9, "value": "4097-9" },
|
||||
{ "text": "Tring HD", "bouquet": 4101, "subBouquet": 27, "value": "4101-27" },
|
||||
{ "text": "Tring SD", "bouquet": 4097, "subBouquet": 27, "value": "4097-27" },
|
||||
{ "text": "Tyne HD", "bouquet": 4101, "subBouquet": 13, "value": "4101-13" },
|
||||
{ "text": "Tyne SD", "bouquet": 4097, "subBouquet": 13, "value": "4097-13" },
|
||||
{ "text": "Wales HD", "bouquet": 4104, "subBouquet": 32, "value": "4104-32" },
|
||||
{ "text": "Wales SD", "bouquet": 4100, "subBouquet": 32, "value": "4100-32" },
|
||||
{ "text": "West Anglia HD", "bouquet": 4101, "subBouquet": 25, "value": "4101-25" },
|
||||
{ "text": "West Anglia SD", "bouquet": 4097, "subBouquet": 25, "value": "4097-25" },
|
||||
{ "text": "West Dorset HD", "bouquet": 4103, "subBouquet": 67, "value": "4103-67" },
|
||||
{ "text": "West Dorset SD", "bouquet": 4099, "subBouquet": 67, "value": "4099-67" },
|
||||
{ "text": "Westcountry HD", "bouquet": 4101, "subBouquet": 6, "value": "4101-6" },
|
||||
{ "text": "Westcountry SD", "bouquet": 4097, "subBouquet": 6, "value": "4097-6" }
|
||||
]
|
||||
}
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
https://www.sky.com/tv-guide
|
||||
|
||||
All Sky countries are covered: Germany, Austria and Switzerland (`DE`), the United Kingdom and Ireland (`GB`), and Italy (`IT`).
|
||||
|
||||
Channel IDs use the format `<territory>#<sid>` (`GB#...`, `IT#...` or `DE#...`).
|
||||
|
||||
### Download the guide
|
||||
|
||||
```sh
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,137 +1,313 @@
|
||||
const axios = require('axios')
|
||||
const dayjs = require('dayjs')
|
||||
const utc = require('dayjs/plugin/utc')
|
||||
const doFetch = require('@ntlab/sfetch')
|
||||
const debug = require('debug')('site:sky.com')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const fs = require('fs/promises')
|
||||
const utc = require('dayjs/plugin/utc')
|
||||
|
||||
dayjs.extend(utc)
|
||||
|
||||
doFetch.setCheckResult(false).setDebugger(debug)
|
||||
|
||||
const eventIds = []
|
||||
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
|
||||
|
||||
module.exports = {
|
||||
site: 'sky.com',
|
||||
days: 2,
|
||||
request: {
|
||||
headers({ channel }) {
|
||||
return getHeaders(parseSiteId(channel.site_id).territory)
|
||||
},
|
||||
cache: {
|
||||
ttl: 24 * 60 * 60 * 1000 // 1 day
|
||||
ttl: 24 * 60 * 60 * 1000, // 1 day
|
||||
vary: ['X-SkyOTT-Territory']
|
||||
}
|
||||
},
|
||||
url({ date, channel }) {
|
||||
return `https://awk.epgsky.com/hawk/linear/schedule/${date.format('YYYYMMDD')}/${
|
||||
channel.site_id
|
||||
}`
|
||||
const { sid } = parseSiteId(channel.site_id)
|
||||
const sids = getScheduleBatch(channel.site_id) || [sid]
|
||||
|
||||
return `${API_ENDPOINT}/schedule/${date.format('YYYYMMDD')}/${sids.join(',')}`
|
||||
},
|
||||
async parser({ content, channel, date }) {
|
||||
async parser({ content, channel, date, config }) {
|
||||
const programs = []
|
||||
if (content) {
|
||||
const events = {}
|
||||
const ev = items => {
|
||||
if (typeof items === 'string' || Buffer.isBuffer(items)) {
|
||||
items = JSON.parse(items)
|
||||
}
|
||||
if (Array.isArray(items?.schedule)) {
|
||||
items.schedule
|
||||
.filter(schedule => schedule.sid === channel.site_id)
|
||||
.forEach(schedule => {
|
||||
if (Array.isArray(schedule.events)) {
|
||||
schedule.events
|
||||
.filter(event => !eventIds.includes(event.eid))
|
||||
.forEach(event => {
|
||||
// use event id (eid) as unique filter
|
||||
if (events[event.eid] === undefined) {
|
||||
events[event.eid] = event
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
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)
|
||||
}
|
||||
ev(content)
|
||||
if (Object.keys(events).length) {
|
||||
date = date.startOf('d')
|
||||
// fetch next day schedule to get 24 hours schedule
|
||||
await doFetch([module.exports.url({ channel, date: date.add(1, 'd') })], (url, res) => {
|
||||
if (res) {
|
||||
ev(res)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
Object.values(events)
|
||||
.forEach(event => {
|
||||
const start = dayjs.utc(event.st * 1000)
|
||||
const stop = start.add(event.d, 's')
|
||||
if (date.isSame(start, 'd') || (date.isSame(stop, 'd') && stop > date)) {
|
||||
eventIds.push(event.eid)
|
||||
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,
|
||||
icon: image,
|
||||
image
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return programs
|
||||
},
|
||||
async channels() {
|
||||
const dataPath = path.join(__dirname, '__data__', 'regions.json')
|
||||
const regions = []
|
||||
|
||||
try {
|
||||
const raw = await fs.readFile(dataPath, 'utf8')
|
||||
const payload = JSON.parse(raw)
|
||||
if (Array.isArray(payload.regions)) {
|
||||
regions.push(...payload.regions)
|
||||
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)
|
||||
}
|
||||
} catch (err) {
|
||||
debug('Failed to read regions from %s: %o', dataPath, err)
|
||||
throw err
|
||||
}
|
||||
|
||||
if (regions.length === 0) {
|
||||
debug('No regions defined in %s', dataPath)
|
||||
return []
|
||||
}
|
||||
|
||||
const uniqueRegions = new Map()
|
||||
regions.forEach(region => {
|
||||
if (!region || region.bouquet === undefined || region.subBouquet === undefined) return
|
||||
const key = `${region.bouquet}-${region.subBouquet}`
|
||||
if (!uniqueRegions.has(key)) uniqueRegions.set(key, region)
|
||||
})
|
||||
|
||||
const channels = {}
|
||||
const queues = Array.from(uniqueRegions.values()).map(region => ({
|
||||
t: 'c',
|
||||
url: `https://awk.epgsky.com/hawk/linear/services/${region.bouquet}/${region.subBouquet}`
|
||||
}))
|
||||
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(queues, (queue, res) => {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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++
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return Object.values(channels)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
const { parser, url } = require('./sky.com.config.js')
|
||||
process.env.CURR_DATE = '2026-06-08'
|
||||
|
||||
const { channels, parser, request, url } = require('./sky.com.config.js')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const axios = require('axios')
|
||||
@@ -13,29 +15,73 @@ jest.mock('axios')
|
||||
|
||||
const date = dayjs.utc('2026-06-08').startOf('d')
|
||||
const channel = {
|
||||
site_id: '4086',
|
||||
lang: 'en',
|
||||
site_id: 'GB#4086',
|
||||
xmltv_id: 'SkyHistory.uk@HD'
|
||||
}
|
||||
|
||||
axios.get.mockImplementation(url => {
|
||||
function mockScheduleRequest(url) {
|
||||
const urls = {
|
||||
'https://awk.epgsky.com/hawk/linear/schedule/20260608/4086': 'content1.json',
|
||||
'https://awk.epgsky.com/hawk/linear/schedule/20260609/4086': 'content2.json'
|
||||
'20260608': 'content1.json',
|
||||
'20260609': 'content2.json'
|
||||
}
|
||||
let data = ''
|
||||
if (urls[url] !== undefined) {
|
||||
data = fs.readFileSync(path.join(__dirname, '__data__', urls[url])).toString()
|
||||
const match = url.match(/\/schedule\/(\d{8})\/([^/]+)$/)
|
||||
if (match && match[2].split(',').includes('4086') && urls[match[1]] !== undefined) {
|
||||
data = fs.readFileSync(path.join(__dirname, '__data__', urls[match[1]])).toString()
|
||||
}
|
||||
return Promise.resolve({ data })
|
||||
return Promise.resolve({ data, headers: {}, request: {} })
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
axios.get.mockImplementation(mockScheduleRequest)
|
||||
})
|
||||
|
||||
it('can generate valid url', () => {
|
||||
expect(url({ channel, date })).toBe('https://awk.epgsky.com/hawk/linear/schedule/20260608/4086')
|
||||
const result = url({ channel, date })
|
||||
const sids = result.split('/').pop().split(',')
|
||||
|
||||
expect(result).toContain('https://awk.epgsky.com/hawk/linear/schedule/20260608/')
|
||||
expect(sids).toHaveLength(20)
|
||||
expect(sids).toContain('4086')
|
||||
expect(new Set(sids)).toHaveProperty('size', 20)
|
||||
|
||||
const companion = {
|
||||
...channel,
|
||||
site_id: `GB#${sids.find(sid => sid !== '4086')}`
|
||||
}
|
||||
expect(url({ channel: companion, date })).toBe(result)
|
||||
})
|
||||
|
||||
it('falls back to a single SID for a channel outside the repository XML', () => {
|
||||
expect(url({ channel: { ...channel, site_id: 'GB#999999' }, date })).toBe(
|
||||
'https://awk.epgsky.com/hawk/linear/schedule/20260608/999999'
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects an unsupported territory in the site ID', () => {
|
||||
expect(() => url({ channel: { ...channel, site_id: 'XX#4086' }, date })).toThrow(
|
||||
'Expected "<territory>#<sid>"'
|
||||
)
|
||||
})
|
||||
|
||||
it('can generate territory headers from the site ID', () => {
|
||||
expect(request.headers({ channel })).toEqual({
|
||||
'X-SkyOTT-Territory': 'GB'
|
||||
})
|
||||
expect(request.headers({ channel: { ...channel, lang: 'en', site_id: 'DE#4086' } })).toEqual({
|
||||
'X-SkyOTT-Territory': 'DE'
|
||||
})
|
||||
expect(request.headers({ channel: { ...channel, lang: 'it', site_id: 'IT#4086' } })).toEqual({
|
||||
'X-SkyOTT-Territory': 'IT'
|
||||
})
|
||||
expect(request.cache.vary).toEqual(['X-SkyOTT-Territory'])
|
||||
})
|
||||
|
||||
it('can parse response', async () => {
|
||||
const content = fs.readFileSync(path.join(__dirname, '__data__', 'content1.json'))
|
||||
const result = (await parser({ content, channel, date }))
|
||||
const result = (await parser({ config: { days: 1 }, content, channel, date }))
|
||||
.map(p => {
|
||||
p.start = p.start.toJSON()
|
||||
p.stop = p.stop.toJSON()
|
||||
@@ -65,13 +111,196 @@ it('can parse response', async () => {
|
||||
icon: 'https://images.metadata.sky.com/pd-image/68152ae7-97d6-44c8-8a54-e78710b94a76/16-9/640',
|
||||
image: 'https://images.metadata.sky.com/pd-image/68152ae7-97d6-44c8-8a54-e78710b94a76/16-9/640'
|
||||
})
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
url({ channel, date: date.add(1, 'd') }),
|
||||
{
|
||||
headers: {
|
||||
'X-SkyOTT-Territory': 'GB'
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('only loads a follow-up batch for the final requested date', async () => {
|
||||
await parser({
|
||||
config: { days: 2 },
|
||||
content: '',
|
||||
channel,
|
||||
date
|
||||
})
|
||||
expect(axios.get).not.toHaveBeenCalled()
|
||||
|
||||
await parser({
|
||||
config: { days: 2 },
|
||||
content: '',
|
||||
channel,
|
||||
date: date.add(1, 'd')
|
||||
})
|
||||
expect(axios.get).toHaveBeenCalledTimes(1)
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
url({ channel, date: date.add(2, 'd') }),
|
||||
{
|
||||
headers: {
|
||||
'X-SkyOTT-Territory': 'GB'
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('loads a shared follow-up batch only once', async () => {
|
||||
const sids = url({ channel, date }).split('/').pop().split(',')
|
||||
const companion = {
|
||||
...channel,
|
||||
site_id: `GB#${sids.find(sid => sid !== '4086')}`
|
||||
}
|
||||
const finalDate = date.add(2, 'd')
|
||||
|
||||
await Promise.all([
|
||||
parser({ config: { days: 3 }, content: '', channel, date: finalDate }),
|
||||
parser({ config: { days: 3 }, content: '', channel: companion, date: finalDate })
|
||||
])
|
||||
|
||||
expect(axios.get).toHaveBeenCalledTimes(1)
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
url({ channel, date: finalDate.add(1, 'd') }),
|
||||
{
|
||||
headers: {
|
||||
'X-SkyOTT-Territory': 'GB'
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('can handle empty guide', async () => {
|
||||
const result = await parser({
|
||||
config: { days: 2 },
|
||||
date,
|
||||
channel,
|
||||
content: ''
|
||||
})
|
||||
expect(result).toMatchObject([])
|
||||
})
|
||||
|
||||
it('keeps identical event IDs isolated by territory', async () => {
|
||||
const content = JSON.stringify({
|
||||
schedule: [
|
||||
{
|
||||
sid: '123',
|
||||
events: [
|
||||
{
|
||||
eid: 'shared-event',
|
||||
st: date.unix(),
|
||||
d: 3600,
|
||||
t: 'Shared event'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
const englishChannel = { lang: 'en', site_id: 'GB#123' }
|
||||
const germanChannel = { lang: 'de', site_id: 'DE#123' }
|
||||
|
||||
expect(await parser({ content, channel: englishChannel, date })).toHaveLength(1)
|
||||
expect(await parser({ content, channel: germanChannel, date })).toHaveLength(1)
|
||||
expect(await parser({ content, channel: englishChannel, date })).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('can load and deduplicate all territories by language and site ID', async () => {
|
||||
axios.get.mockImplementation((url, options) => {
|
||||
const territory = options.headers['X-SkyOTT-Territory']
|
||||
const titles = {
|
||||
DE: 'Sky One Deutschland HD',
|
||||
GB: 'Sky One HD',
|
||||
IT: 'Sky Uno HD'
|
||||
}
|
||||
|
||||
if (url.endsWith('/regions')) {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
regions: [
|
||||
{ bouquetId: 1, subBouquetId: 2 },
|
||||
{ bouquetId: 1, subBouquetId: 2 }
|
||||
]
|
||||
},
|
||||
headers: {},
|
||||
request: {}
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
services: [
|
||||
{ sid: '123', t: titles[territory] },
|
||||
{ sid: '123', t: titles[territory] },
|
||||
...(territory === 'IT' ? [{ sid: '582' }] : [])
|
||||
]
|
||||
},
|
||||
headers: {},
|
||||
request: {}
|
||||
})
|
||||
})
|
||||
|
||||
await expect(channels()).resolves.toEqual([
|
||||
{
|
||||
lang: 'de',
|
||||
site_id: 'DE#123',
|
||||
name: 'Sky One Deutschland HD'
|
||||
},
|
||||
{
|
||||
lang: 'en',
|
||||
site_id: 'GB#123',
|
||||
name: 'Sky One HD'
|
||||
},
|
||||
{
|
||||
lang: 'it',
|
||||
site_id: 'IT#123',
|
||||
name: 'Sky Uno HD'
|
||||
}
|
||||
])
|
||||
expect(axios.get).toHaveBeenCalledTimes(6)
|
||||
expect(axios.get.mock.calls.map(([, options]) => options.headers)).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ 'X-SkyOTT-Territory': 'GB' },
|
||||
{ 'X-SkyOTT-Territory': 'DE' },
|
||||
{ 'X-SkyOTT-Territory': 'IT' }
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects unsupported channel territories', () => {
|
||||
expect(() => request.headers({ channel: { ...channel, site_id: 'FR#4086' } })).toThrow(
|
||||
'territory "DE", "GB" or "IT"'
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects an incomplete channel list', async () => {
|
||||
axios.get.mockImplementation((url, options) => {
|
||||
if (url.endsWith('/regions')) {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
regions: [{ bouquetId: 1, subBouquetId: 2 }]
|
||||
},
|
||||
headers: {},
|
||||
request: {}
|
||||
})
|
||||
}
|
||||
|
||||
if (options.headers['X-SkyOTT-Territory'] === 'DE') {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
services: [{ sid: '123', t: 'Sky One Deutschland HD' }]
|
||||
},
|
||||
headers: {},
|
||||
request: {}
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
data: {},
|
||||
headers: {},
|
||||
request: {}
|
||||
})
|
||||
})
|
||||
|
||||
await expect(channels()).rejects.toThrow('Unable to load complete Sky channel list')
|
||||
})
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"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"}]}]}
|
||||
@@ -1,21 +0,0 @@
|
||||
# sky.de
|
||||
|
||||
https://www.sky.de/tvguide-7599
|
||||
|
||||
### Download the guide
|
||||
|
||||
```sh
|
||||
npm run grab --- --sites=sky.de
|
||||
```
|
||||
|
||||
### Update channel list
|
||||
|
||||
```sh
|
||||
npm run channels:parse --- --config=./sites/sky.de/sky.de.config.js --output=./sites/sky.de/sky.de.channels.xml
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
```sh
|
||||
npm test --- sky.de
|
||||
```
|
||||
@@ -1,77 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<channels>
|
||||
<channel site="sky.de" site_id="116" lang="de" xmltv_id="13thStreetUniversal.de@HD">13th Street HD</channel>
|
||||
<channel site="sky.de" site_id="690" lang="de" xmltv_id="BeateUhseTV.de@HD">Beate Uhse HD</channel>
|
||||
<channel site="sky.de" site_id="761" lang="de" xmltv_id="CartoonitoCEE.uk@Germany">Cartoonito</channel>
|
||||
<channel site="sky.de" site_id="95" lang="de" xmltv_id="CartoonNetworkCEE.uk@Germany">Cartoon Network</channel>
|
||||
<channel site="sky.de" site_id="451" lang="de" xmltv_id="CrimePlusInvestigation.de@SD">Crime + Investigation</channel>
|
||||
<channel site="sky.de" site_id="659" lang="de" xmltv_id="DAZN1.uk@GermanyHD">DAZN 1 HD</channel>
|
||||
<channel site="sky.de" site_id="900" lang="de" xmltv_id="DAZN2.uk@GermanyHD">DAZN 2 HD</channel>
|
||||
<channel site="sky.de" site_id="72" lang="de" xmltv_id="Heimatkanal.de@SD">Heimatkanal</channel>
|
||||
<channel site="sky.de" site_id="86" lang="de" xmltv_id="History.de@HD">HISTORY Channel HD</channel>
|
||||
<channel site="sky.de" site_id="452" lang="de" xmltv_id="Jukebox.de@SD">Jukebox</channel>
|
||||
<channel site="sky.de" site_id="52" lang="de" xmltv_id="Motorvision.de@SD">Motorvision+</channel>
|
||||
<channel site="sky.de" site_id="1221" lang="de" xmltv_id="NationalGeographic.de@HD">NatGeo HD</channel>
|
||||
<channel site="sky.de" site_id="1222" lang="de" xmltv_id="NationalGeographicWild.de@HD">NatGeo Wild HD</channel>
|
||||
<channel site="sky.de" site_id="783" lang="de" xmltv_id="NickJr.de@SD">Nick Jr.</channel>
|
||||
<channel site="sky.de" site_id="951" lang="de" xmltv_id="Nicktoons.de">Nicktoons</channel>
|
||||
<channel site="sky.de" site_id="16" lang="de" xmltv_id="RomanceTV.de@HD">Romance TV HD</channel>
|
||||
<channel site="sky.de" site_id="2" lang="de" xmltv_id="SkyAtlantic.de@HD">Sky Atlantic HD</channel>
|
||||
<channel site="sky.de" site_id="1205" lang="de" xmltv_id="SkyCinemaAction.de@HD">Sky Cinema Action HD</channel>
|
||||
<channel site="sky.de" site_id="1209" lang="de" xmltv_id="SkyCinemaBestOf.de@HD">Sky Cinema Highlights HD</channel>
|
||||
<channel site="sky.de" site_id="1208" lang="de" xmltv_id="SkyCinemaClassics.de">Sky Cinema Classics HD</channel>
|
||||
<channel site="sky.de" site_id="1206" lang="de" xmltv_id="SkyCinemaFamily.de@HD">Sky Cinema Family HD</channel>
|
||||
<channel site="sky.de" site_id="1207" lang="de" xmltv_id="SkyCinemaPremieren.de@HD">Sky Cinema Premiere HD</channel>
|
||||
<channel site="sky.de" site_id="855" lang="de" xmltv_id="SkyCrime.de@HD">Sky Crime HD</channel>
|
||||
<channel site="sky.de" site_id="876" lang="de" xmltv_id="SkyDocumentaries.de@HD">Sky Documentaries HD</channel>
|
||||
<channel site="sky.de" site_id="753" lang="de" xmltv_id="SkyKrimi.de@HD">Sky Krimi HD</channel>
|
||||
<channel site="sky.de" site_id="875" lang="de" xmltv_id="SkyNature.de@HD">Sky Nature HD</channel>
|
||||
<channel site="sky.de" site_id="535" lang="de" xmltv_id="SkyOne.de@HD">Sky One HD</channel>
|
||||
<channel site="sky.de" site_id="888" lang="de" xmltv_id="SkyReplay.de@HD">Sky Replay HD</channel>
|
||||
<channel site="sky.de" site_id="1019" lang="de" xmltv_id="SkyShowcase.de@HD">Sky Showcase HD</channel>
|
||||
<channel site="sky.de" site_id="130" lang="de" xmltv_id="SkySport1.de@HD">Sky Sport 1 HD</channel>
|
||||
<channel site="sky.de" site_id="131" lang="de" xmltv_id="SkySport2.de@HD">Sky Sport 2 HD</channel>
|
||||
<channel site="sky.de" site_id="132" lang="de" xmltv_id="SkySport3.de@HD">Sky Sport 3 HD</channel>
|
||||
<channel site="sky.de" site_id="133" lang="de" xmltv_id="SkySport4.de@HD">Sky Sport 4 HD</channel>
|
||||
<channel site="sky.de" site_id="134" lang="de" xmltv_id="SkySport5.de@HD">Sky Sport 5 HD</channel>
|
||||
<channel site="sky.de" site_id="135" lang="de" xmltv_id="SkySport6.de@HD">Sky Sport 6 HD</channel>
|
||||
<channel site="sky.de" site_id="136" lang="de" xmltv_id="SkySport7.de@HD">Sky Sport 7 HD</channel>
|
||||
<channel site="sky.de" site_id="137" lang="de" xmltv_id="SkySport8.de@HD">Sky Sport 8 HD</channel>
|
||||
<channel site="sky.de" site_id="138" lang="de" xmltv_id="SkySport9.de@HD">Sky Sport 9 HD</channel>
|
||||
<channel site="sky.de" site_id="999" lang="de" xmltv_id="SkySport10.de@HD">Sky Sport 10 HD</channel>
|
||||
<channel site="sky.de" site_id="526" lang="de" xmltv_id="SkySport.de@UHD">Sky Sport UHD</channel>
|
||||
<channel site="sky.de" site_id="492" lang="de" xmltv_id="SkySportAustria1.at@HD">Sky Sport Austria 1 HD</channel>
|
||||
<channel site="sky.de" site_id="729" lang="de" xmltv_id="SkySportAustria2.at@HD">Sky Sport Austria 2 HD</channel>
|
||||
<channel site="sky.de" site_id="730" lang="de" xmltv_id="SkySportAustria3.at@HD">Sky Sport Austria 3 HD</channel>
|
||||
<channel site="sky.de" site_id="898" lang="de" xmltv_id="SkySportAustria4.at@HD">Sky Sport Austria 4 HD</channel>
|
||||
<channel site="sky.de" site_id="883" lang="de" xmltv_id="SkySportAustria5.at@HD">Sky Sport Austria 5 HD</channel>
|
||||
<channel site="sky.de" site_id="884" lang="de" xmltv_id="SkySportAustria6.at@HD">Sky Sport Austria 6 HD</channel>
|
||||
<channel site="sky.de" site_id="885" lang="de" xmltv_id="SkySportAustria7.at@HD">Sky Sport Austria 7 HD</channel>
|
||||
<channel site="sky.de" site_id="928" lang="de" xmltv_id="SkySportAustria8.at@HD">Sky Sport Austria 8 HD</channel>
|
||||
<channel site="sky.de" site_id="119" lang="de" xmltv_id="SkySportBundesliga1.de@HD">Sky Sport Bundesliga 1 HD</channel>
|
||||
<channel site="sky.de" site_id="120" lang="de" xmltv_id="SkySportBundesliga2.de@HD">Sky Sport Bundesliga 2 HD</channel>
|
||||
<channel site="sky.de" site_id="121" lang="de" xmltv_id="SkySportBundesliga3.de@HD">Sky Sport Bundesliga 3 HD</channel>
|
||||
<channel site="sky.de" site_id="122" lang="de" xmltv_id="SkySportBundesliga4.de@HD">Sky Sport Bundesliga 4 HD</channel>
|
||||
<channel site="sky.de" site_id="123" lang="de" xmltv_id="SkySportBundesliga5.de@HD">Sky Sport Bundesliga 5 HD</channel>
|
||||
<channel site="sky.de" site_id="124" lang="de" xmltv_id="SkySportBundesliga6.de@HD">Sky Sport Bundesliga 6 HD</channel>
|
||||
<channel site="sky.de" site_id="125" lang="de" xmltv_id="SkySportBundesliga7.de@HD">Sky Sport Bundesliga 7 HD</channel>
|
||||
<channel site="sky.de" site_id="126" lang="de" xmltv_id="SkySportBundesliga8.de@HD">Sky Sport Bundesliga 8 HD</channel>
|
||||
<channel site="sky.de" site_id="127" lang="de" xmltv_id="SkySportBundesliga9.de@HD">Sky Sport Bundesliga 9 HD</channel>
|
||||
<channel site="sky.de" site_id="998" lang="de" xmltv_id="SkySportBundesliga10.de@HD">Sky Sport Bundesliga 10 HD</channel>
|
||||
<channel site="sky.de" site_id="1018" lang="de" xmltv_id="SkySportBundesliga.de@HD">Sky Sport Bundesliga HD</channel>
|
||||
<channel site="sky.de" site_id="531" lang="de" xmltv_id="SkySportBundesliga.de@UHD">Sky Sport Bundesliga UHD</channel>
|
||||
<channel site="sky.de" site_id="853" lang="de" xmltv_id="SkySportF1.de@HD">Sky Sport F1 HD</channel>
|
||||
<channel site="sky.de" site_id="1002" lang="de" xmltv_id="SkySportGolf.de@HD">Sky Sport Golf HD</channel>
|
||||
<channel site="sky.de" site_id="1005" lang="de" xmltv_id="SkySportMix.de@HD">Sky Sport Mix HD</channel>
|
||||
<channel site="sky.de" site_id="17" lang="de" xmltv_id="SkySportNews.de@HD">Sky Sport News HD</channel>
|
||||
<channel site="sky.de" site_id="1000" lang="de" xmltv_id="SkySportPremierLeague.de@HD">Sky Sport Premier League HD</channel>
|
||||
<channel site="sky.de" site_id="1012" lang="de" xmltv_id="SkySportTennis.de@HD">Sky Sport Tennis HD</channel>
|
||||
<channel site="sky.de" site_id="1014" lang="de" xmltv_id="SkySportTopEvent.de@HD">Sky Sport Top Event HD</channel>
|
||||
<channel site="sky.de" site_id="1032" lang="de" xmltv_id="SportdigitalFUSSBALL.de">SPORTDIGITAL FUSSBALL</channel>
|
||||
<channel site="sky.de" site_id="115" lang="de" xmltv_id="SYFY.de@HD">SYFY HD</channel>
|
||||
<channel site="sky.de" site_id="172" lang="de" xmltv_id="UniversalTV.de@HD">Universal TV HD</channel>
|
||||
<channel site="sky.de" site_id="522" lang="de" xmltv_id="WarnerTVComedy.de@HD">Warner TV Comedy HD</channel>
|
||||
<channel site="sky.de" site_id="689" lang="de" xmltv_id="WarnerTVFilm.de@HD">Warner TV Film HD</channel>
|
||||
<channel site="sky.de" site_id="762" lang="de" xmltv_id="WarnerTVFilm.de@SD">Warner TV Film</channel>
|
||||
<channel site="sky.de" site_id="6" lang="de" xmltv_id="WarnerTVSerie.de@HD">Warner TV Serie HD</channel>
|
||||
</channels>
|
||||
@@ -1,78 +0,0 @@
|
||||
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 : []
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
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([])
|
||||
})
|
||||
Reference in New Issue
Block a user