epg-grabber 0.28.1 → 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.
@@ -5,10 +5,14 @@ const program = new Command()
5
5
  const { merge } = require('lodash')
6
6
  const { gzip } = require('node-gzip')
7
7
  const file = require('../src/file')
8
- const { EPGGrabber, parseChannels, generateXMLTV, loadLogo } = require('../src/index')
8
+ const { EPGGrabber, parseChannels, generateXMLTV } = require('../src/index')
9
9
  const { create: createLogger } = require('../src/logger')
10
- const { parseInteger, getUTCDate } = require('../src/utils')
10
+ const { parseNumber, getUTCDate } = require('../src/utils')
11
11
  const { name, version, description } = require('../package.json')
12
+ const dayjs = require('dayjs')
13
+ const utc = require('dayjs/plugin/utc')
14
+
15
+ dayjs.extend(utc)
12
16
 
13
17
  program
14
18
  .name(name)
@@ -18,13 +22,13 @@ program
18
22
  .option('-o, --output <output>', 'Path to output file')
19
23
  .option('--channels <channels>', 'Path to channels.xml file')
20
24
  .option('--lang <lang>', 'Set default language for all programs')
21
- .option('--days <days>', 'Number of days for which to grab the program', parseInteger, 1)
22
- .option('--delay <delay>', 'Delay between requests (in mileseconds)', parseInteger)
23
- .option('--timeout <timeout>', 'Set a timeout for each request (in mileseconds)', parseInteger)
25
+ .option('--days <days>', 'Number of days for which to grab the program', parseNumber, 1)
26
+ .option('--delay <delay>', 'Delay between requests (in mileseconds)', parseNumber)
27
+ .option('--timeout <timeout>', 'Set a timeout for each request (in mileseconds)', parseNumber)
24
28
  .option(
25
29
  '--cache-ttl <cacheTtl>',
26
30
  'Maximum time for storing each request (in milliseconds)',
27
- parseInteger
31
+ parseNumber
28
32
  )
29
33
  .option('--gzip', 'Compress the output', false)
30
34
  .option('--debug', 'Enable debug mode', false)
@@ -80,9 +84,9 @@ async function main() {
80
84
  await grabber
81
85
  .grab(channel, date, (data, err) => {
82
86
  logger.info(
83
- `[${i}/${total}] ${config.site} - ${data.channel.xmltv_id} - ${data.date.format(
84
- 'MMM D, YYYY'
85
- )} (${data.programs.length} programs)`
87
+ `[${i}/${total}] ${config.site} - ${data.channel.id} - ${dayjs
88
+ .utc(data.date)
89
+ .format('MMM D, YYYY')} (${data.programs.length} programs)`
86
90
  )
87
91
 
88
92
  if (err) logger.error(err.message)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "epg-grabber",
3
- "version": "0.28.1",
3
+ "version": "0.28.2",
4
4
  "description": "Node.js CLI tool for grabbing EPG from different sites",
5
5
  "main": "src/index.js",
6
6
  "preferGlobal": true,
package/src/Channel.js ADDED
@@ -0,0 +1,19 @@
1
+ class Channel {
2
+ constructor(c) {
3
+ const data = {
4
+ id: c.xmltv_id,
5
+ name: c.name,
6
+ site: c.site || '',
7
+ site_id: c.site_id,
8
+ lang: c.lang || '',
9
+ logo: c.logo || '',
10
+ url: c.site ? `https://${c.site}` : ''
11
+ }
12
+
13
+ for (let key in data) {
14
+ this[key] = data[key]
15
+ }
16
+ }
17
+ }
18
+
19
+ module.exports = Channel
package/src/Program.js ADDED
@@ -0,0 +1,122 @@
1
+ const { padStart } = require('lodash')
2
+ const { toArray, toUnix, parseNumber } = require('./utils')
3
+
4
+ class Program {
5
+ constructor(p, c) {
6
+ const data = {
7
+ channel: c.id,
8
+ title: p.title,
9
+ sub_title: p.sub_title || '',
10
+ description: p.description || p.desc,
11
+ icon: toIconObject(p.icon),
12
+ episodeNumbers: getEpisodeNumbers(p.season, p.episode),
13
+ date: p.date ? toUnix(p.date) : null,
14
+ start: toUnix(p.start),
15
+ stop: toUnix(p.stop),
16
+ urls: toArray(p.url).map(toUrlObject),
17
+ ratings: toArray(p.ratings || p.rating).map(toRatingObject),
18
+ categories: toArray(p.categories || p.category),
19
+ directors: toArray(p.directors || p.director).map(toPersonObject),
20
+ actors: toArray(p.actors || p.actor).map(toPersonObject),
21
+ writers: toArray(p.writers || p.writer).map(toPersonObject),
22
+ adapters: toArray(p.adapters || p.adapter).map(toPersonObject),
23
+ producers: toArray(p.producers || p.producer).map(toPersonObject),
24
+ composers: toArray(p.composers || p.composer).map(toPersonObject),
25
+ editors: toArray(p.editors || p.editor).map(toPersonObject),
26
+ presenters: toArray(p.presenters || p.presenter).map(toPersonObject),
27
+ commentators: toArray(p.commentators || p.commentator).map(toPersonObject),
28
+ guests: toArray(p.guests || p.guest).map(toPersonObject)
29
+ }
30
+
31
+ for (let key in data) {
32
+ this[key] = data[key]
33
+ }
34
+ }
35
+ }
36
+
37
+ module.exports = Program
38
+
39
+ function toPersonObject(person) {
40
+ if (typeof person === 'string') {
41
+ return {
42
+ value: person,
43
+ url: [],
44
+ image: []
45
+ }
46
+ }
47
+
48
+ return {
49
+ value: person.value,
50
+ url: toArray(person.url).map(toUrlObject),
51
+ image: toArray(person.image).map(toImageObject)
52
+ }
53
+ }
54
+
55
+ function toImageObject(image) {
56
+ if (typeof image === 'string') return { type: '', size: '', orient: '', system: '', value: image }
57
+
58
+ return {
59
+ type: image.type || '',
60
+ size: image.size || '',
61
+ orient: image.orient || '',
62
+ system: image.system || '',
63
+ value: image.value
64
+ }
65
+ }
66
+
67
+ function toRatingObject(rating) {
68
+ if (typeof rating === 'string') return { system: '', icon: '', value: rating }
69
+
70
+ return {
71
+ system: rating.system || '',
72
+ icon: rating.icon || '',
73
+ value: rating.value || ''
74
+ }
75
+ }
76
+
77
+ function toUrlObject(url) {
78
+ if (typeof url === 'string') return { system: '', value: url }
79
+
80
+ return {
81
+ system: url.system || '',
82
+ value: url.value || ''
83
+ }
84
+ }
85
+
86
+ function toIconObject(icon) {
87
+ if (!icon || typeof icon === 'string') return { src: icon }
88
+
89
+ return {
90
+ src: icon.src || ''
91
+ }
92
+ }
93
+
94
+ function getEpisodeNumbers(s, e) {
95
+ s = parseNumber(s)
96
+ e = parseNumber(e)
97
+
98
+ return [createXMLTVNS(s, e), createOnScreen(s, e)].filter(Boolean)
99
+ }
100
+
101
+ function createXMLTVNS(s, e) {
102
+ if (!e) return null
103
+ s = s || 1
104
+
105
+ return {
106
+ system: 'xmltv_ns',
107
+ value: `${s - 1}.${e - 1}.0/1`
108
+ }
109
+ }
110
+
111
+ function createOnScreen(s, e) {
112
+ if (!e) return null
113
+ s = s || 1
114
+
115
+ s = padStart(s, 2, '0')
116
+ e = padStart(e, 2, '0')
117
+
118
+ return {
119
+ system: 'onscreen',
120
+ value: `S${s}E${e}`
121
+ }
122
+ }
package/src/index.js CHANGED
@@ -1,8 +1,7 @@
1
1
  const { merge } = require('lodash')
2
2
  const { create: createClient, buildRequest, parseResponse } = require('./client')
3
+ const { parseChannels, parsePrograms } = require('./parser')
3
4
  const { generate: generateXMLTV } = require('./xmltv')
4
- const { parse: parseChannels } = require('./channels')
5
- const { parse: parsePrograms } = require('./programs')
6
5
  const { load: loadConfig } = require('./config')
7
6
  const { sleep, isPromise } = require('./utils')
8
7
 
package/src/parser.js ADDED
@@ -0,0 +1,45 @@
1
+ const convert = require('xml-js')
2
+ const Channel = require('./Channel')
3
+ const Program = require('./Program')
4
+ const { isPromise } = require('./utils')
5
+
6
+ module.exports.parseChannels = parseChannels
7
+ module.exports.parsePrograms = parsePrograms
8
+
9
+ function parseChannels(xml) {
10
+ const result = convert.xml2js(xml)
11
+ const siteTag = result.elements.find(el => el.name === 'site') || {}
12
+ if (!siteTag.elements) return []
13
+ const rootSite = siteTag.attributes.site
14
+
15
+ const channelsTag = siteTag.elements.find(el => el.name === 'channels')
16
+ if (!channelsTag.elements) return []
17
+
18
+ const channels = channelsTag.elements
19
+ .filter(el => el.name === 'channel')
20
+ .map(el => {
21
+ let { xmltv_id, site, logo } = el.attributes
22
+ const name = el.elements.find(el => el.type === 'text').text
23
+ if (!name) throw new Error(`Channel '${xmltv_id}' has no valid name`)
24
+ site = site || rootSite
25
+
26
+ return new Channel({ name, xmltv_id, logo, site })
27
+ })
28
+
29
+ return { site: rootSite, channels }
30
+ }
31
+
32
+ async function parsePrograms(data) {
33
+ const { config, channel } = data
34
+ let programs = config.parser(data)
35
+
36
+ if (isPromise(programs)) {
37
+ programs = await programs
38
+ }
39
+
40
+ if (!Array.isArray(programs)) {
41
+ throw new Error('Parser should return an array')
42
+ }
43
+
44
+ return programs.filter(i => i).map(p => new Program(p, channel))
45
+ }
package/src/utils.js CHANGED
@@ -8,7 +8,10 @@ module.exports.getUTCDate = getUTCDate
8
8
  module.exports.isPromise = isPromise
9
9
  module.exports.isObject = isObject
10
10
  module.exports.escapeString = escapeString
11
- module.exports.parseInteger = parseInteger
11
+ module.exports.parseNumber = parseNumber
12
+ module.exports.formatDate = formatDate
13
+ module.exports.toArray = toArray
14
+ module.exports.toUnix = toUnix
12
15
 
13
16
  function sleep(ms) {
14
17
  return new Promise(resolve => setTimeout(resolve, ms))
@@ -28,6 +31,10 @@ function getUTCDate(d = null) {
28
31
  return dayjs.utc().startOf('d')
29
32
  }
30
33
 
34
+ function toUnix(d) {
35
+ return dayjs.utc(d).valueOf()
36
+ }
37
+
31
38
  function escapeString(string, defaultValue = '') {
32
39
  if (!string) return defaultValue
33
40
 
@@ -56,6 +63,16 @@ function escapeString(string, defaultValue = '') {
56
63
  .trim()
57
64
  }
58
65
 
59
- function parseInteger(val) {
66
+ function parseNumber(val) {
60
67
  return val ? parseInt(val) : null
61
68
  }
69
+
70
+ function formatDate(date, format) {
71
+ return date ? dayjs.utc(date).format(format) : null
72
+ }
73
+
74
+ function toArray(value) {
75
+ if (Array.isArray(value)) return value.filter(Boolean)
76
+
77
+ return [value].filter(Boolean)
78
+ }
package/src/xmltv.js CHANGED
@@ -1,9 +1,5 @@
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)
1
+ const { escapeString, getUTCDate, formatDate } = require('./utils')
2
+ const el = createElement
7
3
 
8
4
  module.exports.generate = generate
9
5
 
@@ -14,26 +10,19 @@ function generate({ channels, programs, date = getUTCDate() }) {
14
10
  return output
15
11
  }
16
12
 
17
- const el = createElement
18
-
19
13
  function createElements(channels, programs, date) {
20
- date = formatDate(date, 'YYYYMMDD')
21
- return el('tv', { date }, [
14
+ return el('tv', { date: formatDate(date, 'YYYYMMDD') }, [
22
15
  ...channels.map(channel => {
23
- const url = channel.site ? `https://${channel.site}` : ''
24
-
25
16
  return (
26
17
  '\r\n' +
27
- el('channel', { id: channel.xmltv_id }, [
18
+ el('channel', { id: channel.id }, [
28
19
  el('display-name', {}, [escapeString(channel.name)]),
29
20
  el('icon', { src: channel.logo }),
30
- el('url', {}, [url])
21
+ el('url', {}, [channel.url])
31
22
  ])
32
23
  )
33
24
  }),
34
25
  ...programs.map(program => {
35
- const programDate = program.date ? formatDate(program.date, 'YYYYMMDD') : ''
36
-
37
26
  return (
38
27
  '\r\n' +
39
28
  el(
@@ -48,30 +37,25 @@ function createElements(channels, programs, date) {
48
37
  el('sub-title', {}, [escapeString(program.sub_title)]),
49
38
  el('desc', {}, [escapeString(program.description)]),
50
39
  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))
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))
61
50
  ]),
62
- el('date', {}, [programDate]),
63
- ...toArray(program.category).map(category =>
64
- el('category', {}, [escapeString(category)])
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])
65
57
  ),
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 =>
58
+ ...program.ratings.map(rating =>
75
59
  el('rating', { system: rating.system }, [
76
60
  el('value', {}, [rating.value]),
77
61
  el('icon', { src: rating.icon })
@@ -84,45 +68,15 @@ function createElements(channels, programs, date) {
84
68
  ])
85
69
  }
86
70
 
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
71
  function createCastMember(position, data) {
116
- data = toObject(data)
117
72
  return el(position, {}, [
118
73
  escapeString(data.value),
119
- ...toArray(data.url).map(createURL),
120
- ...toArray(data.image).map(createImage)
74
+ ...data.url.map(createURL),
75
+ ...data.image.map(createImage)
121
76
  ])
122
77
  }
123
78
 
124
79
  function createImage(image) {
125
- image = toObject(image)
126
80
  return el(
127
81
  'image',
128
82
  {
@@ -136,32 +90,15 @@ function createImage(image) {
136
90
  }
137
91
 
138
92
  function createURL(url) {
139
- url = toObject(url)
140
93
  return el('url', { system: url.system }, [url.value])
141
94
  }
142
95
 
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
96
  function createElement(name, attrs = {}, children = []) {
160
97
  return toString({ name, attrs, children })
161
98
  }
162
99
 
163
100
  function toString(elem) {
164
- if (typeof elem === 'string') return elem
101
+ if (typeof elem === 'string' || typeof elem === 'number') return elem
165
102
 
166
103
  let attrs = ''
167
104
  for (let key in elem.attrs) {
@@ -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
+ })
@@ -0,0 +1,163 @@
1
+ import Channel from '../src/Channel'
2
+ import Program from '../src/Program'
3
+
4
+ const channel = new Channel({ xmltv_id: '1tv', lang: 'en' })
5
+
6
+ it('can create new Program', () => {
7
+ const program = new Program(
8
+ {
9
+ title: 'Title',
10
+ sub_title: 'Subtitle',
11
+ description: 'Description',
12
+ icon: 'https://example.com/image.jpg',
13
+ season: 9,
14
+ episode: 238,
15
+ date: '20220506',
16
+ start: 1616133600000,
17
+ stop: '2021-03-19T06:30:00.000Z',
18
+ url: 'http://example.com/title.html',
19
+ category: ['Category1', 'Category2'],
20
+ rating: {
21
+ system: 'MPAA',
22
+ value: 'PG',
23
+ icon: 'http://example.com/pg_symbol.png'
24
+ },
25
+ directors: 'Director1',
26
+ actors: [
27
+ 'Actor1',
28
+ { value: 'Actor2', url: 'http://actor2.com', image: 'http://actor2.com/image.png' }
29
+ ],
30
+ writer: {
31
+ value: 'Writer1',
32
+ url: { system: 'imdb', value: 'http://imdb.com/p/writer1' },
33
+ image: {
34
+ value: 'https://example.com/image.jpg',
35
+ type: 'person',
36
+ size: '2',
37
+ system: 'TestSystem',
38
+ orient: 'P'
39
+ }
40
+ },
41
+ adapters: [
42
+ {
43
+ value: 'Adapter1',
44
+ url: ['http://imdb.com/p/adapter1', 'http://imdb.com/p/adapter2'],
45
+ image: ['https://example.com/image1.jpg', 'https://example.com/image2.jpg']
46
+ }
47
+ ]
48
+ },
49
+ channel
50
+ )
51
+
52
+ expect(program).toMatchObject({
53
+ channel: '1tv',
54
+ title: 'Title',
55
+ sub_title: 'Subtitle',
56
+ description: 'Description',
57
+ urls: [{ system: '', value: 'http://example.com/title.html' }],
58
+ categories: ['Category1', 'Category2'],
59
+ icon: { src: 'https://example.com/image.jpg' },
60
+ episodeNumbers: [
61
+ { system: 'xmltv_ns', value: '8.237.0/1' },
62
+ { system: 'onscreen', value: 'S09E238' }
63
+ ],
64
+ date: 1651795200000,
65
+ start: 1616133600000,
66
+ stop: 1616135400000,
67
+ ratings: [
68
+ {
69
+ system: 'MPAA',
70
+ value: 'PG',
71
+ icon: 'http://example.com/pg_symbol.png'
72
+ }
73
+ ],
74
+ directors: [{ value: 'Director1', url: [], image: [] }],
75
+ actors: [
76
+ { value: 'Actor1', url: [], image: [] },
77
+ {
78
+ value: 'Actor2',
79
+ url: [{ system: '', value: 'http://actor2.com' }],
80
+ image: [
81
+ { type: '', size: '', orient: '', system: '', value: 'http://actor2.com/image.png' }
82
+ ]
83
+ }
84
+ ],
85
+ writers: [
86
+ {
87
+ value: 'Writer1',
88
+ url: [{ system: 'imdb', value: 'http://imdb.com/p/writer1' }],
89
+ image: [
90
+ {
91
+ value: 'https://example.com/image.jpg',
92
+ type: 'person',
93
+ size: '2',
94
+ system: 'TestSystem',
95
+ orient: 'P'
96
+ }
97
+ ]
98
+ }
99
+ ],
100
+ adapters: [
101
+ {
102
+ value: 'Adapter1',
103
+ url: [
104
+ { system: '', value: 'http://imdb.com/p/adapter1' },
105
+ { system: '', value: 'http://imdb.com/p/adapter2' }
106
+ ],
107
+ image: [
108
+ {
109
+ value: 'https://example.com/image1.jpg',
110
+ type: '',
111
+ size: '',
112
+ system: '',
113
+ orient: ''
114
+ },
115
+ {
116
+ value: 'https://example.com/image2.jpg',
117
+ type: '',
118
+ size: '',
119
+ system: '',
120
+ orient: ''
121
+ }
122
+ ]
123
+ }
124
+ ],
125
+ producers: [],
126
+ composers: [],
127
+ editors: [],
128
+ presenters: [],
129
+ commentators: [],
130
+ guests: []
131
+ })
132
+ })
133
+
134
+ it('can create program without season number', () => {
135
+ const program = new Program(
136
+ {
137
+ title: 'Program 1',
138
+ start: '2021-03-19T06:00:00.000Z',
139
+ stop: '2021-03-19T06:30:00.000Z',
140
+ episode: 238
141
+ },
142
+ channel
143
+ )
144
+
145
+ expect(program.episodeNumbers).toMatchObject([
146
+ { system: 'xmltv_ns', value: '0.237.0/1' },
147
+ { system: 'onscreen', value: 'S01E238' }
148
+ ])
149
+ })
150
+
151
+ it('can create program without episode number', () => {
152
+ const program = new Program(
153
+ {
154
+ title: 'Program 1',
155
+ start: '2021-03-19T06:00:00.000Z',
156
+ stop: '2021-03-19T06:30:00.000Z',
157
+ season: 3
158
+ },
159
+ channel
160
+ )
161
+
162
+ expect(program.episodeNumbers).toMatchObject([])
163
+ })
package/tests/bin.test.js CHANGED
@@ -10,7 +10,7 @@ function stdoutResultTester(stdout) {
10
10
 
11
11
  it('can load config', () => {
12
12
  const result = execSync(
13
- `node ${pwd}/bin/epg-grabber.js --config=tests/input/example.com.config.js --delay=0`,
13
+ `node ${pwd}/bin/epg-grabber.js --config=tests/input/example.config.js --delay=0`,
14
14
  {
15
15
  encoding: 'utf8'
16
16
  }
@@ -23,7 +23,7 @@ it('can load mini config', () => {
23
23
  const result = execSync(
24
24
  `node ${pwd}/bin/epg-grabber.js \
25
25
  --config=tests/input/mini.config.js \
26
- --channels=tests/input/example.com.channels.xml \
26
+ --channels=tests/input/example.channels.xml \
27
27
  --output=tests/output/mini.guide.xml \
28
28
  --lang=fr \
29
29
  --days=3 \
@@ -43,7 +43,7 @@ it('can generate gzip version', () => {
43
43
  const result = execSync(
44
44
  `node ${pwd}/bin/epg-grabber.js \
45
45
  --config=tests/input/mini.config.js \
46
- --channels=tests/input/example.com.channels.xml \
46
+ --channels=tests/input/example.channels.xml \
47
47
  --output=tests/output/mini.guide.xml.gz \
48
48
  --gzip`,
49
49
  {
@@ -3,7 +3,7 @@ import path from 'path'
3
3
  import fs from 'fs'
4
4
 
5
5
  it('can load config', () => {
6
- const config = loadConfig(require(path.resolve('./tests/input/example.com.config.js')))
6
+ const config = loadConfig(require(path.resolve('./tests/input/example.config.js')))
7
7
  expect(config).toMatchObject({
8
8
  days: 1,
9
9
  delay: 3000,
@@ -1,6 +1,6 @@
1
1
  module.exports = {
2
2
  site: 'example.com',
3
- channels: 'example.com.channels.xml',
3
+ channels: 'example.channels.xml',
4
4
  url() {
5
5
  return Promise.resolve('http://example.com/20210319/1tv.json')
6
6
  },
@@ -17,7 +17,13 @@ module.exports = {
17
17
  }
18
18
  },
19
19
  parser() {
20
- return Promise.resolve([])
20
+ return Promise.resolve([
21
+ {
22
+ title: 'Program1',
23
+ start: 1640995200000,
24
+ stop: 1640998800000
25
+ }
26
+ ])
21
27
  },
22
28
  logo() {
23
29
  return Promise.resolve('http://example.com/logos/1TV.png?x=шеллы&sid=777')
@@ -5,7 +5,7 @@ dayjs.extend(utc)
5
5
 
6
6
  module.exports = {
7
7
  site: 'example.com',
8
- channels: 'example.com.channels.xml',
8
+ channels: 'example.channels.xml',
9
9
  output: 'tests/output/guide.xml',
10
10
  url: () => 'http://example.com/20210319/1tv.json',
11
11
  request: {
@@ -21,15 +21,9 @@ module.exports = {
21
21
  parser: () => {
22
22
  return [
23
23
  {
24
- title: 'Title',
25
- description: 'Description',
26
- lang: 'en',
27
- category: ['Category1', 'Category2'],
28
- icon: 'https://example.com/image.jpg',
29
- season: 9,
30
- episode: 238,
31
- start: dayjs.utc('2022-01-01 00:00:00'),
32
- stop: dayjs.utc('2022-01-01 01:00:00')
24
+ title: 'Program1',
25
+ start: 1640995200000,
26
+ stop: 1640998800000
33
27
  }
34
28
  ]
35
29
  },
@@ -0,0 +1,40 @@
1
+ import { parseChannels, parsePrograms } from '../src/parser'
2
+ import Channel from '../src/Channel'
3
+ import Program from '../src/Program'
4
+ import fs from 'fs'
5
+
6
+ it('can parse valid channels.xml', () => {
7
+ const file = fs.readFileSync('./tests/input/example.channels.xml', { encoding: 'utf-8' })
8
+ const { channels, site } = parseChannels(file)
9
+
10
+ expect(typeof site).toBe('string')
11
+ expect(channels.length).toBe(2)
12
+ expect(channels[0]).toBeInstanceOf(Channel)
13
+ expect(channels[1]).toBeInstanceOf(Channel)
14
+ })
15
+
16
+ it('can parse programs', done => {
17
+ const channel = { xmltv_id: '1tv' }
18
+ const config = require('./input/example.config.js')
19
+
20
+ parsePrograms({ channel, config })
21
+ .then(programs => {
22
+ expect(programs.length).toBe(1)
23
+ expect(programs[0]).toBeInstanceOf(Program)
24
+ done()
25
+ })
26
+ .catch(done)
27
+ })
28
+
29
+ it('can parse programs async', done => {
30
+ const channel = { xmltv_id: '1tv' }
31
+ const config = require('./input/async.config.js')
32
+
33
+ parsePrograms({ channel, config })
34
+ .then(programs => {
35
+ expect(programs.length).toBe(1)
36
+ expect(programs[0]).toBeInstanceOf(Program)
37
+ done()
38
+ })
39
+ .catch(done)
40
+ })
@@ -1,146 +1,71 @@
1
+ import Channel from '../src/Channel'
2
+ import Program from '../src/Program'
1
3
  import xmltv from '../src/xmltv'
2
4
 
3
5
  jest.useFakeTimers('modern').setSystemTime(new Date('2022-05-05'))
4
6
 
5
7
  const channels = [
6
- {
8
+ new Channel({
7
9
  xmltv_id: '1TV.co',
8
10
  name: '1 TV',
9
11
  logo: 'https://example.com/logos/1TV.png',
10
12
  site: 'example.com'
11
- },
12
- {
13
+ }),
14
+ new Channel({
13
15
  xmltv_id: '2TV.co',
14
16
  name: '2 TV',
15
17
  site: 'example.com'
16
- }
18
+ })
17
19
  ]
18
20
 
19
- it('can generate xmltv', () => {
21
+ fit('can generate xmltv', () => {
20
22
  const programs = [
21
- {
22
- title: 'Program 1',
23
- sub_title: 'Sub-title & 1',
24
- description: 'Description for Program 1',
25
- url: 'http://example.com/title.html',
26
- start: '2021-03-19T06:00:00.000Z',
27
- stop: '2021-03-19T06:30:00.000Z',
28
- category: 'Test',
29
- date: '2022-05-06',
30
- season: 9,
31
- episode: 239,
32
- icon: 'https://example.com/images/Program1.png?x=шеллы&sid=777',
33
- channel: '1TV.co',
34
- rating: {
35
- system: 'MPAA',
36
- value: 'PG',
37
- icon: 'http://example.com/pg_symbol.png'
38
- },
39
- director: [
40
- {
41
- value: 'Director 1',
42
- url: { value: 'http://example.com/director1.html', system: 'TestSystem' },
43
- image: [
44
- 'https://example.com/image1.jpg',
45
- {
46
- value: 'https://example.com/image2.jpg',
47
- type: 'person',
48
- size: '2',
49
- system: 'TestSystem',
50
- orient: 'P'
51
- }
52
- ]
23
+ new Program(
24
+ {
25
+ title: 'Program 1',
26
+ sub_title: 'Sub-title & 1',
27
+ description: 'Description for Program 1',
28
+ url: 'http://example.com/title.html',
29
+ start: '2021-03-19T06:00:00.000Z',
30
+ stop: '2021-03-19T06:30:00.000Z',
31
+ category: 'Test',
32
+ date: '2022-05-06',
33
+ season: 9,
34
+ episode: 239,
35
+ icon: 'https://example.com/images/Program1.png?x=шеллы&sid=777',
36
+ channel: '1TV.co',
37
+ rating: {
38
+ system: 'MPAA',
39
+ value: 'PG',
40
+ icon: 'http://example.com/pg_symbol.png'
53
41
  },
54
- 'Director 2'
55
- ],
56
- actor: ['Actor 1', 'Actor 2'],
57
- writer: 'Writer 1'
58
- }
59
- ]
60
- const output = xmltv.generate({ channels, programs })
61
- expect(output).toBe(
62
- '<?xml version="1.0" encoding="UTF-8" ?><tv date="20220505">\r\n<channel id="1TV.co"><display-name>1 TV</display-name><icon src="https://example.com/logos/1TV.png"/><url>https://example.com</url></channel>\r\n<channel id="2TV.co"><display-name>2 TV</display-name><url>https://example.com</url></channel>\r\n<programme start="20210319060000 +0000" stop="20210319063000 +0000" channel="1TV.co"><title>Program 1</title><sub-title>Sub-title &amp; 1</sub-title><desc>Description for Program 1</desc><credits><director>Director 1<url system="TestSystem">http://example.com/director1.html</url><image>https://example.com/image1.jpg</image><image type="person" size="2" orient="P" system="TestSystem">https://example.com/image2.jpg</image></director><director>Director 2</director><actor>Actor 1</actor><actor>Actor 2</actor><writer>Writer 1</writer></credits><date>20220506</date><category>Test</category><icon src="https://example.com/images/Program1.png?x=шеллы&amp;sid=777"/><url>http://example.com/title.html</url><episode-num system="xmltv_ns">8.238.0/1</episode-num><episode-num system="onscreen">S09E239</episode-num><rating system="MPAA"><value>PG</value><icon src="http://example.com/pg_symbol.png"/></rating></programme></tv>'
63
- )
64
- })
65
-
66
- it('can generate xmltv without season number', () => {
67
- const programs = [
68
- {
69
- channel: '1TV.co',
70
- title: 'Program 1',
71
- start: '2021-03-19T06:00:00.000Z',
72
- stop: '2021-03-19T06:30:00.000Z',
73
- episode: 239
74
- }
75
- ]
76
- const output = xmltv.generate({ channels, programs })
77
- expect(output).toBe(
78
- '<?xml version="1.0" encoding="UTF-8" ?><tv date="20220505">\r\n<channel id="1TV.co"><display-name>1 TV</display-name><icon src="https://example.com/logos/1TV.png"/><url>https://example.com</url></channel>\r\n<channel id="2TV.co"><display-name>2 TV</display-name><url>https://example.com</url></channel>\r\n<programme start="20210319060000 +0000" stop="20210319063000 +0000" channel="1TV.co"><title>Program 1</title><episode-num system="xmltv_ns">0.238.0/1</episode-num><episode-num system="onscreen">S01E239</episode-num></programme></tv>'
79
- )
80
- })
81
-
82
- it('can generate xmltv without episode number', () => {
83
- const programs = [
84
- {
85
- channel: '1TV.co',
86
- title: 'Program 1',
87
- start: '2021-03-19T06:00:00.000Z',
88
- stop: '2021-03-19T06:30:00.000Z',
89
- season: 1
90
- }
91
- ]
92
- const output = xmltv.generate({ channels, programs })
93
- expect(output).toBe(
94
- '<?xml version="1.0" encoding="UTF-8" ?><tv date="20220505">\r\n<channel id="1TV.co"><display-name>1 TV</display-name><icon src="https://example.com/logos/1TV.png"/><url>https://example.com</url></channel>\r\n<channel id="2TV.co"><display-name>2 TV</display-name><url>https://example.com</url></channel>\r\n<programme start="20210319060000 +0000" stop="20210319063000 +0000" channel="1TV.co"><title>Program 1</title></programme></tv>'
95
- )
96
- })
97
-
98
- it('can generate xmltv without categories', () => {
99
- const programs = [
100
- {
101
- channel: '1TV.co',
102
- title: 'Program 1',
103
- start: '2021-03-19T06:00:00.000Z',
104
- stop: '2021-03-19T06:30:00.000Z'
105
- }
42
+ director: [
43
+ {
44
+ value: 'Director 1',
45
+ url: { value: 'http://example.com/director1.html', system: 'TestSystem' },
46
+ image: [
47
+ 'https://example.com/image1.jpg',
48
+ {
49
+ value: 'https://example.com/image2.jpg',
50
+ type: 'person',
51
+ size: '2',
52
+ system: 'TestSystem',
53
+ orient: 'P'
54
+ }
55
+ ]
56
+ },
57
+ 'Director 2'
58
+ ],
59
+ actor: ['Actor 1', 'Actor 2'],
60
+ writer: 'Writer 1'
61
+ },
62
+ channels[0]
63
+ )
106
64
  ]
107
- const output = xmltv.generate({ channels, programs })
108
- expect(output).toBe(
109
- '<?xml version="1.0" encoding="UTF-8" ?><tv date="20220505">\r\n<channel id="1TV.co"><display-name>1 TV</display-name><icon src="https://example.com/logos/1TV.png"/><url>https://example.com</url></channel>\r\n<channel id="2TV.co"><display-name>2 TV</display-name><url>https://example.com</url></channel>\r\n<programme start="20210319060000 +0000" stop="20210319063000 +0000" channel="1TV.co"><title>Program 1</title></programme></tv>'
110
- )
111
- })
112
65
 
113
- it('can generate xmltv with multiple categories', () => {
114
- const programs = [
115
- {
116
- channel: '1TV.co',
117
- title: 'Program 1',
118
- start: '2021-03-19T06:00:00.000Z',
119
- stop: '2021-03-19T06:30:00.000Z',
120
- category: ['Category 1', 'Category 2']
121
- }
122
- ]
123
66
  const output = xmltv.generate({ channels, programs })
124
- expect(output).toBe(
125
- '<?xml version="1.0" encoding="UTF-8" ?><tv date="20220505">\r\n<channel id="1TV.co"><display-name>1 TV</display-name><icon src="https://example.com/logos/1TV.png"/><url>https://example.com</url></channel>\r\n<channel id="2TV.co"><display-name>2 TV</display-name><url>https://example.com</url></channel>\r\n<programme start="20210319060000 +0000" stop="20210319063000 +0000" channel="1TV.co"><title>Program 1</title><category>Category 1</category><category>Category 2</category></programme></tv>'
126
- )
127
- })
128
67
 
129
- it('can generate xmltv with multiple urls', () => {
130
- const programs = [
131
- {
132
- channel: '1TV.co',
133
- title: 'Program 1',
134
- start: '2021-03-19T06:00:00.000Z',
135
- stop: '2021-03-19T06:30:00.000Z',
136
- url: [
137
- 'https://example.com/noattr.html',
138
- { value: 'https://example.com/attr.html', system: 'TestSystem' }
139
- ]
140
- }
141
- ]
142
- const output = xmltv.generate({ channels, programs })
143
68
  expect(output).toBe(
144
- '<?xml version="1.0" encoding="UTF-8" ?><tv date="20220505">\r\n<channel id="1TV.co"><display-name>1 TV</display-name><icon src="https://example.com/logos/1TV.png"/><url>https://example.com</url></channel>\r\n<channel id="2TV.co"><display-name>2 TV</display-name><url>https://example.com</url></channel>\r\n<programme start="20210319060000 +0000" stop="20210319063000 +0000" channel="1TV.co"><title>Program 1</title><url>https://example.com/noattr.html</url><url system="TestSystem">https://example.com/attr.html</url></programme></tv>'
69
+ '<?xml version="1.0" encoding="UTF-8" ?><tv date="20220505">\r\n<channel id="1TV.co"><display-name>1 TV</display-name><icon src="https://example.com/logos/1TV.png"/><url>https://example.com</url></channel>\r\n<channel id="2TV.co"><display-name>2 TV</display-name><url>https://example.com</url></channel>\r\n<programme start="20210319060000 +0000" stop="20210319063000 +0000" channel="1TV.co"><title>Program 1</title><sub-title>Sub-title &amp; 1</sub-title><desc>Description for Program 1</desc><credits><director>Director 1<url system="TestSystem">http://example.com/director1.html</url><image>https://example.com/image1.jpg</image><image type="person" size="2" orient="P" system="TestSystem">https://example.com/image2.jpg</image></director><director>Director 2</director><actor>Actor 1</actor><actor>Actor 2</actor><writer>Writer 1</writer></credits><date>20220506</date><category>Test</category><icon src="https://example.com/images/Program1.png?x=шеллы&amp;sid=777"/><url>http://example.com/title.html</url><episode-num system="xmltv_ns">8.238.0/1</episode-num><episode-num system="onscreen">S09E239</episode-num><rating system="MPAA"><value>PG</value><icon src="http://example.com/pg_symbol.png"/></rating></programme></tv>'
145
70
  )
146
71
  })
package/src/channels.js DELETED
@@ -1,26 +0,0 @@
1
- const convert = require('xml-js')
2
-
3
- module.exports.parse = parse
4
-
5
- function parse(xml) {
6
- const result = convert.xml2js(xml)
7
- const siteTag = result.elements.find(el => el.name === 'site') || {}
8
- if (!siteTag.elements) return []
9
- const site = siteTag.attributes.site
10
-
11
- const channelsTag = siteTag.elements.find(el => el.name === 'channels')
12
- if (!channelsTag.elements) return []
13
-
14
- const channels = channelsTag.elements
15
- .filter(el => el.name === 'channel')
16
- .map(el => {
17
- const channel = el.attributes
18
- if (!el.elements) throw new Error(`Channel '${channel.xmltv_id}' has no valid name`)
19
- channel.name = el.elements.find(el => el.type === 'text').text
20
- channel.site = channel.site || site
21
-
22
- return channel
23
- })
24
-
25
- return { site, channels }
26
- }
package/src/programs.js DELETED
@@ -1,24 +0,0 @@
1
- const { isPromise } = require('./utils')
2
-
3
- module.exports.parse = parse
4
-
5
- async function parse(data) {
6
- const { config, channel } = data
7
- let programs = config.parser(data)
8
-
9
- if (isPromise(programs)) {
10
- programs = await programs
11
- }
12
-
13
- if (!Array.isArray(programs)) {
14
- throw new Error('Parser should return an array')
15
- }
16
-
17
- return programs
18
- .filter(i => i)
19
- .map(program => {
20
- program.channel = channel.xmltv_id
21
-
22
- return program
23
- })
24
- }
@@ -1,26 +0,0 @@
1
- import { parse as parseChannels } from '../src/channels'
2
- import path from 'path'
3
- import fs from 'fs'
4
-
5
- it('can parse valid channels.xml', () => {
6
- const file = fs.readFileSync('./tests/input/example.com.channels.xml', { encoding: 'utf-8' })
7
- const { channels } = parseChannels(file)
8
- expect(channels).toEqual([
9
- {
10
- name: '1 TV',
11
- xmltv_id: '1TV.com',
12
- site_id: '1',
13
- site: 'example.com',
14
- lang: 'fr',
15
- logo: 'https://example.com/logos/1TV.png'
16
- },
17
- {
18
- name: '2 TV',
19
- xmltv_id: '2TV.com',
20
- site_id: '2',
21
- site: 'example.com',
22
- lang: undefined,
23
- logo: undefined
24
- }
25
- ])
26
- })
@@ -1,63 +0,0 @@
1
- import { parse as parsePrograms } from '../src/programs'
2
-
3
- const channel = { xmltv_id: '1tv', lang: 'en' }
4
-
5
- it('can parse programs', done => {
6
- const config = {
7
- parser: () => [
8
- {
9
- title: 'Title',
10
- description: 'Description',
11
- category: ['Category1', 'Category2'],
12
- icon: 'https://example.com/image.jpg',
13
- season: 9,
14
- episode: 238,
15
- start: 1640995200,
16
- stop: 1640998800
17
- }
18
- ]
19
- }
20
-
21
- parsePrograms({ channel, config })
22
- .then(programs => {
23
- expect(programs).toMatchObject([
24
- {
25
- title: 'Title',
26
- description: 'Description',
27
- category: ['Category1', 'Category2'],
28
- icon: 'https://example.com/image.jpg',
29
- season: 9,
30
- episode: 238,
31
- start: 1640995200,
32
- stop: 1640998800,
33
- channel: '1tv'
34
- }
35
- ])
36
- done()
37
- })
38
- .catch(done)
39
- })
40
-
41
- it('can parse programs async', done => {
42
- const config = {
43
- parser: async () => [
44
- {
45
- title: 'Title',
46
- description: 'Description',
47
- category: ['Category1', 'Category2'],
48
- icon: 'https://example.com/image.jpg',
49
- season: 9,
50
- episode: 238,
51
- start: 1640995200,
52
- stop: 1640998800
53
- }
54
- ]
55
- }
56
-
57
- parsePrograms({ channel, config })
58
- .then(programs => {
59
- expect(programs.length).toBe(1)
60
- done()
61
- })
62
- .catch(done)
63
- })