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/src/utils.js
CHANGED
|
@@ -1,127 +1,41 @@
|
|
|
1
|
-
const fs = require('fs')
|
|
2
|
-
const { padStart } = require('lodash')
|
|
3
|
-
const path = require('path')
|
|
4
|
-
const axios = require('axios').default
|
|
5
|
-
const axiosCookieJarSupport = require('axios-cookiejar-support').default
|
|
6
|
-
const { setupCache } = require('axios-cache-interceptor')
|
|
7
|
-
const tough = require('tough-cookie')
|
|
8
|
-
const convert = require('xml-js')
|
|
9
|
-
const { merge } = require('lodash')
|
|
10
1
|
const dayjs = require('dayjs')
|
|
11
2
|
const utc = require('dayjs/plugin/utc')
|
|
12
|
-
const { CurlGenerator } = require('curl-generator')
|
|
13
|
-
dayjs.extend(utc)
|
|
14
|
-
axiosCookieJarSupport(axios)
|
|
15
|
-
|
|
16
|
-
let timeout
|
|
17
|
-
const utils = {}
|
|
18
|
-
const defaultUserAgent =
|
|
19
|
-
'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'
|
|
20
|
-
|
|
21
|
-
utils.loadConfig = function (config) {
|
|
22
|
-
if (!config.site) throw new Error("The required 'site' property is missing")
|
|
23
|
-
if (!config.url) throw new Error("The required 'url' property is missing")
|
|
24
|
-
if (typeof config.url !== 'function' && typeof config.url !== 'string')
|
|
25
|
-
throw new Error("The 'url' property should return the function or string")
|
|
26
|
-
if (!config.parser) throw new Error("The required 'parser' function is missing")
|
|
27
|
-
if (typeof config.parser !== 'function')
|
|
28
|
-
throw new Error("The 'parser' property should return the function")
|
|
29
|
-
if (config.logo && typeof config.logo !== 'function')
|
|
30
|
-
throw new Error("The 'logo' property should return the function")
|
|
31
3
|
|
|
32
|
-
|
|
33
|
-
days: 1,
|
|
34
|
-
lang: 'en',
|
|
35
|
-
delay: 3000,
|
|
36
|
-
output: 'guide.xml',
|
|
37
|
-
request: {
|
|
38
|
-
method: 'GET',
|
|
39
|
-
maxContentLength: 5 * 1024 * 1024,
|
|
40
|
-
timeout: 5000,
|
|
41
|
-
withCredentials: true,
|
|
42
|
-
jar: new tough.CookieJar(),
|
|
43
|
-
responseType: 'arraybuffer',
|
|
44
|
-
cache: false
|
|
45
|
-
}
|
|
46
|
-
}
|
|
4
|
+
dayjs.extend(utc)
|
|
47
5
|
|
|
48
|
-
|
|
6
|
+
module.exports.sleep = sleep
|
|
7
|
+
module.exports.getUTCDate = getUTCDate
|
|
8
|
+
module.exports.isPromise = isPromise
|
|
9
|
+
module.exports.isObject = isObject
|
|
10
|
+
module.exports.escapeString = escapeString
|
|
11
|
+
module.exports.parseNumber = parseNumber
|
|
12
|
+
module.exports.formatDate = formatDate
|
|
13
|
+
module.exports.toArray = toArray
|
|
14
|
+
module.exports.toUnix = toUnix
|
|
15
|
+
|
|
16
|
+
function sleep(ms) {
|
|
17
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
49
18
|
}
|
|
50
19
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
client.interceptors.request.use(
|
|
54
|
-
function (request) {
|
|
55
|
-
if (config.debug) {
|
|
56
|
-
console.log('Request:', JSON.stringify(request, null, 2))
|
|
57
|
-
}
|
|
58
|
-
return request
|
|
59
|
-
},
|
|
60
|
-
function (error) {
|
|
61
|
-
return Promise.reject(error)
|
|
62
|
-
}
|
|
63
|
-
)
|
|
64
|
-
client.interceptors.response.use(
|
|
65
|
-
function (response) {
|
|
66
|
-
if (config.debug) {
|
|
67
|
-
const data =
|
|
68
|
-
utils.isObject(response.data) || Array.isArray(response.data)
|
|
69
|
-
? JSON.stringify(response.data)
|
|
70
|
-
: response.data.toString()
|
|
71
|
-
console.log(
|
|
72
|
-
'Response:',
|
|
73
|
-
JSON.stringify(
|
|
74
|
-
{
|
|
75
|
-
headers: response.headers,
|
|
76
|
-
data,
|
|
77
|
-
cached: response.cached
|
|
78
|
-
},
|
|
79
|
-
null,
|
|
80
|
-
2
|
|
81
|
-
)
|
|
82
|
-
)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
clearTimeout(timeout)
|
|
86
|
-
return response
|
|
87
|
-
},
|
|
88
|
-
function (error) {
|
|
89
|
-
clearTimeout(timeout)
|
|
90
|
-
return Promise.reject(error)
|
|
91
|
-
}
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
return client
|
|
20
|
+
function isObject(a) {
|
|
21
|
+
return !!a && a.constructor === Object
|
|
95
22
|
}
|
|
96
23
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (!siteTag.elements) return []
|
|
101
|
-
const site = siteTag.attributes.site
|
|
102
|
-
|
|
103
|
-
const channelsTag = siteTag.elements.find(el => el.name === 'channels')
|
|
104
|
-
if (!channelsTag.elements) return []
|
|
105
|
-
|
|
106
|
-
const channels = channelsTag.elements
|
|
107
|
-
.filter(el => el.name === 'channel')
|
|
108
|
-
.map(el => {
|
|
109
|
-
const channel = el.attributes
|
|
110
|
-
if (!el.elements) throw new Error(`Channel '${channel.xmltv_id}' has no valid name`)
|
|
111
|
-
channel.name = el.elements.find(el => el.type === 'text').text
|
|
112
|
-
channel.site = channel.site || site
|
|
24
|
+
function isPromise(promise) {
|
|
25
|
+
return !!promise && typeof promise.then === 'function'
|
|
26
|
+
}
|
|
113
27
|
|
|
114
|
-
|
|
115
|
-
|
|
28
|
+
function getUTCDate(d = null) {
|
|
29
|
+
if (typeof d === 'string') return dayjs.utc(d).startOf('d')
|
|
116
30
|
|
|
117
|
-
return
|
|
31
|
+
return dayjs.utc().startOf('d')
|
|
118
32
|
}
|
|
119
33
|
|
|
120
|
-
|
|
121
|
-
return
|
|
34
|
+
function toUnix(d) {
|
|
35
|
+
return dayjs.utc(d).valueOf()
|
|
122
36
|
}
|
|
123
37
|
|
|
124
|
-
|
|
38
|
+
function escapeString(string, defaultValue = '') {
|
|
125
39
|
if (!string) return defaultValue
|
|
126
40
|
|
|
127
41
|
const regex = new RegExp(
|
|
@@ -149,372 +63,16 @@ utils.escapeString = function (string, defaultValue = '') {
|
|
|
149
63
|
.trim()
|
|
150
64
|
}
|
|
151
65
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
'YYYYMMDD'
|
|
155
|
-
)}">\r\n`
|
|
156
|
-
for (let channel of channels) {
|
|
157
|
-
const id = utils.escapeString(channel['xmltv_id'])
|
|
158
|
-
const displayName = utils.escapeString(channel.name)
|
|
159
|
-
output += `<channel id="${id}"><display-name>${displayName}</display-name>`
|
|
160
|
-
if (channel.logo) {
|
|
161
|
-
const logo = utils.escapeString(channel.logo)
|
|
162
|
-
output += `<icon src="${logo}"/>`
|
|
163
|
-
}
|
|
164
|
-
if (channel.site) {
|
|
165
|
-
const url = channel.site ? 'https://' + channel.site : null
|
|
166
|
-
output += `<url>${url}</url>`
|
|
167
|
-
}
|
|
168
|
-
output += `</channel>\r\n`
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
for (let program of programs) {
|
|
172
|
-
if (!program) continue
|
|
173
|
-
|
|
174
|
-
const channel = utils.escapeString(program.channel)
|
|
175
|
-
const title = utils.escapeString(program.title)
|
|
176
|
-
const description = utils.escapeString(program.description)
|
|
177
|
-
const categories = Array.isArray(program.category) ? program.category : [program.category]
|
|
178
|
-
const start = program.start ? dayjs.unix(program.start).utc().format('YYYYMMDDHHmmss ZZ') : ''
|
|
179
|
-
const stop = program.stop ? dayjs.unix(program.stop).utc().format('YYYYMMDDHHmmss ZZ') : ''
|
|
180
|
-
const lang = program.lang || 'en'
|
|
181
|
-
const xmltv_ns = createXMLTVNS(program.season, program.episode)
|
|
182
|
-
const onscreen = createOnScreen(program.season, program.episode)
|
|
183
|
-
const date = program.date || ''
|
|
184
|
-
const credits = createCredits({
|
|
185
|
-
director: program.director,
|
|
186
|
-
actor: program.actor,
|
|
187
|
-
writer: program.writer,
|
|
188
|
-
adapter: program.adapter,
|
|
189
|
-
producer: program.producer,
|
|
190
|
-
composer: program.composer,
|
|
191
|
-
editor: program.editor,
|
|
192
|
-
presenter: program.presenter,
|
|
193
|
-
commentator: program.commentator,
|
|
194
|
-
guest: program.guest
|
|
195
|
-
})
|
|
196
|
-
const icon = utils.escapeString(program.icon)
|
|
197
|
-
const sub_title = utils.escapeString(program.sub_title)
|
|
198
|
-
const url = program.url ? createURL(program.url, channel) : ''
|
|
199
|
-
|
|
200
|
-
if (start && stop && title) {
|
|
201
|
-
output += `<programme start="${start}" stop="${stop}" channel="${channel}"><title lang="${lang}">${title}</title>`
|
|
202
|
-
|
|
203
|
-
if (sub_title) {
|
|
204
|
-
output += `<sub-title>${sub_title}</sub-title>`
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (description) {
|
|
208
|
-
output += `<desc lang="${lang}">${description}</desc>`
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (categories.length) {
|
|
212
|
-
categories.forEach(category => {
|
|
213
|
-
if (category) {
|
|
214
|
-
output += `<category lang="${lang}">${utils.escapeString(category)}</category>`
|
|
215
|
-
}
|
|
216
|
-
})
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (url) {
|
|
220
|
-
output += url
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (xmltv_ns) {
|
|
224
|
-
output += `<episode-num system="xmltv_ns">${xmltv_ns}</episode-num>`
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (onscreen) {
|
|
228
|
-
output += `<episode-num system="onscreen">${onscreen}</episode-num>`
|
|
229
|
-
}
|
|
230
|
-
if (date) {
|
|
231
|
-
output += `<date>${date}</date>`
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (icon) {
|
|
235
|
-
output += `<icon src="${icon}"/>`
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (credits) {
|
|
239
|
-
output += `<credits>${credits}</credits>`
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
output += '</programme>\r\n'
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
output += '</tv>'
|
|
247
|
-
|
|
248
|
-
function createXMLTVNS(s, e) {
|
|
249
|
-
if (!e) return null
|
|
250
|
-
s = s || 1
|
|
251
|
-
|
|
252
|
-
return `${s - 1}.${e - 1}.0/1`
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function createOnScreen(s, e) {
|
|
256
|
-
if (!e) return null
|
|
257
|
-
s = s || 1
|
|
258
|
-
|
|
259
|
-
s = padStart(s, 2, '0')
|
|
260
|
-
e = padStart(e, 2, '0')
|
|
261
|
-
|
|
262
|
-
return `S${s}E${e}`
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function createURL(urlObj, channel = '') {
|
|
266
|
-
const urls = Array.isArray(urlObj) ? urlObj : [urlObj]
|
|
267
|
-
let output = ''
|
|
268
|
-
for (let url of urls) {
|
|
269
|
-
if (typeof url === 'string' || url instanceof String) {
|
|
270
|
-
url = { value: url }
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
let attr = url.system ? ` system="${url.system}"` : ''
|
|
274
|
-
if (url.value.includes('http')) {
|
|
275
|
-
output += `<url${attr}>${url.value}</url>`
|
|
276
|
-
} else if (channel) {
|
|
277
|
-
let chan = channels.find(c => c.xmltv_id.localeCompare(channel) === 0)
|
|
278
|
-
if (chan && chan.site) {
|
|
279
|
-
output += `<url${attr}>https://${chan.site}${url.value}</url>`
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
return output
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function createImage(imgObj, channel = '') {
|
|
288
|
-
const imgs = Array.isArray(imgObj) ? imgObj : [imgObj]
|
|
289
|
-
let output = ''
|
|
290
|
-
for (let img of imgs) {
|
|
291
|
-
if (typeof img === 'string' || img instanceof String) {
|
|
292
|
-
img = { value: img }
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const imageTypes = ['poster', 'backdrop', 'still', 'person', 'character']
|
|
296
|
-
const imageSizes = ['1', '2', '3']
|
|
297
|
-
const imageOrients = ['P', 'L']
|
|
298
|
-
|
|
299
|
-
let attr = ''
|
|
300
|
-
|
|
301
|
-
if (img.type && imageTypes.some(el => img.type.includes(el))) {
|
|
302
|
-
attr += ` type="${img.type}"`
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (img.size && imageSizes.some(el => img.size.includes(el))) {
|
|
306
|
-
attr += ` size="${img.size}"`
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (img.orient && imageOrients.some(el => img.orient.includes(el))) {
|
|
310
|
-
attr += ` orient="${img.orient}"`
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (img.system) {
|
|
314
|
-
attr += ` system="${img.system}"`
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (img.value.includes('http')) {
|
|
318
|
-
output += `<image${attr}>${img.value}</image>`
|
|
319
|
-
} else if (channel) {
|
|
320
|
-
let chan = channels.find(c => c.xmltv_id.localeCompare(channel) === 0)
|
|
321
|
-
if (chan && chan.site) {
|
|
322
|
-
output += `<image${attr}>https://${chan.site}${img.value}</image>`
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
return output
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function createCredits(obj) {
|
|
331
|
-
let cast = Object.entries(obj)
|
|
332
|
-
.filter(x => x[1])
|
|
333
|
-
.map(([name, value]) => ({ name, value }))
|
|
334
|
-
|
|
335
|
-
let output = ''
|
|
336
|
-
for (let type of cast) {
|
|
337
|
-
const r = Array.isArray(type.value) ? type.value : [type.value]
|
|
338
|
-
for (let person of r) {
|
|
339
|
-
if (typeof person === 'string' || person instanceof String) {
|
|
340
|
-
person = { value: person }
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
let attr = ''
|
|
344
|
-
if (type.name.localeCompare('actor') === 0 && type.value.role) {
|
|
345
|
-
attr += ` role="${type.value.role}"`
|
|
346
|
-
}
|
|
347
|
-
if (type.name.localeCompare('actor') === 0 && type.value.guest) {
|
|
348
|
-
attr += ` guest="${type.value.guest}"`
|
|
349
|
-
}
|
|
350
|
-
output += `<${type.name}${attr}>${person.value}`
|
|
351
|
-
|
|
352
|
-
if (person.url) {
|
|
353
|
-
output += createURL(person.url)
|
|
354
|
-
}
|
|
355
|
-
if (person.image) {
|
|
356
|
-
output += createImage(person.image)
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
output += `</${type.name}>`
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
return output
|
|
363
|
-
}
|
|
364
|
-
return output
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
utils.writeToFile = function (filename, data) {
|
|
368
|
-
const dir = path.resolve(path.dirname(filename))
|
|
369
|
-
if (!fs.existsSync(dir)) {
|
|
370
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
fs.writeFileSync(path.resolve(filename), data)
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
utils.buildRequest = async function (item, config) {
|
|
377
|
-
const CancelToken = axios.CancelToken
|
|
378
|
-
const source = CancelToken.source()
|
|
379
|
-
const request = { ...config.request }
|
|
380
|
-
timeout = setTimeout(() => {
|
|
381
|
-
source.cancel('Connection timeout')
|
|
382
|
-
}, request.timeout)
|
|
383
|
-
const headers = await utils.getRequestHeaders(item, config)
|
|
384
|
-
request.headers = { 'User-Agent': defaultUserAgent, ...headers }
|
|
385
|
-
request.url = await utils.getRequestUrl(item, config)
|
|
386
|
-
request.data = await utils.getRequestData(item, config)
|
|
387
|
-
request.cancelToken = source.token
|
|
388
|
-
|
|
389
|
-
if (config.curl) {
|
|
390
|
-
const curl = CurlGenerator({
|
|
391
|
-
url: request.url,
|
|
392
|
-
method: request.method,
|
|
393
|
-
headers: request.headers,
|
|
394
|
-
body: request.data
|
|
395
|
-
})
|
|
396
|
-
console.log(curl)
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
return request
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
utils.fetchData = function (client, request) {
|
|
403
|
-
return client(request)
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
utils.getRequestHeaders = async function (item, config) {
|
|
407
|
-
if (typeof config.request.headers === 'function') {
|
|
408
|
-
const headers = config.request.headers(item)
|
|
409
|
-
if (this.isPromise(headers)) {
|
|
410
|
-
return await headers
|
|
411
|
-
}
|
|
412
|
-
return headers
|
|
413
|
-
}
|
|
414
|
-
return config.request.headers || null
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
utils.getRequestData = async function (item, config) {
|
|
418
|
-
if (typeof config.request.data === 'function') {
|
|
419
|
-
const data = config.request.data(item)
|
|
420
|
-
if (this.isPromise(data)) {
|
|
421
|
-
return await data
|
|
422
|
-
}
|
|
423
|
-
return data
|
|
424
|
-
}
|
|
425
|
-
return config.request.data || null
|
|
66
|
+
function parseNumber(val) {
|
|
67
|
+
return val ? parseInt(val) : null
|
|
426
68
|
}
|
|
427
69
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
const url = config.url(item)
|
|
431
|
-
if (this.isPromise(url)) {
|
|
432
|
-
return await url
|
|
433
|
-
}
|
|
434
|
-
return url
|
|
435
|
-
}
|
|
436
|
-
return config.url
|
|
70
|
+
function formatDate(date, format) {
|
|
71
|
+
return date ? dayjs.utc(date).format(format) : null
|
|
437
72
|
}
|
|
438
73
|
|
|
439
|
-
|
|
440
|
-
if (
|
|
74
|
+
function toArray(value) {
|
|
75
|
+
if (Array.isArray(value)) return value.filter(Boolean)
|
|
441
76
|
|
|
442
|
-
return
|
|
77
|
+
return [value].filter(Boolean)
|
|
443
78
|
}
|
|
444
|
-
|
|
445
|
-
utils.parseResponse = async (item, response, config) => {
|
|
446
|
-
const data = merge(item, config, {
|
|
447
|
-
content: response.data.toString(),
|
|
448
|
-
buffer: response.data,
|
|
449
|
-
headers: response.headers,
|
|
450
|
-
request: response.request,
|
|
451
|
-
cached: response.cached
|
|
452
|
-
})
|
|
453
|
-
|
|
454
|
-
if (!item.channel.logo && config.logo) {
|
|
455
|
-
data.channel.logo = await utils.loadLogo(data, config)
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
return await utils.parsePrograms(data, config)
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
utils.parsePrograms = async function (data, config) {
|
|
462
|
-
let programs = config.parser(data)
|
|
463
|
-
|
|
464
|
-
if (this.isPromise(programs)) {
|
|
465
|
-
programs = await programs
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
if (!Array.isArray(programs)) {
|
|
469
|
-
throw new Error('Parser should return an array')
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const channel = data.channel
|
|
473
|
-
return programs
|
|
474
|
-
.filter(i => i)
|
|
475
|
-
.map(program => {
|
|
476
|
-
return {
|
|
477
|
-
title: program.title,
|
|
478
|
-
description: program.description || null,
|
|
479
|
-
category: program.category || null,
|
|
480
|
-
season: program.season || null,
|
|
481
|
-
episode: program.episode || null,
|
|
482
|
-
sub_title: program.sub_title || null,
|
|
483
|
-
url: program.url || null,
|
|
484
|
-
icon: program.icon || null,
|
|
485
|
-
channel: channel.xmltv_id,
|
|
486
|
-
lang: program.lang || channel.lang || config.lang || 'en',
|
|
487
|
-
start: program.start ? dayjs(program.start).unix() : null,
|
|
488
|
-
stop: program.stop ? dayjs(program.stop).unix() : null,
|
|
489
|
-
date: program.date || null,
|
|
490
|
-
director: program.director || null,
|
|
491
|
-
actor: program.actor || null,
|
|
492
|
-
writer: program.writer || null,
|
|
493
|
-
adapter: program.adapter || null,
|
|
494
|
-
producer: program.producer || null,
|
|
495
|
-
composer: program.composer || null,
|
|
496
|
-
editor: program.editor || null,
|
|
497
|
-
presenter: program.presenter || null,
|
|
498
|
-
commentator: program.commentator || null,
|
|
499
|
-
guest: program.guest || null
|
|
500
|
-
}
|
|
501
|
-
})
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
utils.loadLogo = async function (options, config) {
|
|
505
|
-
const logo = config.logo(options)
|
|
506
|
-
if (this.isPromise(logo)) {
|
|
507
|
-
return await logo
|
|
508
|
-
}
|
|
509
|
-
return logo
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
utils.isPromise = function (promise) {
|
|
513
|
-
return !!promise && typeof promise.then === 'function'
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
utils.isObject = function (a) {
|
|
517
|
-
return !!a && a.constructor === Object
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
module.exports = utils
|
package/src/xmltv.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const { escapeString, getUTCDate, formatDate } = require('./utils')
|
|
2
|
+
const el = createElement
|
|
3
|
+
|
|
4
|
+
module.exports.generate = generate
|
|
5
|
+
|
|
6
|
+
function generate({ channels, programs, date = getUTCDate() }) {
|
|
7
|
+
let output = `<?xml version="1.0" encoding="UTF-8" ?>`
|
|
8
|
+
output += createElements(channels, programs, date)
|
|
9
|
+
|
|
10
|
+
return output
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function createElements(channels, programs, date) {
|
|
14
|
+
return el('tv', { date: formatDate(date, 'YYYYMMDD') }, [
|
|
15
|
+
...channels.map(channel => {
|
|
16
|
+
return (
|
|
17
|
+
'\r\n' +
|
|
18
|
+
el('channel', { id: channel.id }, [
|
|
19
|
+
el('display-name', {}, [escapeString(channel.name)]),
|
|
20
|
+
el('icon', { src: channel.logo }),
|
|
21
|
+
el('url', {}, [channel.url])
|
|
22
|
+
])
|
|
23
|
+
)
|
|
24
|
+
}),
|
|
25
|
+
...programs.map(program => {
|
|
26
|
+
return (
|
|
27
|
+
'\r\n' +
|
|
28
|
+
el(
|
|
29
|
+
'programme',
|
|
30
|
+
{
|
|
31
|
+
start: formatDate(program.start, 'YYYYMMDDHHmmss ZZ'),
|
|
32
|
+
stop: formatDate(program.stop, 'YYYYMMDDHHmmss ZZ'),
|
|
33
|
+
channel: program.channel
|
|
34
|
+
},
|
|
35
|
+
[
|
|
36
|
+
el('title', {}, [escapeString(program.title)]),
|
|
37
|
+
el('sub-title', {}, [escapeString(program.sub_title)]),
|
|
38
|
+
el('desc', {}, [escapeString(program.description)]),
|
|
39
|
+
el('credits', {}, [
|
|
40
|
+
...program.directors.map(data => createCastMember('director', data)),
|
|
41
|
+
...program.actors.map(data => createCastMember('actor', data)),
|
|
42
|
+
...program.writers.map(data => createCastMember('writer', data)),
|
|
43
|
+
...program.adapters.map(data => createCastMember('adapter', data)),
|
|
44
|
+
...program.producers.map(data => createCastMember('producer', data)),
|
|
45
|
+
...program.composers.map(data => createCastMember('composer', data)),
|
|
46
|
+
...program.editors.map(data => createCastMember('editor', data)),
|
|
47
|
+
...program.presenters.map(data => createCastMember('presenter', data)),
|
|
48
|
+
...program.commentators.map(data => createCastMember('commentator', data)),
|
|
49
|
+
...program.guests.map(data => createCastMember('guest', data))
|
|
50
|
+
]),
|
|
51
|
+
el('date', {}, [formatDate(program.date, 'YYYYMMDD')]),
|
|
52
|
+
...program.categories.map(category => el('category', {}, [escapeString(category)])),
|
|
53
|
+
el('icon', { src: program.icon.src }),
|
|
54
|
+
...program.urls.map(createURL),
|
|
55
|
+
...program.episodeNumbers.map(episode =>
|
|
56
|
+
el('episode-num', { system: episode.system }, [episode.value])
|
|
57
|
+
),
|
|
58
|
+
...program.ratings.map(rating =>
|
|
59
|
+
el('rating', { system: rating.system }, [
|
|
60
|
+
el('value', {}, [rating.value]),
|
|
61
|
+
el('icon', { src: rating.icon })
|
|
62
|
+
])
|
|
63
|
+
)
|
|
64
|
+
]
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
})
|
|
68
|
+
])
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createCastMember(position, data) {
|
|
72
|
+
return el(position, {}, [
|
|
73
|
+
escapeString(data.value),
|
|
74
|
+
...data.url.map(createURL),
|
|
75
|
+
...data.image.map(createImage)
|
|
76
|
+
])
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function createImage(image) {
|
|
80
|
+
return el(
|
|
81
|
+
'image',
|
|
82
|
+
{
|
|
83
|
+
type: image.type,
|
|
84
|
+
size: image.size,
|
|
85
|
+
orient: image.orient,
|
|
86
|
+
system: image.system
|
|
87
|
+
},
|
|
88
|
+
[image.value]
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function createURL(url) {
|
|
93
|
+
return el('url', { system: url.system }, [url.value])
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function createElement(name, attrs = {}, children = []) {
|
|
97
|
+
return toString({ name, attrs, children })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function toString(elem) {
|
|
101
|
+
if (typeof elem === 'string' || typeof elem === 'number') return elem
|
|
102
|
+
|
|
103
|
+
let attrs = ''
|
|
104
|
+
for (let key in elem.attrs) {
|
|
105
|
+
let value = elem.attrs[key]
|
|
106
|
+
if (value) {
|
|
107
|
+
attrs += ` ${key}="${escapeString(value)}"`
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (elem.children.filter(Boolean).length) {
|
|
112
|
+
let children = ''
|
|
113
|
+
elem.children.forEach(childElem => {
|
|
114
|
+
children += toString(childElem)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
return `<${elem.name}${attrs}>${children}</${elem.name}>`
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!attrs) return ''
|
|
121
|
+
if (!['icon'].includes(elem.name)) return ''
|
|
122
|
+
|
|
123
|
+
return `<${elem.name}${attrs}/>`
|
|
124
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Channel from '../src/Channel'
|
|
2
|
+
|
|
3
|
+
it('can create new Channel', () => {
|
|
4
|
+
const channel = new Channel({
|
|
5
|
+
name: '1 TV',
|
|
6
|
+
xmltv_id: '1TV.com',
|
|
7
|
+
site_id: '1',
|
|
8
|
+
site: 'example.com',
|
|
9
|
+
lang: 'fr',
|
|
10
|
+
logo: 'https://example.com/logos/1TV.png'
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
expect(channel).toMatchObject({
|
|
14
|
+
name: '1 TV',
|
|
15
|
+
id: '1TV.com',
|
|
16
|
+
site_id: '1',
|
|
17
|
+
site: 'example.com',
|
|
18
|
+
url: 'https://example.com',
|
|
19
|
+
lang: 'fr',
|
|
20
|
+
logo: 'https://example.com/logos/1TV.png'
|
|
21
|
+
})
|
|
22
|
+
})
|