epg-grabber 0.23.0 → 0.25.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 +9 -0
- package/bin/epg-grabber.js +24 -5
- package/package.json +3 -1
- package/src/index.js +19 -13
- package/src/utils.js +70 -35
- package/tests/bin.test.js +16 -0
- package/tests/index.test.js +5 -3
- package/tests/utils.test.js +2 -1
package/README.md
CHANGED
|
@@ -80,6 +80,8 @@ Arguments:
|
|
|
80
80
|
- `--days`: number of days for which to grab the program (default: 1)
|
|
81
81
|
- `--delay`: delay between requests (default: 3000)
|
|
82
82
|
- `--timeout`: set a timeout for each request (default: 5000)
|
|
83
|
+
- `--cache-max-age`: maximum time for storing each request in milliseconds (default: 0)
|
|
84
|
+
- `--gzip`: compress the output (default: false)
|
|
83
85
|
- `--debug`: enable debug mode (default: false)
|
|
84
86
|
- `--curl`: display current request as CURL (default: false)
|
|
85
87
|
- `--log`: path to log file (optional)
|
|
@@ -100,6 +102,13 @@ module.exports = {
|
|
|
100
102
|
|
|
101
103
|
method: 'GET',
|
|
102
104
|
timeout: 5000,
|
|
105
|
+
cache: { // cache options (details: https://github.com/RasCarlito/axios-cache-adapter#options)
|
|
106
|
+
maxAge: 0,
|
|
107
|
+
readHeaders: false,
|
|
108
|
+
exclude: {
|
|
109
|
+
query: false
|
|
110
|
+
}
|
|
111
|
+
},
|
|
103
112
|
|
|
104
113
|
/**
|
|
105
114
|
* @param {object} context
|
package/bin/epg-grabber.js
CHANGED
|
@@ -4,10 +4,11 @@ const { Command } = require('commander')
|
|
|
4
4
|
const program = new Command()
|
|
5
5
|
const fs = require('fs')
|
|
6
6
|
const path = require('path')
|
|
7
|
-
const
|
|
7
|
+
const EPGGrabber = 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')
|
|
11
|
+
const { gzip } = require('node-gzip')
|
|
11
12
|
const { createLogger, format, transports } = require('winston')
|
|
12
13
|
const { combine, timestamp, printf } = format
|
|
13
14
|
|
|
@@ -22,6 +23,12 @@ program
|
|
|
22
23
|
.option('--days <days>', 'Number of days for which to grab the program', parseInteger, 1)
|
|
23
24
|
.option('--delay <delay>', 'Delay between requests (in mileseconds)', parseInteger)
|
|
24
25
|
.option('--timeout <timeout>', 'Set a timeout for each request (in mileseconds)', parseInteger)
|
|
26
|
+
.option(
|
|
27
|
+
'--cache-max-age <cacheMaxAge>',
|
|
28
|
+
'Maximum time for storing each request (in milliseconds)',
|
|
29
|
+
parseInteger
|
|
30
|
+
)
|
|
31
|
+
.option('--gzip', 'Compress the output', false)
|
|
25
32
|
.option('--debug', 'Enable debug mode', false)
|
|
26
33
|
.option('--curl', 'Display request as CURL', false)
|
|
27
34
|
.option('--log <log>', 'Path to log file')
|
|
@@ -65,11 +72,15 @@ async function main() {
|
|
|
65
72
|
config = merge(config, {
|
|
66
73
|
days: options.days,
|
|
67
74
|
debug: options.debug,
|
|
75
|
+
gzip: options.gzip,
|
|
68
76
|
curl: options.curl,
|
|
69
77
|
lang: options.lang,
|
|
70
78
|
delay: options.delay,
|
|
71
79
|
request: {
|
|
72
|
-
timeout: options.timeout
|
|
80
|
+
timeout: options.timeout,
|
|
81
|
+
cache: {
|
|
82
|
+
maxAge: options.cacheMaxAge
|
|
83
|
+
}
|
|
73
84
|
}
|
|
74
85
|
})
|
|
75
86
|
|
|
@@ -89,10 +100,11 @@ async function main() {
|
|
|
89
100
|
const total = channels.length * days
|
|
90
101
|
const utcDate = utils.getUTCDate()
|
|
91
102
|
const dates = Array.from({ length: config.days }, (_, i) => utcDate.add(i, 'd'))
|
|
103
|
+
const grabber = new EPGGrabber(config)
|
|
92
104
|
for (let channel of channels) {
|
|
93
105
|
for (let date of dates) {
|
|
94
106
|
await grabber
|
|
95
|
-
.grab(channel, date,
|
|
107
|
+
.grab(channel, date, (data, err) => {
|
|
96
108
|
logger.info(
|
|
97
109
|
`[${i}/${total}] ${config.site} - ${data.channel.xmltv_id} - ${data.date.format(
|
|
98
110
|
'MMM D, YYYY'
|
|
@@ -110,8 +122,15 @@ async function main() {
|
|
|
110
122
|
}
|
|
111
123
|
|
|
112
124
|
const xml = utils.convertToXMLTV({ config, channels, programs })
|
|
113
|
-
|
|
114
|
-
|
|
125
|
+
let outputPath = options.output || config.output
|
|
126
|
+
if (options.gzip) {
|
|
127
|
+
outputPath = outputPath || 'guide.xml.gz'
|
|
128
|
+
const compressed = await gzip(xml)
|
|
129
|
+
utils.writeToFile(outputPath, compressed)
|
|
130
|
+
} else {
|
|
131
|
+
outputPath = outputPath || 'guide.xml'
|
|
132
|
+
utils.writeToFile(outputPath, xml)
|
|
133
|
+
}
|
|
115
134
|
|
|
116
135
|
logger.info(`File '${outputPath}' successfully saved`)
|
|
117
136
|
logger.info('Finish')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "epg-grabber",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.1",
|
|
4
4
|
"description": "Node.js CLI tool for grabbing EPG from different sites",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"preferGlobal": true,
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"axios": "^0.21.1",
|
|
32
|
+
"axios-cache-adapter": "^2.7.3",
|
|
32
33
|
"axios-cookiejar-support": "^1.0.1",
|
|
33
34
|
"axios-mock-adapter": "^1.20.0",
|
|
34
35
|
"commander": "^7.1.0",
|
|
@@ -36,6 +37,7 @@
|
|
|
36
37
|
"dayjs": "^1.10.4",
|
|
37
38
|
"glob": "^7.1.6",
|
|
38
39
|
"lodash": "^4.17.21",
|
|
40
|
+
"node-gzip": "^1.1.2",
|
|
39
41
|
"tough-cookie": "^4.0.0",
|
|
40
42
|
"winston": "^3.3.3",
|
|
41
43
|
"xml-js": "^1.6.11"
|
package/src/index.js
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
const utils = require('./utils')
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
class EPGGrabber {
|
|
4
|
+
constructor(config = {}) {
|
|
5
|
+
this.config = utils.loadConfig(config)
|
|
6
|
+
this.client = utils.createClient(config)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async grab(channel, date, cb = () => {}) {
|
|
5
10
|
date = typeof date === 'string' ? utils.getUTCDate(date) : date
|
|
6
|
-
|
|
7
|
-
channel.lang = channel.lang || config.lang || null
|
|
11
|
+
channel.lang = channel.lang || this.config.lang || null
|
|
8
12
|
|
|
9
13
|
let programs = []
|
|
10
|
-
|
|
11
14
|
const item = { date, channel }
|
|
12
15
|
await utils
|
|
13
|
-
.buildRequest(item, config)
|
|
14
|
-
.then(request => utils.fetchData(request))
|
|
15
|
-
.then(response => utils.parseResponse(item, response, config))
|
|
16
|
+
.buildRequest(item, this.config)
|
|
17
|
+
.then(request => utils.fetchData(this.client, request))
|
|
18
|
+
.then(response => utils.parseResponse(item, response, this.config))
|
|
16
19
|
.then(results => {
|
|
17
20
|
item.programs = results
|
|
18
21
|
cb(item, null)
|
|
@@ -20,16 +23,19 @@ module.exports = {
|
|
|
20
23
|
})
|
|
21
24
|
.catch(error => {
|
|
22
25
|
item.programs = []
|
|
23
|
-
if (config.debug) {
|
|
26
|
+
if (this.config.debug) {
|
|
24
27
|
console.log('Error:', JSON.stringify(error, null, 2))
|
|
25
28
|
}
|
|
26
29
|
cb(item, error)
|
|
27
30
|
})
|
|
28
31
|
|
|
29
|
-
await utils.sleep(config.delay)
|
|
32
|
+
await utils.sleep(this.config.delay)
|
|
30
33
|
|
|
31
34
|
return programs
|
|
32
|
-
}
|
|
33
|
-
convertToXMLTV: utils.convertToXMLTV,
|
|
34
|
-
parseChannels: utils.parseChannels
|
|
35
|
+
}
|
|
35
36
|
}
|
|
37
|
+
|
|
38
|
+
EPGGrabber.convertToXMLTV = utils.convertToXMLTV
|
|
39
|
+
EPGGrabber.parseChannels = utils.parseChannels
|
|
40
|
+
|
|
41
|
+
module.exports = EPGGrabber
|
package/src/utils.js
CHANGED
|
@@ -3,6 +3,7 @@ const { padStart } = require('lodash')
|
|
|
3
3
|
const path = require('path')
|
|
4
4
|
const axios = require('axios').default
|
|
5
5
|
const axiosCookieJarSupport = require('axios-cookiejar-support').default
|
|
6
|
+
const axiosCacheAdapter = require('axios-cache-adapter')
|
|
6
7
|
const tough = require('tough-cookie')
|
|
7
8
|
const convert = require('xml-js')
|
|
8
9
|
const { merge } = require('lodash')
|
|
@@ -39,13 +40,66 @@ utils.loadConfig = function (config) {
|
|
|
39
40
|
timeout: 5000,
|
|
40
41
|
withCredentials: true,
|
|
41
42
|
jar: new tough.CookieJar(),
|
|
42
|
-
responseType: 'arraybuffer'
|
|
43
|
+
responseType: 'arraybuffer',
|
|
44
|
+
cache: {
|
|
45
|
+
readHeaders: false,
|
|
46
|
+
exclude: {
|
|
47
|
+
query: false
|
|
48
|
+
},
|
|
49
|
+
maxAge: 0
|
|
50
|
+
}
|
|
43
51
|
}
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
return merge(defaultConfig, config)
|
|
47
55
|
}
|
|
48
56
|
|
|
57
|
+
utils.createClient = function (config) {
|
|
58
|
+
const client = axiosCacheAdapter.setup()
|
|
59
|
+
client.interceptors.request.use(
|
|
60
|
+
function (request) {
|
|
61
|
+
if (config.debug) {
|
|
62
|
+
console.log('Request:', JSON.stringify(request, null, 2))
|
|
63
|
+
}
|
|
64
|
+
return request
|
|
65
|
+
},
|
|
66
|
+
function (error) {
|
|
67
|
+
return Promise.reject(error)
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
client.interceptors.response.use(
|
|
71
|
+
function (response) {
|
|
72
|
+
if (config.debug) {
|
|
73
|
+
const data =
|
|
74
|
+
utils.isObject(response.data) || Array.isArray(response.data)
|
|
75
|
+
? JSON.stringify(response.data)
|
|
76
|
+
: response.data.toString()
|
|
77
|
+
console.log(
|
|
78
|
+
'Response:',
|
|
79
|
+
JSON.stringify(
|
|
80
|
+
{
|
|
81
|
+
headers: response.headers,
|
|
82
|
+
data,
|
|
83
|
+
fromCache: response.request.fromCache === true
|
|
84
|
+
},
|
|
85
|
+
null,
|
|
86
|
+
2
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
clearTimeout(timeout)
|
|
92
|
+
return response
|
|
93
|
+
},
|
|
94
|
+
function (error) {
|
|
95
|
+
clearTimeout(timeout)
|
|
96
|
+
return Promise.reject(error)
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return client
|
|
101
|
+
}
|
|
102
|
+
|
|
49
103
|
utils.parseChannels = function (xml) {
|
|
50
104
|
const result = convert.xml2js(xml)
|
|
51
105
|
const siteTag = result.elements.find(el => el.name === 'site') || {}
|
|
@@ -205,10 +259,6 @@ utils.buildRequest = async function (item, config) {
|
|
|
205
259
|
request.data = await utils.getRequestData(item, config)
|
|
206
260
|
request.cancelToken = source.token
|
|
207
261
|
|
|
208
|
-
if (config.debug) {
|
|
209
|
-
console.log('Request:', JSON.stringify(request, null, 2))
|
|
210
|
-
}
|
|
211
|
-
|
|
212
262
|
if (config.curl) {
|
|
213
263
|
const curl = CurlGenerator({
|
|
214
264
|
url: request.url,
|
|
@@ -222,19 +272,8 @@ utils.buildRequest = async function (item, config) {
|
|
|
222
272
|
return request
|
|
223
273
|
}
|
|
224
274
|
|
|
225
|
-
utils.fetchData = function (request) {
|
|
226
|
-
|
|
227
|
-
function (response) {
|
|
228
|
-
clearTimeout(timeout)
|
|
229
|
-
return response
|
|
230
|
-
},
|
|
231
|
-
function (error) {
|
|
232
|
-
clearTimeout(timeout)
|
|
233
|
-
return Promise.reject(error)
|
|
234
|
-
}
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
return axios(request)
|
|
275
|
+
utils.fetchData = function (client, request) {
|
|
276
|
+
return client(request)
|
|
238
277
|
}
|
|
239
278
|
|
|
240
279
|
utils.getRequestHeaders = async function (item, config) {
|
|
@@ -277,24 +316,16 @@ utils.getUTCDate = function (d = null) {
|
|
|
277
316
|
}
|
|
278
317
|
|
|
279
318
|
utils.parseResponse = async (item, response, config) => {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
headers: response.headers,
|
|
286
|
-
data: response.data.toString()
|
|
287
|
-
},
|
|
288
|
-
null,
|
|
289
|
-
2
|
|
290
|
-
)
|
|
291
|
-
)
|
|
292
|
-
}
|
|
293
|
-
|
|
319
|
+
const content =
|
|
320
|
+
utils.isObject(response.data) || Array.isArray(response.data)
|
|
321
|
+
? JSON.stringify(response.data)
|
|
322
|
+
: response.data.toString()
|
|
323
|
+
const buffer = Buffer.from(content, 'utf8')
|
|
294
324
|
const data = merge(item, config, {
|
|
295
|
-
content
|
|
296
|
-
buffer
|
|
297
|
-
headers: response.headers
|
|
325
|
+
content,
|
|
326
|
+
buffer,
|
|
327
|
+
headers: response.headers,
|
|
328
|
+
request: response.request
|
|
298
329
|
})
|
|
299
330
|
|
|
300
331
|
if (!item.channel.logo && config.logo) {
|
|
@@ -346,4 +377,8 @@ utils.isPromise = function (promise) {
|
|
|
346
377
|
return !!promise && typeof promise.then === 'function'
|
|
347
378
|
}
|
|
348
379
|
|
|
380
|
+
utils.isObject = function (a) {
|
|
381
|
+
return !!a && a.constructor === Object
|
|
382
|
+
}
|
|
383
|
+
|
|
349
384
|
module.exports = utils
|
package/tests/bin.test.js
CHANGED
|
@@ -38,3 +38,19 @@ it('can load mini config', () => {
|
|
|
38
38
|
expect(stdoutResultTester(result)).toBe(true)
|
|
39
39
|
expect(result.includes("File 'tests/output/mini.guide.xml' successfully saved")).toBe(true)
|
|
40
40
|
})
|
|
41
|
+
|
|
42
|
+
it('can generate gzip version', () => {
|
|
43
|
+
const result = execSync(
|
|
44
|
+
`node ${pwd}/bin/epg-grabber.js \
|
|
45
|
+
--config=tests/input/mini.config.js \
|
|
46
|
+
--channels=tests/input/example.com.channels.xml \
|
|
47
|
+
--output=tests/output/mini.guide.xml.gz \
|
|
48
|
+
--gzip`,
|
|
49
|
+
{
|
|
50
|
+
encoding: 'utf8'
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
expect(stdoutResultTester(result)).toBe(true)
|
|
55
|
+
expect(result.includes("File 'tests/output/mini.guide.xml.gz' successfully saved")).toBe(true)
|
|
56
|
+
})
|
package/tests/index.test.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @jest-environment node
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import EPGGrabber from '../src/index'
|
|
6
6
|
import axios from 'axios'
|
|
7
7
|
|
|
8
8
|
jest.mock('axios')
|
|
@@ -27,7 +27,8 @@ it('return "Connection timeout" error if server does not response', done => {
|
|
|
27
27
|
lang: 'en',
|
|
28
28
|
name: 'CNN'
|
|
29
29
|
}
|
|
30
|
-
grabber
|
|
30
|
+
const grabber = new EPGGrabber(config)
|
|
31
|
+
grabber.grab(channel, '2022-01-01', (data, err) => {
|
|
31
32
|
expect(err.message).toBe('Connection timeout')
|
|
32
33
|
done()
|
|
33
34
|
})
|
|
@@ -53,8 +54,9 @@ it('can grab single channel programs', done => {
|
|
|
53
54
|
lang: 'fr',
|
|
54
55
|
name: '1TV'
|
|
55
56
|
}
|
|
57
|
+
const grabber = new EPGGrabber(config)
|
|
56
58
|
grabber
|
|
57
|
-
.grab(channel, '2022-01-01',
|
|
59
|
+
.grab(channel, '2022-01-01', (data, err) => {
|
|
58
60
|
if (err) {
|
|
59
61
|
console.log(` Error: ${err.message}`)
|
|
60
62
|
done()
|
package/tests/utils.test.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import mockAxios from 'jest-mock-axios'
|
|
2
2
|
import utils from '../src/utils'
|
|
3
|
+
import axios from 'axios'
|
|
3
4
|
import path from 'path'
|
|
4
5
|
import fs from 'fs'
|
|
5
6
|
|
|
@@ -148,7 +149,7 @@ it('can fetch data', () => {
|
|
|
148
149
|
url: 'http://example.com/20210319/1tv.json',
|
|
149
150
|
withCredentials: true
|
|
150
151
|
}
|
|
151
|
-
utils.fetchData(request).then(jest.fn).catch(jest.fn)
|
|
152
|
+
utils.fetchData(axios, request).then(jest.fn).catch(jest.fn)
|
|
152
153
|
expect(mockAxios).toHaveBeenCalledWith(
|
|
153
154
|
expect.objectContaining({
|
|
154
155
|
data: { accountID: '123' },
|