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/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
- 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")
4
+ dayjs.extend(utc)
31
5
 
32
- const defaultConfig = {
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
- }
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
- return merge(defaultConfig, config)
13
+ function sleep(ms) {
14
+ return new Promise(resolve => setTimeout(resolve, ms))
49
15
  }
50
16
 
51
- utils.createClient = function (config) {
52
- const client = setupCache(axios.create())
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
- utils.parseChannels = function (xml) {
98
- const result = convert.xml2js(xml)
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
- utils.sleep = function (ms) {
121
- return new Promise(resolve => setTimeout(resolve, ms))
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
- utils.escapeString = function (string, defaultValue = '') {
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
- utils.convertToXMLTV = function ({ channels, programs, date = dayjs.utc() }) {
153
- let output = `<?xml version="1.0" encoding="UTF-8" ?><tv date="${dayjs(date).format(
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
+ }