epg-grabber 0.12.1 → 0.15.1

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
@@ -8,7 +8,64 @@ Node.js CLI tool for grabbing EPG from different websites.
8
8
  npm install -g epg-grabber
9
9
  ```
10
10
 
11
- ## Usage
11
+ ## Quick Start
12
+
13
+ ```sh
14
+ epg-grabber --config=example.com.config.js
15
+ ```
16
+
17
+ #### example.com.config.js
18
+
19
+ ```js
20
+ module.exports = {
21
+ site: 'example.com',
22
+ channels: 'example.com.channels.xml',
23
+ url: function (context) {
24
+ const { date, channel } = context
25
+
26
+ return `https://api.example.com/${date.format('YYYY-MM-DD')}/channel/${channel.site_id}`
27
+ },
28
+ parser: function (context) {
29
+ const programs = JSON.parse(context.content)
30
+
31
+ return programs.map(program => {
32
+ return {
33
+ title: program.title,
34
+ start: program.start,
35
+ stop: program.stop
36
+ }
37
+ })
38
+ }
39
+ }
40
+ ```
41
+
42
+ #### example.com.channels.xml
43
+
44
+ ```xml
45
+ <?xml version="1.0" ?>
46
+ <site site="example.com">
47
+ <channels>
48
+ <channel site_id="cnn-23" xmltv_id="CNN.us">CNN</channel>
49
+ </channels>
50
+ </site>
51
+ ```
52
+
53
+ ## Example Output
54
+
55
+ ```xml
56
+ <tv>
57
+ <channel id="CNN.us">
58
+ <display-name>CNN</display-name>
59
+ <url>https://example.com</url>
60
+ </channel>
61
+ <programme start="20211116040000 +0000" stop="20211116050000 +0000" channel="CNN.us">
62
+ <title lang="en">News at 10PM</title>
63
+ </programme>
64
+ // ...
65
+ </tv>
66
+ ```
67
+
68
+ ## CLI
12
69
 
13
70
  ```sh
14
71
  epg-grabber --config=example.com.config.js
@@ -22,9 +79,12 @@ Arguments:
22
79
  - `--lang`: set default language for all programs (default: 'en')
23
80
  - `--days`: number of days for which to grab the program (default: 1)
24
81
  - `--delay`: delay between requests (default: 3000)
82
+ - `--timeout`: set a timeout for each request (default: 5000)
25
83
  - `--debug`: enable debug mode (default: false)
84
+ - `--log`: path to log file (optional)
85
+ - `--log-level`: set the log level (default: 'info')
26
86
 
27
- #### example.com.config.js
87
+ ## Site Config
28
88
 
29
89
  ```js
30
90
  module.exports = {
@@ -41,12 +101,11 @@ module.exports = {
41
101
  timeout: 5000,
42
102
 
43
103
  /**
44
- * @param {object} date The 'dayjs' instance with the requested date
45
- * @param {object} channel Data about the requested channel
104
+ * @param {object} context
46
105
  *
47
106
  * @return {string} The function should return headers for each request (optional)
48
107
  */
49
- headers: function({ date, channel }) {
108
+ headers: function(context) {
50
109
  return {
51
110
  'User-Agent':
52
111
  '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'
@@ -54,12 +113,13 @@ module.exports = {
54
113
  },
55
114
 
56
115
  /**
57
- * @param {object} date The 'dayjs' instance with the requested date
58
- * @param {object} channel Data about the requested channel
116
+ * @param {object} context
59
117
  *
60
118
  * @return {string} The function should return data for each request (optional)
61
119
  */
62
- data: function({ date, channel }) {
120
+ data: function(context) {
121
+ const { channel, date } = context
122
+
63
123
  return {
64
124
  channels: [channel.site_id],
65
125
  dateStart: date.format('YYYY-MM-DDT00:00:00-00:00'),
@@ -69,32 +129,29 @@ module.exports = {
69
129
  },
70
130
 
71
131
  /**
72
- * @param {object} date The 'dayjs' instance with the requested date
73
- * @param {object} channel Data about the requested channel
132
+ * @param {object} context
74
133
  *
75
134
  * @return {string} The function should return URL of the program page for the channel
76
135
  */
77
- url: function ({ date, channel }) {
78
- return `https://example.com/${date.format('YYYY-MM-DD')}/channel/${channel.site_id}.html`
136
+ url: function (context) {
137
+ return `https://example.com/${context.date.format('YYYY-MM-DD')}/channel/${context.channel.site_id}.html`
79
138
  },
80
139
 
81
140
  /**
82
- * @param {object} channel Data about the requested channel
83
- * @param {string} content The response received after the request at the above url
141
+ * @param {object} context
84
142
  *
85
143
  * @return {string} The function should return URL of the channel logo (optional)
86
144
  */
87
- logo: function ({ channel, content }) {
88
- return `https://example.com/logos/${channel.site_id}.png`
145
+ logo: function (context) {
146
+ return `https://example.com/logos/${context.channel.site_id}.png`
89
147
  },
90
148
 
91
149
  /**
92
- * @param {object} date The 'dayjs' instance with the requested date
93
- * @param {string} content The response received after the request at the above url
150
+ * @param {object} context
94
151
  *
95
152
  * @return {array} The function should return an array of programs with their descriptions
96
153
  */
97
- parser: function ({ date, content }) {
154
+ parser: function (context) {
98
155
 
99
156
  // content parsing...
100
157
 
@@ -114,7 +171,16 @@ module.exports = {
114
171
  }
115
172
  ```
116
173
 
117
- #### example.com.channels.xml
174
+ ## Context Object
175
+
176
+ From each function in `config.js` you can access a `context` object containing the following data:
177
+
178
+ - `channel`: The object describing the current channel (xmltv_id, site_id, name, lang)
179
+ - `date`: The 'dayjs' instance with the requested date
180
+ - `content`: The response data as a String
181
+ - `buffer`: The response data as an ArrayBuffer
182
+
183
+ ## Channels List
118
184
 
119
185
  ```xml
120
186
  <?xml version="1.0" ?>
@@ -8,6 +8,8 @@ const grabber = require('../src/index')
8
8
  const utils = require('../src/utils')
9
9
  const { name, version, description } = require('../package.json')
10
10
  const merge = require('lodash.merge')
11
+ const { createLogger, format, transports } = require('winston')
12
+ const { combine, timestamp, printf } = format
11
13
 
12
14
  program
13
15
  .name(name)
@@ -17,44 +19,84 @@ program
17
19
  .option('-o, --output <output>', 'Path to output file')
18
20
  .option('--channels <channels>', 'Path to channels.xml file')
19
21
  .option('--lang <lang>', 'Set default language for all programs')
20
- .option('--days <days>', 'Number of days for which to grab the program', parseInteger)
22
+ .option('--days <days>', 'Number of days for which to grab the program', parseInteger, 1)
21
23
  .option('--delay <delay>', 'Delay between requests (in mileseconds)', parseInteger)
24
+ .option('--timeout <timeout>', 'Set a timeout for each request (in mileseconds)', parseInteger)
22
25
  .option('--debug', 'Enable debug mode', false)
26
+ .option('--log <log>', 'Path to log file')
27
+ .option('--log-level <level>', 'Set log level', 'info')
23
28
  .parse(process.argv)
24
29
 
25
30
  const options = program.opts()
26
31
 
32
+ const fileFormat = printf(({ level, message, timestamp }) => {
33
+ return `[${timestamp}] ${level.toUpperCase()}: ${message}`
34
+ })
35
+
36
+ const consoleFormat = printf(({ level, message, timestamp }) => {
37
+ if (level === 'error') return ` Error: ${message}`
38
+
39
+ return message
40
+ })
41
+
42
+ const t = [new transports.Console({ format: consoleFormat })]
43
+
44
+ if (options.log) {
45
+ t.push(
46
+ new transports.File({
47
+ filename: path.resolve(options.log),
48
+ format: combine(timestamp(), fileFormat),
49
+ options: { flags: 'w' }
50
+ })
51
+ )
52
+ }
53
+
54
+ const logger = createLogger({
55
+ level: options.logLevel,
56
+ transports: t
57
+ })
58
+
27
59
  async function main() {
28
- console.log('\r\nStarting...')
60
+ logger.info('Starting...')
29
61
 
30
- console.log(`Loading '${options.config}'...`)
62
+ logger.info(`Loading '${options.config}'...`)
31
63
  let config = require(path.resolve(options.config))
32
- config = merge(config, options)
64
+ config = merge(config, {
65
+ days: options.days,
66
+ debug: options.debug,
67
+ lang: options.lang,
68
+ delay: options.delay,
69
+ request: {
70
+ timeout: options.timeout
71
+ }
72
+ })
33
73
 
34
74
  if (options.channels) config.channels = options.channels
35
75
  else if (config.channels)
36
76
  config.channels = path.join(path.dirname(options.config), config.channels)
37
77
  else throw new Error("The required 'channels' property is missing")
38
78
 
39
- if (!config.channels) throw new Error('Path to [site].channels.xml is missing')
40
- console.log(`Loading '${config.channels}'...`)
79
+ if (!config.channels) return logger.error('Path to [site].channels.xml is missing')
80
+ logger.info(`Loading '${config.channels}'...`)
41
81
  const channelsXML = fs.readFileSync(path.resolve(config.channels), { encoding: 'utf-8' })
42
- const parsed = utils.parseChannels(channelsXML)
43
- const channels = parsed.channels || []
82
+ const channels = utils.parseChannels(channelsXML)
44
83
 
45
84
  let programs = []
85
+ let i = 1
86
+ let days = options.days || 1
87
+ const total = channels.length * days
46
88
  for (let channel of channels) {
47
89
  await grabber
48
90
  .grab(channel, config, (data, err) => {
49
- console.log(
50
- ` ${config.site} - ${data.channel.xmltv_id} - ${data.date.format('MMM D, YYYY')} (${
51
- data.programs.length
52
- } programs)`
91
+ logger.info(
92
+ `[${i}/${total}] ${config.site} - ${data.channel.xmltv_id} - ${data.date.format(
93
+ 'MMM D, YYYY'
94
+ )} (${data.programs.length} programs)`
53
95
  )
54
96
 
55
- if (err) {
56
- console.log(` Error: ${err.message}`)
57
- }
97
+ if (err) logger.error(err.message)
98
+
99
+ if (i < total) i++
58
100
  })
59
101
  .then(results => {
60
102
  programs = programs.concat(results)
@@ -62,11 +104,11 @@ async function main() {
62
104
  }
63
105
 
64
106
  const xml = utils.convertToXMLTV({ config, channels, programs })
65
- const outputPath = config.output || 'guide.xml'
107
+ const outputPath = options.output || config.output || 'guide.xml'
66
108
  utils.writeToFile(outputPath, xml)
67
109
 
68
- console.log(`File '${outputPath}' successfully saved`)
69
- console.log('Finish')
110
+ logger.info(`File '${outputPath}' successfully saved`)
111
+ logger.info('Finish')
70
112
  }
71
113
 
72
114
  main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "epg-grabber",
3
- "version": "0.12.1",
3
+ "version": "0.15.1",
4
4
  "description": "Node.js CLI tool for grabbing EPG from different sites",
5
5
  "main": "src/index.js",
6
6
  "preferGlobal": true,
@@ -36,6 +36,7 @@
36
36
  "glob": "^7.1.6",
37
37
  "lodash.merge": "^4.6.2",
38
38
  "tough-cookie": "^4.0.0",
39
+ "winston": "^3.3.3",
39
40
  "xml-js": "^1.6.11"
40
41
  },
41
42
  "devDependencies": {
package/src/utils.js CHANGED
@@ -59,11 +59,12 @@ utils.parseChannels = function (xml) {
59
59
  const channel = el.attributes
60
60
  if (!el.elements) throw new Error(`Channel '${channel.xmltv_id}' has no valid name`)
61
61
  channel.name = el.elements.find(el => el.type === 'text').text
62
+ channel.site = channel.site || site
62
63
 
63
64
  return channel
64
65
  })
65
66
 
66
- return { site, channels }
67
+ return channels
67
68
  }
68
69
 
69
70
  utils.sleep = function (ms) {
@@ -122,8 +123,8 @@ utils.convertToXMLTV = function ({ channels, programs }) {
122
123
  const title = utils.escapeString(program.title)
123
124
  const description = utils.escapeString(program.description)
124
125
  const categories = Array.isArray(program.category) ? program.category : [program.category]
125
- const start = program.start ? dayjs.utc(program.start).format('YYYYMMDDHHmmss ZZ') : ''
126
- const stop = program.stop ? dayjs.utc(program.stop).format('YYYYMMDDHHmmss ZZ') : ''
126
+ const start = program.start ? dayjs.unix(program.start).utc().format('YYYYMMDDHHmmss ZZ') : ''
127
+ const stop = program.stop ? dayjs.unix(program.stop).utc().format('YYYYMMDDHHmmss ZZ') : ''
127
128
  const lang = program.lang || 'en'
128
129
  const icon = utils.escapeString(program.icon)
129
130
 
@@ -264,9 +265,16 @@ utils.parsePrograms = async function (data, config) {
264
265
  return programs
265
266
  .filter(i => i)
266
267
  .map(program => {
267
- program.channel = channel.xmltv_id
268
- program.lang = program.lang || channel.lang || config.lang || 'en'
269
- return program
268
+ return {
269
+ title: program.title,
270
+ description: program.description || null,
271
+ category: program.category || null,
272
+ icon: program.icon || null,
273
+ channel: channel.xmltv_id,
274
+ lang: program.lang || channel.lang || config.lang || 'en',
275
+ start: program.start ? dayjs(program.start).unix() : null,
276
+ stop: program.stop ? dayjs(program.stop).unix() : null
277
+ }
270
278
  })
271
279
  }
272
280
 
package/tests/bin.test.js CHANGED
@@ -1,5 +1,8 @@
1
1
  const { execSync } = require('child_process')
2
2
  const pwd = `${__dirname}/..`
3
+ import axios from 'axios'
4
+
5
+ jest.mock('axios')
3
6
 
4
7
  function stdoutResultTester(stdout) {
5
8
  return [`Finish`].every(val => {
@@ -19,6 +22,8 @@ it('can load config', () => {
19
22
  })
20
23
 
21
24
  it('can load mini config', () => {
25
+ axios.mockImplementation(() => Promise.resolve({ data: '' }))
26
+
22
27
  const result = execSync(
23
28
  `node ${pwd}/bin/epg-grabber.js \
24
29
  --config=tests/input/mini.config.js \
@@ -26,7 +31,8 @@ it('can load mini config', () => {
26
31
  --output=tests/output/mini.guide.xml \
27
32
  --lang=fr \
28
33
  --days=3 \
29
- --delay=0`,
34
+ --delay=0 \
35
+ --timeout=10000`,
30
36
  {
31
37
  encoding: 'utf8'
32
38
  }
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
2
  <site site="example.com">
3
3
  <channels>
4
- <channel xmltv_id="1TV.com" site="example.com" site_id="1" lang="fr" logo="https://example.com/logos/1TV.png">1 TV</channel>
4
+ <channel xmltv_id="1TV.com" site_id="1" lang="fr" logo="https://example.com/logos/1TV.png">1 TV</channel>
5
5
  <channel xmltv_id="2TV.com" site="example.com" site_id="2">2 TV</channel>
6
6
  </channels>
7
7
  </site>
@@ -28,9 +28,8 @@ it('can load valid config.js', () => {
28
28
 
29
29
  it('can parse valid channels.xml', () => {
30
30
  const file = fs.readFileSync('./tests/input/example.com.channels.xml', { encoding: 'utf-8' })
31
- const parsed = utils.parseChannels(file)
32
- expect(parsed.site).toBe('example.com')
33
- expect(parsed.channels).toEqual([
31
+ const channels = utils.parseChannels(file)
32
+ expect(channels).toEqual([
34
33
  {
35
34
  name: '1 TV',
36
35
  xmltv_id: '1TV.com',
@@ -52,22 +51,20 @@ it('can parse valid channels.xml', () => {
52
51
 
53
52
  it('can convert object to xmltv string', () => {
54
53
  const file = fs.readFileSync('./tests/input/example.com.channels.xml', { encoding: 'utf-8' })
55
- const parsed = utils.parseChannels(file)
56
- const channels = parsed.channels
54
+ const channels = utils.parseChannels(file)
57
55
  const programs = [
58
56
  {
59
57
  title: 'Program 1',
60
58
  description: 'Description for Program 1',
61
- start: '2021-03-19 06:00:00 +0000',
62
- stop: '2021-03-19 06:30:00 +0000',
59
+ start: 1616133600,
60
+ stop: 1616135400,
63
61
  category: 'Test',
64
62
  icon: 'https://example.com/images/Program1.png?x=шеллы&sid=777',
65
63
  channel: '1TV.com',
66
64
  lang: 'it'
67
65
  }
68
66
  ]
69
- const config = { site: 'example.com' }
70
- const output = utils.convertToXMLTV({ config, channels, programs })
67
+ const output = utils.convertToXMLTV({ channels, programs })
71
68
  expect(output).toBe(
72
69
  '<?xml version="1.0" encoding="UTF-8" ?><tv>\r\n<channel id="1TV.com"><display-name>1 TV</display-name><icon src="https://example.com/logos/1TV.png"/><url>https://example.com</url></channel>\r\n<channel id="2TV.com"><display-name>2 TV</display-name><url>https://example.com</url></channel>\r\n<programme start="20210319060000 +0000" stop="20210319063000 +0000" channel="1TV.com"><title lang="it">Program 1</title><desc lang="it">Description for Program 1</desc><category lang="it">Test</category><icon src="https://example.com/images/Program1.png?x=шеллы&amp;sid=777"/></programme>\r\n</tv>'
73
70
  )
@@ -87,8 +84,8 @@ it('can convert object to xmltv string without categories', () => {
87
84
  const programs = [
88
85
  {
89
86
  title: 'Program 1',
90
- start: '2021-03-19 06:00:00 +0000',
91
- stop: '2021-03-19 06:30:00 +0000',
87
+ start: 1616133600,
88
+ stop: 1616135400,
92
89
  channel: '1TV.com',
93
90
  lang: 'it'
94
91
  }
@@ -102,22 +99,20 @@ it('can convert object to xmltv string without categories', () => {
102
99
 
103
100
  it('can convert object to xmltv string with multiple categories', () => {
104
101
  const file = fs.readFileSync('./tests/input/example.com.channels.xml', { encoding: 'utf-8' })
105
- const parsed = utils.parseChannels(file)
106
- const channels = parsed.channels
102
+ const channels = utils.parseChannels(file)
107
103
  const programs = [
108
104
  {
109
105
  title: 'Program 1',
110
106
  description: 'Description for Program 1',
111
- start: '2021-03-19 06:00:00 +0000',
112
- stop: '2021-03-19 06:30:00 +0000',
107
+ start: 1616133600,
108
+ stop: 1616135400,
113
109
  category: ['Test1', 'Test2'],
114
110
  icon: 'https://example.com/images/Program1.png?x=шеллы&sid=777',
115
111
  channel: '1TV.com',
116
112
  lang: 'it'
117
113
  }
118
114
  ]
119
- const config = { site: 'example.com' }
120
- const output = utils.convertToXMLTV({ config, channels, programs })
115
+ const output = utils.convertToXMLTV({ channels, programs })
121
116
  expect(output).toBe(
122
117
  '<?xml version="1.0" encoding="UTF-8" ?><tv>\r\n<channel id="1TV.com"><display-name>1 TV</display-name><icon src="https://example.com/logos/1TV.png"/><url>https://example.com</url></channel>\r\n<channel id="2TV.com"><display-name>2 TV</display-name><url>https://example.com</url></channel>\r\n<programme start="20210319060000 +0000" stop="20210319063000 +0000" channel="1TV.com"><title lang="it">Program 1</title><desc lang="it">Description for Program 1</desc><category lang="it">Test1</category><category lang="it">Test2</category><icon src="https://example.com/images/Program1.png?x=шеллы&amp;sid=777"/></programme>\r\n</tv>'
123
118
  )