epg-grabber 0.27.2 → 0.28.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -166,12 +166,24 @@ module.exports = {
166
166
  title, // program title (required)
167
167
  start, // start time of the program (required)
168
168
  stop, // end time of the program (required)
169
+ sub_title, // program sub-title (optional)
169
170
  description, // description of the program (optional)
170
- category, // program type (optional)
171
+ category, // type of program (optional)
171
172
  season, // season number (optional)
172
173
  episode, // episode number (optional)
174
+ date, // the date the programme or film was finished (optional)
173
175
  icon, // image associated with the program (optional)
174
- lang // language of the description (default: 'en')
176
+ rating, // program rating (optional)
177
+ director, // the name of director (optional)
178
+ actor, // the name of actor (optional)
179
+ writer, // the name of writer (optional)
180
+ adapter, // the name of adapter (optional)
181
+ producer, // the name of producer (optional)
182
+ composer, // the name of composer (optional)
183
+ editor, // the name of editor (optional)
184
+ presenter, // the name of presenter (optional)
185
+ commentator, // the name of commentator (optional)
186
+ guest // the name of guest (optional)
175
187
  },
176
188
  ...
177
189
  ]
@@ -2,15 +2,17 @@
2
2
 
3
3
  const { Command } = require('commander')
4
4
  const program = new Command()
5
- const fs = require('fs')
6
- const path = require('path')
7
- const EPGGrabber = require('../src/index')
8
- const utils = require('../src/utils')
9
- const { name, version, description } = require('../package.json')
10
5
  const { merge } = require('lodash')
11
6
  const { gzip } = require('node-gzip')
12
- const { createLogger, format, transports } = require('winston')
13
- const { combine, timestamp, printf } = format
7
+ const file = require('../src/file')
8
+ const { EPGGrabber, parseChannels, generateXMLTV } = require('../src/index')
9
+ const { create: createLogger } = require('../src/logger')
10
+ const { parseNumber, getUTCDate } = require('../src/utils')
11
+ const { name, version, description } = require('../package.json')
12
+ const dayjs = require('dayjs')
13
+ const utc = require('dayjs/plugin/utc')
14
+
15
+ dayjs.extend(utc)
14
16
 
15
17
  program
16
18
  .name(name)
@@ -20,13 +22,13 @@ program
20
22
  .option('-o, --output <output>', 'Path to output file')
21
23
  .option('--channels <channels>', 'Path to channels.xml file')
22
24
  .option('--lang <lang>', 'Set default language for all programs')
23
- .option('--days <days>', 'Number of days for which to grab the program', parseInteger, 1)
24
- .option('--delay <delay>', 'Delay between requests (in mileseconds)', parseInteger)
25
- .option('--timeout <timeout>', 'Set a timeout for each request (in mileseconds)', parseInteger)
25
+ .option('--days <days>', 'Number of days for which to grab the program', parseNumber, 1)
26
+ .option('--delay <delay>', 'Delay between requests (in mileseconds)', parseNumber)
27
+ .option('--timeout <timeout>', 'Set a timeout for each request (in mileseconds)', parseNumber)
26
28
  .option(
27
29
  '--cache-ttl <cacheTtl>',
28
30
  'Maximum time for storing each request (in milliseconds)',
29
- parseInteger
31
+ parseNumber
30
32
  )
31
33
  .option('--gzip', 'Compress the output', false)
32
34
  .option('--debug', 'Enable debug mode', false)
@@ -36,39 +38,13 @@ program
36
38
  .parse(process.argv)
37
39
 
38
40
  const options = program.opts()
39
-
40
- const fileFormat = printf(({ level, message, timestamp }) => {
41
- return `[${timestamp}] ${level.toUpperCase()}: ${message}`
42
- })
43
-
44
- const consoleFormat = printf(({ level, message, timestamp }) => {
45
- if (level === 'error') return ` Error: ${message}`
46
-
47
- return message
48
- })
49
-
50
- const t = [new transports.Console({ format: consoleFormat })]
51
-
52
- if (options.log) {
53
- t.push(
54
- new transports.File({
55
- filename: path.resolve(options.log),
56
- format: combine(timestamp(), fileFormat),
57
- options: { flags: 'w' }
58
- })
59
- )
60
- }
61
-
62
- const logger = createLogger({
63
- level: options.logLevel,
64
- transports: t
65
- })
41
+ const logger = createLogger(options)
66
42
 
67
43
  async function main() {
68
44
  logger.info('Starting...')
69
45
 
70
46
  logger.info(`Loading '${options.config}'...`)
71
- let config = require(path.resolve(options.config))
47
+ let config = require(file.resolve(options.config))
72
48
  config = merge(config, {
73
49
  days: options.days,
74
50
  debug: options.debug,
@@ -83,29 +59,34 @@ async function main() {
83
59
  if (options.cacheTtl) config.request.cache.ttl = options.cacheTtl
84
60
  if (options.channels) config.channels = options.channels
85
61
  else if (config.channels)
86
- config.channels = path.join(path.dirname(options.config), config.channels)
62
+ config.channels = file.join(file.dirname(options.config), config.channels)
87
63
  else throw new Error("The required 'channels' property is missing")
88
64
 
89
65
  if (!config.channels) return logger.error('Path to [site].channels.xml is missing')
90
66
  logger.info(`Loading '${config.channels}'...`)
91
- const channelsXML = fs.readFileSync(path.resolve(config.channels), { encoding: 'utf-8' })
92
- const { channels } = utils.parseChannels(channelsXML)
67
+ const grabber = new EPGGrabber(config)
68
+
69
+ const channelsXML = file.read(config.channels)
70
+ const { channels } = parseChannels(channelsXML)
93
71
 
94
72
  let programs = []
95
73
  let i = 1
96
74
  let days = options.days || 1
97
75
  const total = channels.length * days
98
- const utcDate = utils.getUTCDate()
76
+ const utcDate = getUTCDate()
99
77
  const dates = Array.from({ length: config.days }, (_, i) => utcDate.add(i, 'd'))
100
- const grabber = new EPGGrabber(config)
101
78
  for (let channel of channels) {
79
+ if (!channel.logo && config.logo) {
80
+ channel.logo = await grabber.loadLogo(channel)
81
+ }
82
+
102
83
  for (let date of dates) {
103
84
  await grabber
104
85
  .grab(channel, date, (data, err) => {
105
86
  logger.info(
106
- `[${i}/${total}] ${config.site} - ${data.channel.xmltv_id} - ${data.date.format(
107
- 'MMM D, YYYY'
108
- )} (${data.programs.length} programs)`
87
+ `[${i}/${total}] ${config.site} - ${data.channel.id} - ${dayjs
88
+ .utc(data.date)
89
+ .format('MMM D, YYYY')} (${data.programs.length} programs)`
109
90
  )
110
91
 
111
92
  if (err) logger.error(err.message)
@@ -118,15 +99,15 @@ async function main() {
118
99
  }
119
100
  }
120
101
 
121
- const xml = utils.convertToXMLTV({ config, channels, programs })
102
+ const xml = generateXMLTV({ channels, programs })
122
103
  let outputPath = options.output || config.output
123
104
  if (options.gzip) {
124
105
  outputPath = outputPath || 'guide.xml.gz'
125
106
  const compressed = await gzip(xml)
126
- utils.writeToFile(outputPath, compressed)
107
+ file.write(outputPath, compressed)
127
108
  } else {
128
109
  outputPath = outputPath || 'guide.xml'
129
- utils.writeToFile(outputPath, xml)
110
+ file.write(outputPath, xml)
130
111
  }
131
112
 
132
113
  logger.info(`File '${outputPath}' successfully saved`)
@@ -134,7 +115,3 @@ async function main() {
134
115
  }
135
116
 
136
117
  main()
137
-
138
- function parseInteger(val) {
139
- return val ? parseInt(val) : null
140
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "epg-grabber",
3
- "version": "0.27.2",
3
+ "version": "0.28.2",
4
4
  "description": "Node.js CLI tool for grabbing EPG from different sites",
5
5
  "main": "src/index.js",
6
6
  "preferGlobal": true,
package/src/Channel.js ADDED
@@ -0,0 +1,19 @@
1
+ class Channel {
2
+ constructor(c) {
3
+ const data = {
4
+ id: c.xmltv_id,
5
+ name: c.name,
6
+ site: c.site || '',
7
+ site_id: c.site_id,
8
+ lang: c.lang || '',
9
+ logo: c.logo || '',
10
+ url: c.site ? `https://${c.site}` : ''
11
+ }
12
+
13
+ for (let key in data) {
14
+ this[key] = data[key]
15
+ }
16
+ }
17
+ }
18
+
19
+ module.exports = Channel
package/src/Program.js ADDED
@@ -0,0 +1,122 @@
1
+ const { padStart } = require('lodash')
2
+ const { toArray, toUnix, parseNumber } = require('./utils')
3
+
4
+ class Program {
5
+ constructor(p, c) {
6
+ const data = {
7
+ channel: c.id,
8
+ title: p.title,
9
+ sub_title: p.sub_title || '',
10
+ description: p.description || p.desc,
11
+ icon: toIconObject(p.icon),
12
+ episodeNumbers: getEpisodeNumbers(p.season, p.episode),
13
+ date: p.date ? toUnix(p.date) : null,
14
+ start: toUnix(p.start),
15
+ stop: toUnix(p.stop),
16
+ urls: toArray(p.url).map(toUrlObject),
17
+ ratings: toArray(p.ratings || p.rating).map(toRatingObject),
18
+ categories: toArray(p.categories || p.category),
19
+ directors: toArray(p.directors || p.director).map(toPersonObject),
20
+ actors: toArray(p.actors || p.actor).map(toPersonObject),
21
+ writers: toArray(p.writers || p.writer).map(toPersonObject),
22
+ adapters: toArray(p.adapters || p.adapter).map(toPersonObject),
23
+ producers: toArray(p.producers || p.producer).map(toPersonObject),
24
+ composers: toArray(p.composers || p.composer).map(toPersonObject),
25
+ editors: toArray(p.editors || p.editor).map(toPersonObject),
26
+ presenters: toArray(p.presenters || p.presenter).map(toPersonObject),
27
+ commentators: toArray(p.commentators || p.commentator).map(toPersonObject),
28
+ guests: toArray(p.guests || p.guest).map(toPersonObject)
29
+ }
30
+
31
+ for (let key in data) {
32
+ this[key] = data[key]
33
+ }
34
+ }
35
+ }
36
+
37
+ module.exports = Program
38
+
39
+ function toPersonObject(person) {
40
+ if (typeof person === 'string') {
41
+ return {
42
+ value: person,
43
+ url: [],
44
+ image: []
45
+ }
46
+ }
47
+
48
+ return {
49
+ value: person.value,
50
+ url: toArray(person.url).map(toUrlObject),
51
+ image: toArray(person.image).map(toImageObject)
52
+ }
53
+ }
54
+
55
+ function toImageObject(image) {
56
+ if (typeof image === 'string') return { type: '', size: '', orient: '', system: '', value: image }
57
+
58
+ return {
59
+ type: image.type || '',
60
+ size: image.size || '',
61
+ orient: image.orient || '',
62
+ system: image.system || '',
63
+ value: image.value
64
+ }
65
+ }
66
+
67
+ function toRatingObject(rating) {
68
+ if (typeof rating === 'string') return { system: '', icon: '', value: rating }
69
+
70
+ return {
71
+ system: rating.system || '',
72
+ icon: rating.icon || '',
73
+ value: rating.value || ''
74
+ }
75
+ }
76
+
77
+ function toUrlObject(url) {
78
+ if (typeof url === 'string') return { system: '', value: url }
79
+
80
+ return {
81
+ system: url.system || '',
82
+ value: url.value || ''
83
+ }
84
+ }
85
+
86
+ function toIconObject(icon) {
87
+ if (!icon || typeof icon === 'string') return { src: icon }
88
+
89
+ return {
90
+ src: icon.src || ''
91
+ }
92
+ }
93
+
94
+ function getEpisodeNumbers(s, e) {
95
+ s = parseNumber(s)
96
+ e = parseNumber(e)
97
+
98
+ return [createXMLTVNS(s, e), createOnScreen(s, e)].filter(Boolean)
99
+ }
100
+
101
+ function createXMLTVNS(s, e) {
102
+ if (!e) return null
103
+ s = s || 1
104
+
105
+ return {
106
+ system: 'xmltv_ns',
107
+ value: `${s - 1}.${e - 1}.0/1`
108
+ }
109
+ }
110
+
111
+ function createOnScreen(s, e) {
112
+ if (!e) return null
113
+ s = s || 1
114
+
115
+ s = padStart(s, 2, '0')
116
+ e = padStart(e, 2, '0')
117
+
118
+ return {
119
+ system: 'onscreen',
120
+ value: `S${s}E${e}`
121
+ }
122
+ }
package/src/client.js ADDED
@@ -0,0 +1,138 @@
1
+ const { CurlGenerator } = require('curl-generator')
2
+ const axios = require('axios').default
3
+ const axiosCookieJarSupport = require('axios-cookiejar-support').default
4
+ const { setupCache } = require('axios-cache-interceptor')
5
+ const { isObject, isPromise, getUTCDate } = require('./utils')
6
+
7
+ axiosCookieJarSupport(axios)
8
+
9
+ module.exports.create = create
10
+ module.exports.buildRequest = buildRequest
11
+ module.exports.parseResponse = parseResponse
12
+
13
+ let timeout
14
+
15
+ function create(config) {
16
+ const client = setupCache(
17
+ axios.create({
18
+ headers: {
19
+ 'User-Agent':
20
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36 Edg/79.0.309.71'
21
+ }
22
+ })
23
+ )
24
+
25
+ client.interceptors.request.use(
26
+ function (request) {
27
+ if (config.debug) {
28
+ console.log('Request:', JSON.stringify(request, null, 2))
29
+ }
30
+ return request
31
+ },
32
+ function (error) {
33
+ return Promise.reject(error)
34
+ }
35
+ )
36
+
37
+ client.interceptors.response.use(
38
+ function (response) {
39
+ if (config.debug) {
40
+ const data =
41
+ isObject(response.data) || Array.isArray(response.data)
42
+ ? JSON.stringify(response.data)
43
+ : response.data.toString()
44
+ console.log(
45
+ 'Response:',
46
+ JSON.stringify(
47
+ {
48
+ headers: response.headers,
49
+ data,
50
+ cached: response.cached
51
+ },
52
+ null,
53
+ 2
54
+ )
55
+ )
56
+ }
57
+
58
+ clearTimeout(timeout)
59
+ return response
60
+ },
61
+ function (error) {
62
+ clearTimeout(timeout)
63
+ return Promise.reject(error)
64
+ }
65
+ )
66
+
67
+ return client
68
+ }
69
+
70
+ async function buildRequest({ channel, date, config }) {
71
+ date = typeof date === 'string' ? getUTCDate(date) : date
72
+ const CancelToken = axios.CancelToken
73
+ const source = CancelToken.source()
74
+ const request = { ...config.request }
75
+ timeout = setTimeout(() => {
76
+ source.cancel('Connection timeout')
77
+ }, request.timeout)
78
+ request.headers = await getRequestHeaders({ channel, date, config })
79
+ request.url = await getRequestUrl({ channel, date, config })
80
+ request.data = await getRequestData({ channel, date, config })
81
+ request.cancelToken = source.token
82
+
83
+ if (config.curl) {
84
+ const curl = CurlGenerator({
85
+ url: request.url,
86
+ method: request.method,
87
+ headers: request.headers,
88
+ body: request.data
89
+ })
90
+ console.log(curl)
91
+ }
92
+
93
+ return request
94
+ }
95
+
96
+ function parseResponse(response) {
97
+ return {
98
+ content: response.data.toString(),
99
+ buffer: response.data,
100
+ headers: response.headers,
101
+ request: response.request,
102
+ cached: response.cached
103
+ }
104
+ }
105
+
106
+ async function getRequestHeaders({ channel, date, config }) {
107
+ if (typeof config.request.headers === 'function') {
108
+ const headers = config.request.headers({ channel, date })
109
+ if (isPromise(headers)) {
110
+ return await headers
111
+ }
112
+ return headers
113
+ }
114
+
115
+ return config.request.headers || null
116
+ }
117
+
118
+ async function getRequestData({ channel, date, config }) {
119
+ if (typeof config.request.data === 'function') {
120
+ const data = config.request.data({ channel, date })
121
+ if (isPromise(data)) {
122
+ return await data
123
+ }
124
+ return data
125
+ }
126
+ return config.request.data || null
127
+ }
128
+
129
+ async function getRequestUrl({ channel, date, config }) {
130
+ if (typeof config.url === 'function') {
131
+ const url = config.url({ channel, date })
132
+ if (isPromise(url)) {
133
+ return await url
134
+ }
135
+ return url
136
+ }
137
+ return config.url
138
+ }
package/src/config.js ADDED
@@ -0,0 +1,34 @@
1
+ const tough = require('tough-cookie')
2
+ const { merge } = require('lodash')
3
+
4
+ module.exports.load = load
5
+
6
+ function load(config) {
7
+ if (!config.site) throw new Error("The required 'site' property is missing")
8
+ if (!config.url) throw new Error("The required 'url' property is missing")
9
+ if (typeof config.url !== 'function' && typeof config.url !== 'string')
10
+ throw new Error("The 'url' property should return the function or string")
11
+ if (!config.parser) throw new Error("The required 'parser' function is missing")
12
+ if (typeof config.parser !== 'function')
13
+ throw new Error("The 'parser' property should return the function")
14
+ if (config.logo && typeof config.logo !== 'function')
15
+ throw new Error("The 'logo' property should return the function")
16
+
17
+ const defaultConfig = {
18
+ days: 1,
19
+ lang: 'en',
20
+ delay: 3000,
21
+ output: 'guide.xml',
22
+ request: {
23
+ method: 'GET',
24
+ maxContentLength: 5 * 1024 * 1024,
25
+ timeout: 5000,
26
+ withCredentials: true,
27
+ jar: new tough.CookieJar(),
28
+ responseType: 'arraybuffer',
29
+ cache: false
30
+ }
31
+ }
32
+
33
+ return merge(defaultConfig, config)
34
+ }
package/src/file.js ADDED
@@ -0,0 +1,33 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+
4
+ module.exports.read = read
5
+ module.exports.write = write
6
+ module.exports.resolve = resolve
7
+ module.exports.join = join
8
+ module.exports.dirname = dirname
9
+
10
+ function read(filepath) {
11
+ return fs.readFileSync(path.resolve(filepath), { encoding: 'utf-8' })
12
+ }
13
+
14
+ function write(filepath, data) {
15
+ const dir = path.resolve(path.dirname(filepath))
16
+ if (!fs.existsSync(dir)) {
17
+ fs.mkdirSync(dir, { recursive: true })
18
+ }
19
+
20
+ fs.writeFileSync(path.resolve(filepath), data)
21
+ }
22
+
23
+ function resolve(filepath) {
24
+ return path.resolve(filepath)
25
+ }
26
+
27
+ function join(path1, path2) {
28
+ return path.join(path1, path2)
29
+ }
30
+
31
+ function dirname(filepath) {
32
+ return path.dirname(filepath)
33
+ }
package/src/index.js CHANGED
@@ -1,41 +1,47 @@
1
- const utils = require('./utils')
1
+ const { merge } = require('lodash')
2
+ const { create: createClient, buildRequest, parseResponse } = require('./client')
3
+ const { parseChannels, parsePrograms } = require('./parser')
4
+ const { generate: generateXMLTV } = require('./xmltv')
5
+ const { load: loadConfig } = require('./config')
6
+ const { sleep, isPromise } = require('./utils')
7
+
8
+ module.exports.generateXMLTV = generateXMLTV
9
+ module.exports.parseChannels = parseChannels
2
10
 
3
11
  class EPGGrabber {
4
12
  constructor(config = {}) {
5
- this.config = utils.loadConfig(config)
6
- this.client = utils.createClient(config)
13
+ this.config = loadConfig(config)
14
+ this.client = createClient(config)
15
+ }
16
+
17
+ async loadLogo(channel) {
18
+ const logo = this.config.logo({ channel })
19
+ if (isPromise(logo)) {
20
+ return await logo
21
+ }
22
+ return logo
7
23
  }
8
24
 
9
25
  async grab(channel, date, cb = () => {}) {
10
- date = typeof date === 'string' ? utils.getUTCDate(date) : date
11
- channel.lang = channel.lang || this.config.lang || null
12
-
13
- let programs = []
14
- const item = { date, channel }
15
- await utils
16
- .buildRequest(item, this.config)
17
- .then(request => utils.fetchData(this.client, request))
18
- .then(response => utils.parseResponse(item, response, this.config))
19
- .then(results => {
20
- item.programs = results
21
- cb(item, null)
22
- programs = programs.concat(results)
23
- })
24
- .catch(error => {
25
- item.programs = []
26
- if (this.config.debug) {
27
- console.log('Error:', JSON.stringify(error, null, 2))
28
- }
29
- cb(item, error)
30
- })
26
+ await sleep(this.config.delay)
31
27
 
32
- await utils.sleep(this.config.delay)
28
+ return buildRequest({ channel, date, config: this.config })
29
+ .then(this.client)
30
+ .then(parseResponse)
31
+ .then(data => merge({ channel, date, config: this.config }, data))
32
+ .then(parsePrograms)
33
+ .then(programs => {
34
+ cb({ channel, date, programs })
33
35
 
34
- return programs
36
+ return programs
37
+ })
38
+ .catch(err => {
39
+ if (this.config.debug) console.log('Error:', JSON.stringify(err, null, 2))
40
+ cb({ channel, date, programs: [] }, err)
41
+
42
+ return []
43
+ })
35
44
  }
36
45
  }
37
46
 
38
- EPGGrabber.convertToXMLTV = utils.convertToXMLTV
39
- EPGGrabber.parseChannels = utils.parseChannels
40
-
41
- module.exports = EPGGrabber
47
+ module.exports.EPGGrabber = EPGGrabber
package/src/logger.js ADDED
@@ -0,0 +1,34 @@
1
+ const { createLogger, format, transports } = require('winston')
2
+ const { combine, timestamp, printf } = format
3
+ const path = require('path')
4
+
5
+ module.exports.create = create
6
+
7
+ function create(options) {
8
+ const fileFormat = printf(({ level, message, timestamp }) => {
9
+ return `[${timestamp}] ${level.toUpperCase()}: ${message}`
10
+ })
11
+
12
+ const consoleFormat = printf(({ level, message }) => {
13
+ if (level === 'error') return ` Error: ${message}`
14
+
15
+ return message
16
+ })
17
+
18
+ const t = [new transports.Console({ format: consoleFormat })]
19
+
20
+ if (options.log) {
21
+ t.push(
22
+ new transports.File({
23
+ filename: path.resolve(options.log),
24
+ format: combine(timestamp(), fileFormat),
25
+ options: { flags: 'w' }
26
+ })
27
+ )
28
+ }
29
+
30
+ return createLogger({
31
+ level: options.logLevel,
32
+ transports: t
33
+ })
34
+ }
package/src/parser.js ADDED
@@ -0,0 +1,45 @@
1
+ const convert = require('xml-js')
2
+ const Channel = require('./Channel')
3
+ const Program = require('./Program')
4
+ const { isPromise } = require('./utils')
5
+
6
+ module.exports.parseChannels = parseChannels
7
+ module.exports.parsePrograms = parsePrograms
8
+
9
+ function parseChannels(xml) {
10
+ const result = convert.xml2js(xml)
11
+ const siteTag = result.elements.find(el => el.name === 'site') || {}
12
+ if (!siteTag.elements) return []
13
+ const rootSite = siteTag.attributes.site
14
+
15
+ const channelsTag = siteTag.elements.find(el => el.name === 'channels')
16
+ if (!channelsTag.elements) return []
17
+
18
+ const channels = channelsTag.elements
19
+ .filter(el => el.name === 'channel')
20
+ .map(el => {
21
+ let { xmltv_id, site, logo } = el.attributes
22
+ const name = el.elements.find(el => el.type === 'text').text
23
+ if (!name) throw new Error(`Channel '${xmltv_id}' has no valid name`)
24
+ site = site || rootSite
25
+
26
+ return new Channel({ name, xmltv_id, logo, site })
27
+ })
28
+
29
+ return { site: rootSite, channels }
30
+ }
31
+
32
+ async function parsePrograms(data) {
33
+ const { config, channel } = data
34
+ let programs = config.parser(data)
35
+
36
+ if (isPromise(programs)) {
37
+ programs = await programs
38
+ }
39
+
40
+ if (!Array.isArray(programs)) {
41
+ throw new Error('Parser should return an array')
42
+ }
43
+
44
+ return programs.filter(i => i).map(p => new Program(p, channel))
45
+ }