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/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
- 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
- }
4
+ dayjs.extend(utc)
47
5
 
48
- return merge(defaultConfig, config)
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
- 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
20
+ function isObject(a) {
21
+ return !!a && a.constructor === Object
95
22
  }
96
23
 
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
24
+ function isPromise(promise) {
25
+ return !!promise && typeof promise.then === 'function'
26
+ }
113
27
 
114
- return channel
115
- })
28
+ function getUTCDate(d = null) {
29
+ if (typeof d === 'string') return dayjs.utc(d).startOf('d')
116
30
 
117
- return { site, channels }
31
+ return dayjs.utc().startOf('d')
118
32
  }
119
33
 
120
- utils.sleep = function (ms) {
121
- return new Promise(resolve => setTimeout(resolve, ms))
34
+ function toUnix(d) {
35
+ return dayjs.utc(d).valueOf()
122
36
  }
123
37
 
124
- utils.escapeString = function (string, defaultValue = '') {
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
- 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)
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
- 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
70
+ function formatDate(date, format) {
71
+ return date ? dayjs.utc(date).format(format) : null
437
72
  }
438
73
 
439
- utils.getUTCDate = function (d = null) {
440
- if (typeof d === 'string') return dayjs.utc(d).startOf('d')
74
+ function toArray(value) {
75
+ if (Array.isArray(value)) return value.filter(Boolean)
441
76
 
442
- return dayjs.utc().startOf('d')
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
+ })