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 +14 -2
- package/bin/epg-grabber.js +31 -54
- package/package.json +1 -1
- package/src/Channel.js +19 -0
- package/src/Program.js +122 -0
- package/src/client.js +138 -0
- package/src/config.js +34 -0
- package/src/file.js +33 -0
- package/src/index.js +36 -30
- package/src/logger.js +34 -0
- package/src/parser.js +45 -0
- package/src/utils.js +31 -473
- package/src/xmltv.js +124 -0
- package/tests/Channel.test.js +22 -0
- package/tests/Program.test.js +163 -0
- package/tests/bin.test.js +3 -3
- package/tests/client.test.js +47 -0
- package/tests/config.test.js +26 -0
- package/tests/index.test.js +1 -1
- package/tests/input/async.config.js +8 -2
- package/tests/input/{example.com.channels.xml → example.channels.xml} +0 -0
- package/tests/input/{example.com.config.js → example.config.js} +4 -10
- package/tests/parser.test.js +40 -0
- package/tests/utils.test.js +3 -393
- package/tests/xmltv.test.js +71 -0
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
|
|
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
|
-
|
|
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
|
]
|
package/bin/epg-grabber.js
CHANGED
|
@@ -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
|
|
13
|
-
const {
|
|
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',
|
|
24
|
-
.option('--delay <delay>', 'Delay between requests (in mileseconds)',
|
|
25
|
-
.option('--timeout <timeout>', 'Set a timeout for each request (in mileseconds)',
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
92
|
-
|
|
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 =
|
|
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.
|
|
107
|
-
|
|
108
|
-
|
|
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 =
|
|
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
|
-
|
|
107
|
+
file.write(outputPath, compressed)
|
|
127
108
|
} else {
|
|
128
109
|
outputPath = outputPath || 'guide.xml'
|
|
129
|
-
|
|
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
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
|
|
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 =
|
|
6
|
-
this.client =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|