epg-grabber 0.9.1 → 0.10.0

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.
@@ -0,0 +1,70 @@
1
+ #! /usr/bin/env node
2
+
3
+ const { Command } = require('commander')
4
+ const program = new Command()
5
+ const path = require('path')
6
+ const grabber = require('../src/index')
7
+ const utils = require('../src/utils')
8
+ const { name, version, description } = require('../package.json')
9
+ const merge = require('lodash.merge')
10
+
11
+ program
12
+ .name(name)
13
+ .version(version, '-v, --version')
14
+ .description(description)
15
+ .requiredOption('-c, --config <config>', 'Path to [site].config.js file')
16
+ .option('-o, --output <output>', 'Path to output file')
17
+ .option('--channels <channels>', 'Path to channels.xml file')
18
+ .option('--lang <lang>', 'Set default language for all programs')
19
+ .option('--days <days>', 'Number of days for which to grab the program', parseInteger)
20
+ .option('--delay <delay>', 'Delay between requests (in mileseconds)', parseInteger)
21
+ .option('--debug', 'Enable debug mode', false)
22
+ .parse(process.argv)
23
+
24
+ const options = program.opts()
25
+
26
+ async function main() {
27
+ console.log('\r\nStarting...')
28
+
29
+ console.log(`Loading '${options.config}'...`)
30
+ let config = require(path.resolve(options.config))
31
+ config = merge(config, options)
32
+
33
+ if (options.channels) config.channels = options.channels
34
+ else if (config.channels)
35
+ config.channels = path.join(path.dirname(options.config), config.channels)
36
+ else throw new Error("The required 'channels' property is missing")
37
+
38
+ console.log('Parsing:')
39
+ let programs = []
40
+ const channels = utils.parseChannels(config.channels)
41
+ for (let channel of channels) {
42
+ await grabber
43
+ .grab(channel, config, (data, err) => {
44
+ console.log(
45
+ ` ${config.site} - ${data.channel.xmltv_id} - ${data.date.format('MMM D, YYYY')} (${
46
+ data.programs.length
47
+ } programs)`
48
+ )
49
+
50
+ if (err) {
51
+ console.log(` Error: ${err.message}`)
52
+ }
53
+ })
54
+ .then(results => {
55
+ programs = programs.concat(results)
56
+ })
57
+ }
58
+
59
+ const xml = utils.convertToXMLTV({ config, channels, programs })
60
+ utils.writeToFile(config.output, xml)
61
+
62
+ console.log(`File '${config.output}' successfully saved`)
63
+ console.log('Finish')
64
+ }
65
+
66
+ main()
67
+
68
+ function parseInteger(val) {
69
+ return val ? parseInt(val) : null
70
+ }
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "epg-grabber",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Node.js CLI tool for grabbing EPG from different sites",
5
5
  "main": "src/index.js",
6
6
  "preferGlobal": true,
7
7
  "bin": {
8
- "epg-grabber": "src/index.js"
8
+ "epg-grabber": "bin/epg-grabber.js"
9
9
  },
10
10
  "scripts": {
11
11
  "lint": "npx eslint ./src/**/*.js",
@@ -30,6 +30,7 @@
30
30
  "dependencies": {
31
31
  "axios": "^0.21.1",
32
32
  "axios-cookiejar-support": "^1.0.1",
33
+ "axios-mock-adapter": "^1.20.0",
33
34
  "commander": "^7.1.0",
34
35
  "dayjs": "^1.10.4",
35
36
  "glob": "^7.1.6",
@@ -43,6 +44,9 @@
43
44
  "babel-jest": "^26.6.3",
44
45
  "eslint": "^7.22.0",
45
46
  "jest": "^26.6.3",
46
- "jest-mock-axios": "^4.3.0"
47
+ "jest-mock-axios": "^4.4.1"
48
+ },
49
+ "jest": {
50
+ "testEnvironment": "node"
47
51
  }
48
52
  }
package/src/index.js CHANGED
@@ -1,74 +1,37 @@
1
- #! /usr/bin/env node
2
-
3
- const { Command } = require('commander')
4
- const program = new Command()
5
1
  const utils = require('./utils')
6
- const { name, version, description } = require('../package.json')
7
-
8
- program
9
- .name(name)
10
- .version(version, '-v, --version')
11
- .description(description)
12
- .option('-c, --config <config>', 'Path to [site].config.js file')
13
- .option('-o, --output <output>', 'Path to output file', 'guide.xml')
14
- .option('--channels <channels>', 'Path to channels.xml file')
15
- .option('--lang <lang>', 'Set default language for all programs', 'en')
16
- .option('--days <days>', 'Number of days for which to grab the program', 1)
17
- .option('--delay <delay>', 'Delay between requests (in mileseconds)', 3000)
18
- .option('--debug', 'Enable debug mode', false)
19
- .parse(process.argv)
20
-
21
- const options = program.opts()
22
- const config = utils.loadConfig(options)
23
-
24
- async function main() {
25
- console.log('\r\nStarting...')
26
2
 
27
- const channels = utils.parseChannels(config.channels)
28
- const utcDate = utils.getUTCDate()
29
- const dates = Array.from({ length: config.days }, (_, i) => utcDate.add(i, 'd'))
3
+ module.exports = {
4
+ grab: async function (channel, config, cb) {
5
+ config = utils.loadConfig(config)
6
+ channel.lang = channel.lang || config.lang || null
30
7
 
31
- const queue = []
32
- channels.forEach(channel => {
8
+ const utcDate = utils.getUTCDate()
9
+ const dates = Array.from({ length: config.days }, (_, i) => utcDate.add(i, 'd'))
10
+ const queue = []
33
11
  dates.forEach(date => {
34
12
  queue.push({ date, channel })
35
13
  })
36
- })
37
14
 
38
- let programs = []
39
- console.log('Parsing:')
40
- for (let item of queue) {
41
- if (options.debug) console.time(' Response Time')
42
- await utils
43
- .buildRequest(item, config)
44
- .then(utils.fetchData)
45
- .then(async response => {
46
- if (options.debug) console.timeEnd(' Response Time')
47
- if (options.debug) console.time(' Parsing Time')
48
- const results = await utils.parseResponse(item, response, config)
49
- if (options.debug) console.timeEnd(' Parsing Time')
50
- programs = programs.concat(results)
51
- })
52
- .then(utils.sleep(config.delay))
53
- .catch(err => {
54
- console.log(
55
- ` ${config.site} - ${item.channel.xmltv_id} - ${item.date.format(
56
- 'MMM D, YYYY'
57
- )} (0 programs)`
58
- )
59
- console.log(` Error: ${err.message}`)
60
- if (options.debug) {
61
- console.timeEnd(' Response Time')
62
- console.timeEnd(' Parsing Time')
63
- }
64
- })
65
- }
66
-
67
- const xml = utils.convertToXMLTV({ config, channels, programs })
68
- utils.writeToFile(config.output, xml)
69
-
70
- console.log(`File '${config.output}' successfully saved`)
71
- console.log('Finish')
15
+ let programs = []
16
+ for (let item of queue) {
17
+ await utils
18
+ .buildRequest(item, config)
19
+ .then(request => utils.fetchData(request))
20
+ .then(response => utils.parseResponse(item, response, config))
21
+ .then(results => {
22
+ item.programs = results
23
+ cb(item, null)
24
+ programs = programs.concat(results)
25
+ })
26
+ .catch(err => {
27
+ item.programs = []
28
+ cb(item, err)
29
+ })
30
+
31
+ await utils.sleep(config.delay)
32
+ }
33
+
34
+ return programs
35
+ },
36
+ convertToXMLTV: utils.convertToXMLTV
72
37
  }
73
-
74
- main()
package/src/utils.js CHANGED
@@ -14,18 +14,7 @@ const utils = {}
14
14
  const defaultUserAgent =
15
15
  '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'
16
16
 
17
- utils.loadConfig = function (options) {
18
- const file = options.config
19
- if (!file) throw new Error('Path to [site].config.js is missing')
20
- console.log(`Loading '${file}'...`)
21
-
22
- const configPath = path.resolve(file)
23
- const config = require(configPath)
24
-
25
- if (options.channels) config.channels = options.channels
26
- else if (config.channels) config.channels = path.join(path.dirname(file), config.channels)
27
- else throw new Error("The required 'channels' property is missing")
28
-
17
+ utils.loadConfig = function (config) {
29
18
  if (!config.site) throw new Error("The required 'site' property is missing")
30
19
  if (!config.url) throw new Error("The required 'url' property is missing")
31
20
  if (typeof config.url !== 'function' && typeof config.url !== 'string')
@@ -37,10 +26,10 @@ utils.loadConfig = function (options) {
37
26
  throw new Error("The 'logo' property should return the function")
38
27
 
39
28
  const defaultConfig = {
40
- days: options.days ? parseInt(options.days) : 1,
41
- lang: options.lang || 'en',
42
- delay: options.delay ? parseInt(options.delay) : 3000,
43
- output: options.output || 'guide.xml',
29
+ days: 1,
30
+ lang: 'en',
31
+ delay: 3000,
32
+ output: 'guide.xml',
44
33
  request: {
45
34
  method: 'GET',
46
35
  maxContentLength: 5 * 1024 * 1024,
@@ -60,8 +49,7 @@ utils.parseChannels = function (filename) {
60
49
 
61
50
  const xml = fs.readFileSync(path.resolve(filename), { encoding: 'utf-8' })
62
51
  const result = convert.xml2js(xml)
63
- const site = result.elements.find(el => el.name === 'site')
64
- const channels = site.elements.find(el => el.name === 'channels')
52
+ const channels = result.elements.find(el => el.name === 'channels')
65
53
 
66
54
  return channels.elements
67
55
  .filter(el => el.name === 'channel')
@@ -69,16 +57,13 @@ utils.parseChannels = function (filename) {
69
57
  const channel = el.attributes
70
58
  if (!el.elements) throw new Error(`Channel '${channel.xmltv_id}' has no valid name`)
71
59
  channel.name = el.elements.find(el => el.type === 'text').text
72
- channel.site = channel.site || site.attributes.site
73
60
 
74
61
  return channel
75
62
  })
76
63
  }
77
64
 
78
65
  utils.sleep = function (ms) {
79
- return function (x) {
80
- return new Promise(resolve => setTimeout(() => resolve(x), ms))
81
- }
66
+ return new Promise(resolve => setTimeout(resolve, ms))
82
67
  }
83
68
 
84
69
  utils.escapeString = function (string, defaultValue = '') {
@@ -109,18 +94,18 @@ utils.escapeString = function (string, defaultValue = '') {
109
94
  .trim()
110
95
  }
111
96
 
112
- utils.convertToXMLTV = function ({ config, channels, programs }) {
113
- const url = config.site ? 'https://' + config.site : null
97
+ utils.convertToXMLTV = function ({ channels, programs }) {
114
98
  let output = `<?xml version="1.0" encoding="UTF-8" ?><tv>\r\n`
115
99
  for (let channel of channels) {
116
- const id = this.escapeString(channel['xmltv_id'])
117
- const displayName = this.escapeString(channel.name)
100
+ const id = utils.escapeString(channel['xmltv_id'])
101
+ const displayName = utils.escapeString(channel.name)
118
102
  output += `<channel id="${id}"><display-name>${displayName}</display-name>`
119
103
  if (channel.logo) {
120
- const logo = this.escapeString(channel.logo)
104
+ const logo = utils.escapeString(channel.logo)
121
105
  output += `<icon src="${logo}"/>`
122
106
  }
123
- if (url) {
107
+ if (channel.site) {
108
+ const url = channel.site ? 'https://' + channel.site : null
124
109
  output += `<url>${url}</url>`
125
110
  }
126
111
  output += `</channel>\r\n`
@@ -129,14 +114,14 @@ utils.convertToXMLTV = function ({ config, channels, programs }) {
129
114
  for (let program of programs) {
130
115
  if (!program) continue
131
116
 
132
- const channel = this.escapeString(program.channel)
133
- const title = this.escapeString(program.title)
134
- const description = this.escapeString(program.description)
135
- const category = this.escapeString(program.category)
117
+ const channel = utils.escapeString(program.channel)
118
+ const title = utils.escapeString(program.title)
119
+ const description = utils.escapeString(program.description)
120
+ const category = utils.escapeString(program.category)
136
121
  const start = program.start ? dayjs.utc(program.start).format('YYYYMMDDHHmmss ZZ') : ''
137
122
  const stop = program.stop ? dayjs.utc(program.stop).format('YYYYMMDDHHmmss ZZ') : ''
138
- const lang = program.lang || config.lang
139
- const icon = this.escapeString(program.icon)
123
+ const lang = program.lang || 'en'
124
+ const icon = utils.escapeString(program.icon)
140
125
 
141
126
  if (start && title) {
142
127
  output += `<programme start="${start}"`
@@ -184,6 +169,10 @@ utils.buildRequest = async function (item, config) {
184
169
  request.url = await utils.getRequestUrl(item, config)
185
170
  request.data = await utils.getRequestData(item, config)
186
171
 
172
+ if (config.debug) {
173
+ console.log('Request:', JSON.stringify(request, null, 2))
174
+ }
175
+
187
176
  return request
188
177
  }
189
178
 
@@ -199,7 +188,7 @@ utils.getRequestHeaders = async function (item, config) {
199
188
  }
200
189
  return headers
201
190
  }
202
- return config.request.headers
191
+ return config.request.headers || null
203
192
  }
204
193
 
205
194
  utils.getRequestData = async function (item, config) {
@@ -210,7 +199,7 @@ utils.getRequestData = async function (item, config) {
210
199
  }
211
200
  return data
212
201
  }
213
- return config.request.data
202
+ return config.request.data || null
214
203
  }
215
204
 
216
205
  utils.getRequestUrl = async function (item, config) {
@@ -229,28 +218,20 @@ utils.getUTCDate = function () {
229
218
  }
230
219
 
231
220
  utils.parseResponse = async (item, response, config) => {
232
- const options = merge(item, config, {
221
+ const data = merge(item, config, {
233
222
  content: response.data.toString(),
234
223
  buffer: response.data
235
224
  })
236
225
 
237
226
  if (!item.channel.logo && config.logo) {
238
- item.channel.logo = await utils.loadLogo(options, config)
227
+ item.channel.logo = await utils.loadLogo(data, config)
239
228
  }
240
229
 
241
- const parsed = await utils.parsePrograms(options, config)
242
-
243
- console.log(
244
- ` ${config.site} - ${item.channel.xmltv_id} - ${item.date.format('MMM D, YYYY')} (${
245
- parsed.length
246
- } programs)`
247
- )
248
-
249
- return parsed
230
+ return await utils.parsePrograms(data, config)
250
231
  }
251
232
 
252
- utils.parsePrograms = async function (options, config) {
253
- let programs = config.parser(options)
233
+ utils.parsePrograms = async function (data, config) {
234
+ let programs = config.parser(data)
254
235
 
255
236
  if (this.isPromise(programs)) {
256
237
  programs = await programs
@@ -260,12 +241,12 @@ utils.parsePrograms = async function (options, config) {
260
241
  throw new Error('Parser should return an array')
261
242
  }
262
243
 
263
- const channel = options.channel
244
+ const channel = data.channel
264
245
  return programs
265
246
  .filter(i => i)
266
247
  .map(program => {
267
248
  program.channel = channel.xmltv_id
268
- program.lang = program.lang || channel.lang || undefined
249
+ program.lang = program.lang || channel.lang || config.lang || 'en'
269
250
  return program
270
251
  })
271
252
  }
@@ -0,0 +1,37 @@
1
+ const { execSync } = require('child_process')
2
+ const pwd = `${__dirname}/..`
3
+
4
+ function stdoutResultTester(stdout) {
5
+ return [`Finish`].every(val => {
6
+ return RegExp(val).test(stdout)
7
+ })
8
+ }
9
+
10
+ it('can load config', () => {
11
+ const result = execSync(
12
+ `node ${pwd}/bin/epg-grabber.js --config=tests/input/example.com.config.js`,
13
+ {
14
+ encoding: 'utf8'
15
+ }
16
+ )
17
+
18
+ expect(stdoutResultTester(result)).toBe(true)
19
+ })
20
+
21
+ it('can load mini config', () => {
22
+ const result = execSync(
23
+ `node ${pwd}/bin/epg-grabber.js \
24
+ --config=tests/input/mini.config.js \
25
+ --channels=tests/input/example.com.channels.xml \
26
+ --output=tests/output/mini.guide.xml \
27
+ --lang=fr \
28
+ --days=3 \
29
+ --delay=5000`,
30
+ {
31
+ encoding: 'utf8'
32
+ }
33
+ )
34
+
35
+ expect(stdoutResultTester(result)).toBe(true)
36
+ expect(result.includes("File 'tests/output/mini.guide.xml' successfully saved")).toBe(true)
37
+ })
@@ -1,34 +1,45 @@
1
- const { execSync } = require('child_process')
2
- const pwd = `${__dirname}/..`
1
+ /**
2
+ * @jest-environment node
3
+ */
3
4
 
4
- function stdoutResultTester(stdout) {
5
- return [`Finish`].every(val => {
6
- return RegExp(val).test(stdout)
7
- })
8
- }
5
+ import grabber from '../src/index'
6
+ import utils from '../src/utils'
7
+ import axios from 'axios'
8
+ import path from 'path'
9
9
 
10
- it('can load config', () => {
11
- const result = execSync(`node ${pwd}/src/index.js --config=tests/input/example.com.config.js`, {
12
- encoding: 'utf8'
13
- })
10
+ jest.mock('axios')
14
11
 
15
- expect(stdoutResultTester(result)).toBe(true)
16
- })
17
-
18
- it('can load mini config', () => {
19
- const result = execSync(
20
- `node ${pwd}/src/index.js \
21
- --config=tests/input/mini.config.js \
22
- --channels=tests/input/example.com.channels.xml \
23
- --output=tests/output/mini.guide.xml \
24
- --lang=fr \
25
- --days=3 \
26
- --delay=5000`,
27
- {
28
- encoding: 'utf8'
12
+ it('can grab single channel programs', done => {
13
+ const data = {
14
+ data: {
15
+ toString: () => 'string'
29
16
  }
30
- )
17
+ }
18
+ axios.mockImplementation(() => Promise.resolve(data))
31
19
 
32
- expect(stdoutResultTester(result)).toBe(true)
33
- expect(result.includes("File 'tests/output/mini.guide.xml' successfully saved")).toBe(true)
20
+ const config = utils.loadConfig(require(path.resolve('./tests/input/mini.config.js')))
21
+ const channel = {
22
+ site: 'example.com',
23
+ site_id: '1',
24
+ xmltv_id: '1TV.fr',
25
+ lang: 'fr',
26
+ name: '1TV'
27
+ }
28
+ grabber
29
+ .grab(channel, config, (data, err) => {
30
+ if (err) {
31
+ console.log(` Error: ${err.message}`)
32
+ done()
33
+ } else {
34
+ console.log(
35
+ ` ${data.channel.site} - ${data.channel.xmltv_id} - ${data.date.format(
36
+ 'MMM D, YYYY'
37
+ )} (${data.programs.length} programs)`
38
+ )
39
+ }
40
+ })
41
+ .then(programs => {
42
+ expect(programs.length).toBe(0)
43
+ done()
44
+ })
34
45
  })
@@ -1,7 +1,5 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
- <site site="example.com">
3
- <channels>
4
- <channel site_id="1" xmltv_id="1TV.com" lang="fr" logo="https://example.com/logos/1TV.png">1 TV</channel>
5
- <channel site_id="2" xmltv_id="2TV.com">2 TV</channel>
6
- </channels>
7
- </site>
2
+ <channels>
3
+ <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="2TV.com" site="example.com" site_id="2">2 TV</channel>
5
+ </channels>
@@ -1,5 +1,5 @@
1
1
  module.exports = {
2
2
  site: 'example.com',
3
- url: () => 'http://example.com/20210319/1tv.json',
3
+ url: 'http://example.com/20210319/1tv.json',
4
4
  parser: () => []
5
5
  }
@@ -1,14 +1,13 @@
1
1
  import mockAxios from 'jest-mock-axios'
2
2
  import utils from '../src/utils'
3
+ import path from 'path'
3
4
 
4
5
  it('can load valid config.js', () => {
5
- const config = utils.loadConfig({ config: './tests/input/example.com.config.js' })
6
+ const config = utils.loadConfig(require(path.resolve('./tests/input/example.com.config.js')))
6
7
  expect(config).toMatchObject({
7
- channels: 'tests/input/example.com.channels.xml',
8
8
  days: 1,
9
9
  delay: 3000,
10
10
  lang: 'en',
11
- output: 'tests/output/guide.xml',
12
11
  site: 'example.com'
13
12
  })
14
13
  expect(config.request).toMatchObject({
@@ -116,7 +115,7 @@ it('can fetch data', async () => {
116
115
  })
117
116
 
118
117
  it('can build request async', async () => {
119
- const config = utils.loadConfig({ config: './tests/input/async.config.js' })
118
+ const config = utils.loadConfig(require(path.resolve('./tests/input/async.config.js')))
120
119
  return utils.buildRequest({}, config).then(request => {
121
120
  expect(request).toMatchObject({
122
121
  data: { accountID: '123' },
@@ -137,14 +136,14 @@ it('can build request async', async () => {
137
136
  })
138
137
 
139
138
  it('can load logo async', async () => {
140
- const config = utils.loadConfig({ config: './tests/input/async.config.js' })
139
+ const config = utils.loadConfig(require(path.resolve('./tests/input/async.config.js')))
141
140
  return utils.loadLogo({}, config).then(logo => {
142
141
  expect(logo).toBe('http://example.com/logos/1TV.png?x=шеллы&sid=777')
143
142
  })
144
143
  })
145
144
 
146
145
  it('can parse programs async', async () => {
147
- const config = utils.loadConfig({ config: './tests/input/async.config.js' })
146
+ const config = utils.loadConfig(require(path.resolve('./tests/input/async.config.js')))
148
147
  return utils
149
148
  .parsePrograms({ channel: { xmltv_id: '1tv', lang: 'en' } }, config)
150
149
  .then(programs => {