epg-grabber 0.27.2 → 0.28.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.
- package/README.md +14 -2
- package/bin/epg-grabber.js +20 -47
- package/package.json +1 -1
- package/src/channels.js +26 -0
- package/src/client.js +138 -0
- package/src/config.js +34 -0
- package/src/file.js +33 -0
- package/src/index.js +35 -28
- package/src/logger.js +34 -0
- package/src/programs.js +24 -0
- package/src/utils.js +20 -479
- package/src/xmltv.js +187 -0
- package/tests/channels.test.js +26 -0
- package/tests/client.test.js +47 -0
- package/tests/config.test.js +26 -0
- package/tests/programs.test.js +63 -0
- package/tests/utils.test.js +3 -393
- package/tests/xmltv.test.js +146 -0
package/src/utils.js
CHANGED
|
@@ -1,127 +1,34 @@
|
|
|
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
3
|
|
|
21
|
-
|
|
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")
|
|
4
|
+
dayjs.extend(utc)
|
|
31
5
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
}
|
|
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.parseInteger = parseInteger
|
|
47
12
|
|
|
48
|
-
|
|
13
|
+
function sleep(ms) {
|
|
14
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
49
15
|
}
|
|
50
16
|
|
|
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
|
|
17
|
+
function isObject(a) {
|
|
18
|
+
return !!a && a.constructor === Object
|
|
95
19
|
}
|
|
96
20
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const siteTag = result.elements.find(el => el.name === 'site') || {}
|
|
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
|
|
113
|
-
|
|
114
|
-
return channel
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
return { site, channels }
|
|
21
|
+
function isPromise(promise) {
|
|
22
|
+
return !!promise && typeof promise.then === 'function'
|
|
118
23
|
}
|
|
119
24
|
|
|
120
|
-
|
|
121
|
-
|
|
25
|
+
function getUTCDate(d = null) {
|
|
26
|
+
if (typeof d === 'string') return dayjs.utc(d).startOf('d')
|
|
27
|
+
|
|
28
|
+
return dayjs.utc().startOf('d')
|
|
122
29
|
}
|
|
123
30
|
|
|
124
|
-
|
|
31
|
+
function escapeString(string, defaultValue = '') {
|
|
125
32
|
if (!string) return defaultValue
|
|
126
33
|
|
|
127
34
|
const regex = new RegExp(
|
|
@@ -149,372 +56,6 @@ utils.escapeString = function (string, defaultValue = '') {
|
|
|
149
56
|
.trim()
|
|
150
57
|
}
|
|
151
58
|
|
|
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)
|
|
59
|
+
function parseInteger(val) {
|
|
60
|
+
return val ? parseInt(val) : null
|
|
374
61
|
}
|
|
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
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
utils.getRequestUrl = async function (item, config) {
|
|
429
|
-
if (typeof config.url === 'function') {
|
|
430
|
-
const url = config.url(item)
|
|
431
|
-
if (this.isPromise(url)) {
|
|
432
|
-
return await url
|
|
433
|
-
}
|
|
434
|
-
return url
|
|
435
|
-
}
|
|
436
|
-
return config.url
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
utils.getUTCDate = function (d = null) {
|
|
440
|
-
if (typeof d === 'string') return dayjs.utc(d).startOf('d')
|
|
441
|
-
|
|
442
|
-
return dayjs.utc().startOf('d')
|
|
443
|
-
}
|
|
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,187 @@
|
|
|
1
|
+
const { padStart } = require('lodash')
|
|
2
|
+
const { escapeString, getUTCDate } = require('./utils')
|
|
3
|
+
const dayjs = require('dayjs')
|
|
4
|
+
const utc = require('dayjs/plugin/utc')
|
|
5
|
+
|
|
6
|
+
dayjs.extend(utc)
|
|
7
|
+
|
|
8
|
+
module.exports.generate = generate
|
|
9
|
+
|
|
10
|
+
function generate({ channels, programs, date = getUTCDate() }) {
|
|
11
|
+
let output = `<?xml version="1.0" encoding="UTF-8" ?>`
|
|
12
|
+
output += createElements(channels, programs, date)
|
|
13
|
+
|
|
14
|
+
return output
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const el = createElement
|
|
18
|
+
|
|
19
|
+
function createElements(channels, programs, date) {
|
|
20
|
+
date = formatDate(date, 'YYYYMMDD')
|
|
21
|
+
return el('tv', { date }, [
|
|
22
|
+
...channels.map(channel => {
|
|
23
|
+
const url = channel.site ? `https://${channel.site}` : ''
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
'\r\n' +
|
|
27
|
+
el('channel', { id: channel.xmltv_id }, [
|
|
28
|
+
el('display-name', {}, [escapeString(channel.name)]),
|
|
29
|
+
el('icon', { src: channel.logo }),
|
|
30
|
+
el('url', {}, [url])
|
|
31
|
+
])
|
|
32
|
+
)
|
|
33
|
+
}),
|
|
34
|
+
...programs.map(program => {
|
|
35
|
+
const programDate = program.date ? formatDate(program.date, 'YYYYMMDD') : ''
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
'\r\n' +
|
|
39
|
+
el(
|
|
40
|
+
'programme',
|
|
41
|
+
{
|
|
42
|
+
start: formatDate(program.start, 'YYYYMMDDHHmmss ZZ'),
|
|
43
|
+
stop: formatDate(program.stop, 'YYYYMMDDHHmmss ZZ'),
|
|
44
|
+
channel: program.channel
|
|
45
|
+
},
|
|
46
|
+
[
|
|
47
|
+
el('title', {}, [escapeString(program.title)]),
|
|
48
|
+
el('sub-title', {}, [escapeString(program.sub_title)]),
|
|
49
|
+
el('desc', {}, [escapeString(program.description)]),
|
|
50
|
+
el('credits', {}, [
|
|
51
|
+
...toArray(program.director).map(data => createCastMember('director', data)),
|
|
52
|
+
...toArray(program.actor).map(data => createCastMember('actor', data)),
|
|
53
|
+
...toArray(program.writer).map(data => createCastMember('writer', data)),
|
|
54
|
+
...toArray(program.adapter).map(data => createCastMember('adapter', data)),
|
|
55
|
+
...toArray(program.producer).map(data => createCastMember('producer', data)),
|
|
56
|
+
...toArray(program.composer).map(data => createCastMember('composer', data)),
|
|
57
|
+
...toArray(program.editor).map(data => createCastMember('editor', data)),
|
|
58
|
+
...toArray(program.presenter).map(data => createCastMember('presenter', data)),
|
|
59
|
+
...toArray(program.commentator).map(data => createCastMember('commentator', data)),
|
|
60
|
+
...toArray(program.guest).map(data => createCastMember('guest', data))
|
|
61
|
+
]),
|
|
62
|
+
el('date', {}, [programDate]),
|
|
63
|
+
...toArray(program.category).map(category =>
|
|
64
|
+
el('category', {}, [escapeString(category)])
|
|
65
|
+
),
|
|
66
|
+
el('icon', { src: program.icon }),
|
|
67
|
+
...toArray(program.url).map(createURL),
|
|
68
|
+
el('episode-num', { system: 'xmltv_ns' }, [
|
|
69
|
+
formatEpisodeNum(program.season, program.episode, 'xmltv_ns')
|
|
70
|
+
]),
|
|
71
|
+
el('episode-num', { system: 'onscreen' }, [
|
|
72
|
+
formatEpisodeNum(program.season, program.episode, 'onscreen')
|
|
73
|
+
]),
|
|
74
|
+
...toArray(program.rating).map(rating =>
|
|
75
|
+
el('rating', { system: rating.system }, [
|
|
76
|
+
el('value', {}, [rating.value]),
|
|
77
|
+
el('icon', { src: rating.icon })
|
|
78
|
+
])
|
|
79
|
+
)
|
|
80
|
+
]
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
])
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function formatEpisodeNum(s, e, system) {
|
|
88
|
+
switch (system) {
|
|
89
|
+
case 'xmltv_ns':
|
|
90
|
+
return createXMLTVNS(s, e)
|
|
91
|
+
case 'onscreen':
|
|
92
|
+
return createOnScreen(s, e)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return ''
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function createXMLTVNS(s, e) {
|
|
99
|
+
if (!e) return ''
|
|
100
|
+
s = s || 1
|
|
101
|
+
|
|
102
|
+
return `${s - 1}.${e - 1}.0/1`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function createOnScreen(s, e) {
|
|
106
|
+
if (!e) return ''
|
|
107
|
+
s = s || 1
|
|
108
|
+
|
|
109
|
+
s = padStart(s, 2, '0')
|
|
110
|
+
e = padStart(e, 2, '0')
|
|
111
|
+
|
|
112
|
+
return `S${s}E${e}`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function createCastMember(position, data) {
|
|
116
|
+
data = toObject(data)
|
|
117
|
+
return el(position, {}, [
|
|
118
|
+
escapeString(data.value),
|
|
119
|
+
...toArray(data.url).map(createURL),
|
|
120
|
+
...toArray(data.image).map(createImage)
|
|
121
|
+
])
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function createImage(image) {
|
|
125
|
+
image = toObject(image)
|
|
126
|
+
return el(
|
|
127
|
+
'image',
|
|
128
|
+
{
|
|
129
|
+
type: image.type,
|
|
130
|
+
size: image.size,
|
|
131
|
+
orient: image.orient,
|
|
132
|
+
system: image.system
|
|
133
|
+
},
|
|
134
|
+
[image.value]
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function createURL(url) {
|
|
139
|
+
url = toObject(url)
|
|
140
|
+
return el('url', { system: url.system }, [url.value])
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function toObject(value) {
|
|
144
|
+
if (typeof value === 'string') return { value }
|
|
145
|
+
|
|
146
|
+
return value
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function toArray(value) {
|
|
150
|
+
if (Array.isArray(value)) return value.filter(Boolean)
|
|
151
|
+
|
|
152
|
+
return [value].filter(Boolean)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function formatDate(date, format) {
|
|
156
|
+
return date ? dayjs.utc(date).format(format) : null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function createElement(name, attrs = {}, children = []) {
|
|
160
|
+
return toString({ name, attrs, children })
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function toString(elem) {
|
|
164
|
+
if (typeof elem === 'string') return elem
|
|
165
|
+
|
|
166
|
+
let attrs = ''
|
|
167
|
+
for (let key in elem.attrs) {
|
|
168
|
+
let value = elem.attrs[key]
|
|
169
|
+
if (value) {
|
|
170
|
+
attrs += ` ${key}="${escapeString(value)}"`
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (elem.children.filter(Boolean).length) {
|
|
175
|
+
let children = ''
|
|
176
|
+
elem.children.forEach(childElem => {
|
|
177
|
+
children += toString(childElem)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
return `<${elem.name}${attrs}>${children}</${elem.name}>`
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!attrs) return ''
|
|
184
|
+
if (!['icon'].includes(elem.name)) return ''
|
|
185
|
+
|
|
186
|
+
return `<${elem.name}${attrs}/>`
|
|
187
|
+
}
|