epg-grabber 0.36.0 → 0.37.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 CHANGED
@@ -172,27 +172,9 @@ module.exports = {
172
172
 
173
173
  return [
174
174
  {
175
- title, // program title (required)
176
- start, // start time of the program (required)
177
- stop, // end time of the program (required)
178
- sub_title, // program sub-title (optional)
179
- description, // description of the program (optional)
180
- category, // type of program (optional)
181
- season, // season number (optional)
182
- episode, // episode number (optional)
183
- date, // the date the programme or film was finished (optional)
184
- icon, // image associated with the program (optional)
185
- rating, // program rating (optional)
186
- director, // the name of director (optional)
187
- actor, // the name of actor (optional)
188
- writer, // the name of writer (optional)
189
- adapter, // the name of adapter (optional)
190
- producer, // the name of producer (optional)
191
- composer, // the name of composer (optional)
192
- editor, // the name of editor (optional)
193
- presenter, // the name of presenter (optional)
194
- commentator, // the name of commentator (optional)
195
- guest // the name of guest (optional)
175
+ title,
176
+ start,
177
+ stop
196
178
  },
197
179
  ...
198
180
  ]
@@ -212,6 +194,176 @@ From each function in `config.js` you can access a `context` object containing t
212
194
  - `request`: The request config
213
195
  - `cached`: A boolean to check whether this request was cached or not
214
196
 
197
+ ## Program Object
198
+
199
+ | Name | Aliases | Type | Required |
200
+ | --------------- | -------------------------------- | ------------------------------------------------ | -------- |
201
+ | start | | `String` or `Number` or `Date()` | true |
202
+ | stop | | `String` or `Number` or `Date()` | true |
203
+ | title | titles | `String` or `Object` or `String[]` or `Object[]` | true |
204
+ | subTitle | subTitles, sub_title, sub_titles | `String` or `Object` or `String[]` or `Object[]` | false |
205
+ | description | desc, descriptions | `String` or `Object` or `String[]` or `Object[]` | false |
206
+ | date | | `String` or `Number` or `Date()` | false |
207
+ | category | categories | `String` or `Object` or `String[]` or `Object[]` | false |
208
+ | keyword | keywords | `String` or `Object` or `String[]` or `Object[]` | false |
209
+ | language | languages | `String` or `Object` or `String[]` or `Object[]` | false |
210
+ | origLanguage | origLanguages | `String` or `Object` or `String[]` or `Object[]` | false |
211
+ | length | | `String` or `Object` or `String[]` or `Object[]` | false |
212
+ | url | urls | `String` or `Object` or `String[]` or `Object[]` | false |
213
+ | country | countries | `String` or `Object` or `String[]` or `Object[]` | false |
214
+ | video | | `Object` | false |
215
+ | audio | | `Object` | false |
216
+ | season | | `String` or `Number` | false |
217
+ | episode | | `String` or `Number` | false |
218
+ | episodeNumber | episodeNum, episodeNumbers | `Object` | false |
219
+ | previouslyShown | | `String` or `Object` or `String[]` or `Object[]` | false |
220
+ | premiere | | `String` or `Object` or `String[]` or `Object[]` | false |
221
+ | lastChance | | `String` or `Object` or `String[]` or `Object[]` | false |
222
+ | new | | `Boolean` | false |
223
+ | subtitles | | `Object` or `Object[]` | false |
224
+ | rating | ratings | `String` or `Object` or `String[]` or `Object[]` | false |
225
+ | starRating | starRatings | `String` or `Object` or `String[]` or `Object[]` | false |
226
+ | review | reviews | `String` or `Object` or `String[]` or `Object[]` | false |
227
+ | director | directors | `String` or `Object` or `String[]` or `Object[]` | false |
228
+ | actor | actors | `String` or `Object` or `String[]` or `Object[]` | false |
229
+ | writer | writers | `String` or `Object` or `String[]` or `Object[]` | false |
230
+ | adapter | adapters | `String` or `Object` or `String[]` or `Object[]` | false |
231
+ | producer | producers | `String` or `Object` or `String[]` or `Object[]` | false |
232
+ | presenter | presenters | `String` or `Object` or `String[]` or `Object[]` | false |
233
+ | composer | composers | `String` or `Object` or `String[]` or `Object[]` | false |
234
+ | editor | editors | `String` or `Object` or `String[]` or `Object[]` | false |
235
+ | commentator | commentators | `String` or `Object` or `String[]` or `Object[]` | false |
236
+ | guest | guests | `String` or `Object` or `String[]` or `Object[]` | false |
237
+ | image | images | `String` or `Object` or `String[]` or `Object[]` | false |
238
+ | icon | icons | `String` or `Object` or `String[]` or `Object[]` | false |
239
+
240
+ Example:
241
+
242
+ ```js
243
+ {
244
+ start: '2021-03-19T06:00:00.000Z',
245
+ stop: '2021-03-19T06:30:00.000Z',
246
+ title: 'Program 1',
247
+ subTitle: 'Sub-title & 1',
248
+ description: 'Description for Program 1',
249
+ date: '2022-05-06',
250
+ categories: ['Comedy', 'Drama'],
251
+ keyword: [
252
+ { lang: 'en', value: 'physical-comedy' },
253
+ { lang: 'en', value: 'romantic' }
254
+ ],
255
+ language: 'English',
256
+ origLanguage: { lang: 'en', value: 'French' },
257
+ length: { units: 'minutes', value: '60' },
258
+ url: 'http://example.com/title.html',
259
+ country: 'US',
260
+ video: {
261
+ present: 'yes',
262
+ colour: 'no',
263
+ aspect: '16:9',
264
+ quality: 'HDTV'
265
+ },
266
+ audio: {
267
+ present: 'yes',
268
+ stereo: 'Dolby Digital'
269
+ },
270
+ season: 9,
271
+ episode: 239,
272
+ previouslyShown: [{ start: '20080711000000', channel: 'channel-two.tv' }],
273
+ premiere: 'First time on British TV',
274
+ lastChance: [{ lang: 'en', value: 'Last time on this channel' }],
275
+ new: true,
276
+ subtitles: [
277
+ { type: 'teletext', language: 'English' },
278
+ { type: 'onscreen', language: [{ lang: 'en', value: 'Spanish' }] }
279
+ ],
280
+ rating: {
281
+ system: 'MPAA',
282
+ value: 'P&G',
283
+ icon: 'http://example.com/pg_symbol.png'
284
+ },
285
+ starRatings: [
286
+ {
287
+ system: 'TV Guide',
288
+ value: '4/5',
289
+ icon: [{ src: 'stars.png', width: 100, height: 100 }]
290
+ },
291
+ {
292
+ system: 'IMDB',
293
+ value: '8/10'
294
+ }
295
+ ],
296
+ reviews: [
297
+ {
298
+ type: 'text',
299
+ source: 'Rotten Tomatoes',
300
+ reviewer: 'Joe Bloggs',
301
+ lang: 'en',
302
+ value: 'This is a fantastic show!'
303
+ },
304
+ {
305
+ type: 'text',
306
+ source: 'IDMB',
307
+ reviewer: 'Jane Doe',
308
+ lang: 'en',
309
+ value: 'I love this show!'
310
+ },
311
+ {
312
+ type: 'url',
313
+ source: 'Rotten Tomatoes',
314
+ reviewer: 'Joe Bloggs',
315
+ lang: 'en',
316
+ value: 'https://example.com/programme_one_review'
317
+ }
318
+ ],
319
+ directors: [
320
+ {
321
+ value: 'Director 1',
322
+ url: { value: 'http://example.com/director1.html', system: 'TestSystem' },
323
+ image: [
324
+ 'https://example.com/image1.jpg',
325
+ {
326
+ value: 'https://example.com/image2.jpg',
327
+ type: 'person',
328
+ size: '2',
329
+ system: 'TestSystem',
330
+ orient: 'P'
331
+ }
332
+ ]
333
+ },
334
+ 'Director 2'
335
+ ],
336
+ actors: ['Actor 1', 'Actor 2'],
337
+ writer: 'Writer 1',
338
+ producers: 'Roger Dobkowitz',
339
+ presenters: 'Drew Carey',
340
+ images: [
341
+ {
342
+ type: 'poster',
343
+ size: '1',
344
+ orient: 'P',
345
+ system: 'tvdb',
346
+ value: 'https://tvdb.com/programme_one_poster_1.jpg'
347
+ },
348
+ {
349
+ type: 'poster',
350
+ size: '2',
351
+ orient: 'P',
352
+ system: 'tmdb',
353
+ value: 'https://tmdb.com/programme_one_poster_2.jpg'
354
+ },
355
+ {
356
+ type: 'backdrop',
357
+ size: '3',
358
+ orient: 'L',
359
+ system: 'tvdb',
360
+ value: 'https://tvdb.com/programme_one_backdrop_3.jpg'
361
+ }
362
+ ],
363
+ icon: 'https://example.com/images/Program1.png?x=шеллы&sid=777'
364
+ }
365
+ ```
366
+
215
367
  ## Channels List
216
368
 
217
369
  ```xml
@@ -222,7 +374,7 @@ From each function in `config.js` you can access a `context` object containing t
222
374
  </channels>
223
375
  ```
224
376
 
225
- You can also specify the language, site and logo for each channel individually, like so:
377
+ You can also specify the language, site, url, logo and LCN (Logical Channel Number) for each channel individually, like so:
226
378
 
227
379
  ```xml
228
380
  <channel
@@ -231,6 +383,8 @@ You can also specify the language, site and logo for each channel individually,
231
383
  xmltv_id="France24.fr"
232
384
  lang="fr"
233
385
  logo="https://example.com/france24.png"
386
+ url="https://example.com/"
387
+ lcn="36"
234
388
  >France 24</channel>
235
389
  ```
236
390
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "epg-grabber",
3
- "version": "0.36.0",
3
+ "version": "0.37.0",
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 CHANGED
@@ -1,13 +1,18 @@
1
+ const { toArray } = require('./utils')
2
+
1
3
  class Channel {
2
4
  constructor(c) {
3
5
  const data = {
4
6
  xmltv_id: c.xmltv_id,
5
- name: c.name,
7
+ displayNames: toArray(c.displayNames || c.displayName || c.names || c.name).map(text =>
8
+ toTextObject(text, c.lang)
9
+ ),
6
10
  site: c.site || '',
7
11
  site_id: c.site_id,
8
12
  lang: c.lang || '',
9
- logo: c.logo || '',
10
- url: c.url || toURL(c.site)
13
+ icons: toArray(c.icons || c.icon || c.logo).map(toIconObject),
14
+ urls: toArray(c.urls || c.url || toURL(c.site)).map(toUrlObject),
15
+ lcn: toArray(c.lcn).map(toTextObject)
11
16
  }
12
17
 
13
18
  for (let key in data) {
@@ -21,3 +26,34 @@ module.exports = Channel
21
26
  function toURL(site) {
22
27
  return site ? `https://${site}` : ''
23
28
  }
29
+
30
+ function toTextObject(text, lang) {
31
+ if (typeof text === 'string') {
32
+ return { value: text, lang }
33
+ }
34
+
35
+ return {
36
+ value: text.value,
37
+ lang: text.lang
38
+ }
39
+ }
40
+
41
+ function toIconObject(icon) {
42
+ if (!icon) return { src: '' }
43
+ if (typeof icon === 'string') return { src: icon }
44
+
45
+ return {
46
+ src: icon.src,
47
+ width: icon.width,
48
+ height: icon.height
49
+ }
50
+ }
51
+
52
+ function toUrlObject(url) {
53
+ if (typeof url === 'string') return { system: '', value: url }
54
+
55
+ return {
56
+ system: url.system || '',
57
+ value: url.value || ''
58
+ }
59
+ }
package/src/Program.js CHANGED
@@ -9,21 +9,43 @@ class Program {
9
9
  }
10
10
 
11
11
  const data = {
12
- site: c.site || '',
12
+ start: p.start ? toUnix(p.start) : null,
13
+ stop: p.stop ? toUnix(p.stop) : null,
13
14
  channel: c.xmltv_id || '',
14
15
  titles: toArray(p.titles || p.title).map(text => toTextObject(text, c.lang)),
15
- sub_titles: toArray(p.sub_titles || p.sub_title).map(text => toTextObject(text, c.lang)),
16
+ subTitles: toArray(p.subTitles || p.subTitle || p.sub_titles || p.sub_title).map(text =>
17
+ toTextObject(text, c.lang)
18
+ ),
16
19
  descriptions: toArray(p.descriptions || p.description || p.desc).map(text =>
17
20
  toTextObject(text, c.lang)
18
21
  ),
19
- icon: toIconObject(p.icon),
20
- episodeNumbers: p.episodeNum || p.episodeNumbers || getEpisodeNumbers(p.season, p.episode),
21
22
  date: p.date ? toUnix(p.date) : null,
22
- start: p.start ? toUnix(p.start) : null,
23
- stop: p.stop ? toUnix(p.stop) : null,
23
+ categories: toArray(p.categories || p.category).map(text => toTextObject(text, c.lang)),
24
+ keywords: toArray(p.keywords || p.keyword).map(text => toTextObject(text, c.lang)),
25
+ languages: toArray(p.languages || p.language).map(text => toTextObject(text, c.lang)),
26
+ origLanguages: toArray(p.origLanguages || p.origLanguage).map(text =>
27
+ toTextObject(text, c.lang)
28
+ ),
29
+ length: toArray(p.length).map(toLengthObject),
24
30
  urls: toArray(p.urls || p.url).map(toUrlObject),
31
+ countries: toArray(p.countries || p.country).map(text => toTextObject(text, c.lang)),
32
+ site: c.site || '',
33
+ episodeNumbers: toArray(
34
+ p.episodeNum ||
35
+ p.episodeNumber ||
36
+ p.episodeNumbers ||
37
+ makeEpisodeNumberObjects(p.season, p.episode)
38
+ ).map(toEpisodeNumberObject),
39
+ video: toVideoObject(p.video),
40
+ audio: toAudioObject(p.audio),
41
+ previouslyShown: toArray(p.previouslyShown).map(toPreviouslyShownObject),
42
+ premiere: toArray(p.premiere).map(toTextObject),
43
+ lastChance: toArray(p.lastChance).map(toTextObject),
44
+ new: p.new,
45
+ subtitles: toArray(p.subtitles).map(toSubtitlesObject),
25
46
  ratings: toArray(p.ratings || p.rating).map(toRatingObject),
26
- categories: toArray(p.categories || p.category).map(text => toTextObject(text, c.lang)),
47
+ starRatings: toArray(p.starRatings || p.starRating).map(toRatingObject),
48
+ reviews: toArray(p.reviews || p.review).map(toReviewObject),
27
49
  directors: toArray(p.directors || p.director).map(toPersonObject),
28
50
  actors: toArray(p.actors || p.actor).map(toPersonObject),
29
51
  writers: toArray(p.writers || p.writer).map(toPersonObject),
@@ -33,7 +55,9 @@ class Program {
33
55
  editors: toArray(p.editors || p.editor).map(toPersonObject),
34
56
  presenters: toArray(p.presenters || p.presenter).map(toPersonObject),
35
57
  commentators: toArray(p.commentators || p.commentator).map(toPersonObject),
36
- guests: toArray(p.guests || p.guest).map(toPersonObject)
58
+ guests: toArray(p.guests || p.guest).map(toPersonObject),
59
+ images: toArray(p.images || p.image).map(toImageObject),
60
+ icons: toArray(p.icons || p.icon).map(toIconObject)
37
61
  }
38
62
 
39
63
  for (let key in data) {
@@ -71,6 +95,10 @@ function toPersonObject(person) {
71
95
  }
72
96
  }
73
97
 
98
+ function toSubtitlesObject(subtitles) {
99
+ return { type: subtitles.type || '', language: toArray(subtitles.language).map(toTextObject) }
100
+ }
101
+
74
102
  function toImageObject(image) {
75
103
  if (typeof image === 'string') return { type: '', size: '', orient: '', system: '', value: image }
76
104
 
@@ -84,15 +112,24 @@ function toImageObject(image) {
84
112
  }
85
113
 
86
114
  function toRatingObject(rating) {
87
- if (typeof rating === 'string') return { system: '', icon: '', value: rating }
115
+ if (typeof rating === 'string') return { system: '', icon: [], value: rating }
88
116
 
89
117
  return {
90
118
  system: rating.system || '',
91
- icon: rating.icon || '',
119
+ icon: toArray(rating.icon).map(toIconObject),
92
120
  value: rating.value || ''
93
121
  }
94
122
  }
95
123
 
124
+ function toLengthObject(length) {
125
+ if (typeof length === 'string') return { units: '', value: length }
126
+
127
+ return {
128
+ units: length.units || '',
129
+ value: length.value || ''
130
+ }
131
+ }
132
+
96
133
  function toUrlObject(url) {
97
134
  if (typeof url === 'string') return { system: '', value: url }
98
135
 
@@ -102,22 +139,74 @@ function toUrlObject(url) {
102
139
  }
103
140
  }
104
141
 
142
+ function toVideoObject(video) {
143
+ if (!video) return {}
144
+
145
+ return {
146
+ present: video.present || '',
147
+ colour: video.colour || '',
148
+ aspect: video.aspect || '',
149
+ quality: video.quality || ''
150
+ }
151
+ }
152
+
153
+ function toAudioObject(audio) {
154
+ if (!audio) return {}
155
+
156
+ return {
157
+ present: audio.present || '',
158
+ stereo: audio.stereo || ''
159
+ }
160
+ }
161
+
162
+ function toReviewObject(review) {
163
+ if (!review) return {}
164
+
165
+ return {
166
+ type: review.type || '',
167
+ source: review.source || '',
168
+ reviewer: review.reviewer || '',
169
+ lang: review.lang || '',
170
+ value: review.value || ''
171
+ }
172
+ }
173
+
174
+ function toPreviouslyShownObject(previouslyShown) {
175
+ if (!previouslyShown) return {}
176
+
177
+ return {
178
+ start: previouslyShown.start || '',
179
+ channel: previouslyShown.channel || ''
180
+ }
181
+ }
182
+
105
183
  function toIconObject(icon) {
106
184
  if (!icon) return { src: '' }
107
185
  if (typeof icon === 'string') return { src: icon }
108
186
 
109
187
  return {
110
- src: icon.src || ''
188
+ src: icon.src,
189
+ width: icon.width,
190
+ height: icon.height
111
191
  }
112
192
  }
113
193
 
114
- function getEpisodeNumbers(s, e) {
194
+ function makeEpisodeNumberObjects(s, e) {
115
195
  s = parseNumber(s)
116
196
  e = parseNumber(e)
117
197
 
118
198
  return [createXMLTVNS(s, e), createOnScreen(s, e)].filter(Boolean)
119
199
  }
120
200
 
201
+ function toEpisodeNumberObject(episode) {
202
+ if (!episode) return {}
203
+
204
+ return {
205
+ system: episode.system || '',
206
+ value: episode.value || ''
207
+ }
208
+ }
209
+
121
210
  function createXMLTVNS(s, e) {
122
211
  if (!e) return null
123
212
  s = s || 1
package/src/index.js CHANGED
@@ -64,12 +64,18 @@ class EPGGrabberMock {
64
64
  this.config = config
65
65
  }
66
66
 
67
- async grab(channel, date, cb) {
67
+ async grab(channel, date, config = {}, cb = () => {}) {
68
68
  let _date = getUTCDate(date)
69
- let _programs = await this.config.parser({ channel, date: _date })
69
+ if (typeof config == 'function') {
70
+ cb = config
71
+ config = {}
72
+ }
73
+ config = merge(this.config, config)
74
+ config = parseConfig(config)
75
+ let _programs = await config.parser({ channel, date: _date })
70
76
  let programs = _programs.map(data => new Program(data, channel))
71
77
 
72
- if (this.config.request?.timeout !== undefined && this.config.request.timeout < 1) {
78
+ if (config.request?.timeout !== undefined && config.request.timeout < 1) {
73
79
  cb({ programs: [], date: _date, channel }, new Error('Connection timeout'))
74
80
  return []
75
81
  }
package/src/xmltv.js CHANGED
@@ -30,9 +30,12 @@ function createElements(channels, programs, date) {
30
30
  return (
31
31
  '\r\n' +
32
32
  el('channel', { id: channel.xmltv_id }, [
33
- el('display-name', {}, [escapeString(channel.name)]),
34
- el('icon', { src: channel.logo }),
35
- el('url', {}, [channel.url])
33
+ ...channel.displayNames.map(name =>
34
+ el('display-name', { lang: name.lang }, [escapeString(name.value)])
35
+ ),
36
+ ...channel.icons.map(icon => el('icon', icon)),
37
+ ...channel.urls.map(createURLElement),
38
+ ...channel.lcn.map(lcn => el('lcn', {}, [escapeString(lcn.value)]))
36
39
  ])
37
40
  )
38
41
  }),
@@ -50,8 +53,8 @@ function createElements(channels, programs, date) {
50
53
  ...program.titles.map(title =>
51
54
  el('title', { lang: title.lang }, [escapeString(title.value)])
52
55
  ),
53
- ...program.sub_titles.map(sub_title =>
54
- el('sub-title', { lang: sub_title.lang }, [escapeString(sub_title.value)])
56
+ ...program.subTitles.map(subTitle =>
57
+ el('sub-title', { lang: subTitle.lang }, [escapeString(subTitle.value)])
55
58
  ),
56
59
  ...program.descriptions.map(desc =>
57
60
  el('desc', { lang: desc.lang }, [escapeString(desc.value)])
@@ -72,17 +75,83 @@ function createElements(channels, programs, date) {
72
75
  ...program.categories.map(category =>
73
76
  el('category', { lang: category.lang }, [escapeString(category.value)])
74
77
  ),
75
- el('icon', { src: program.icon.src }),
76
- ...program.urls.map(createURL),
78
+ ...program.keywords.map(keyword =>
79
+ el('keyword', { lang: keyword.lang }, [escapeString(keyword.value)])
80
+ ),
81
+ ...program.languages.map(language =>
82
+ el('language', { lang: language.lang }, [escapeString(language.value)])
83
+ ),
84
+ ...program.origLanguages.map(origLanguage =>
85
+ el('orig-language', { lang: origLanguage.lang }, [escapeString(origLanguage.value)])
86
+ ),
87
+ ...program.length.map(length =>
88
+ el('length', { units: length.units }, [escapeString(length.value)])
89
+ ),
90
+ ...program.countries.map(country =>
91
+ el('country', { lang: country.lang }, [escapeString(country.value)])
92
+ ),
93
+ ...program.urls.map(createURLElement),
77
94
  ...program.episodeNumbers.map(episode =>
78
95
  el('episode-num', { system: episode.system }, [episode.value])
79
96
  ),
97
+ el('video', {}, [
98
+ el('present', {}, [program.video.present]),
99
+ el('colour', {}, [program.video.colour]),
100
+ el('aspect', {}, [program.video.aspect]),
101
+ el('quality', {}, [program.video.quality])
102
+ ]),
103
+ el('audio', {}, [
104
+ el('present', {}, [program.audio.present]),
105
+ el('stereo', {}, [program.audio.stereo])
106
+ ]),
107
+ ...program.previouslyShown.map(previouslyShown =>
108
+ el('previously-shown', {
109
+ start: previouslyShown.start,
110
+ channel: previouslyShown.channel
111
+ })
112
+ ),
113
+ ...program.premiere.map(premiere =>
114
+ el('premiere', { lang: premiere.lang }, [escapeString(premiere.value)])
115
+ ),
116
+ ...program.lastChance.map(lastChance =>
117
+ el('last-chance', { lang: lastChance.lang }, [escapeString(lastChance.value)])
118
+ ),
119
+ program.new ? el('new') : '',
120
+ ...program.subtitles.map(subtitles =>
121
+ el(
122
+ 'subtitles',
123
+ { type: subtitles.type },
124
+ subtitles.language.map(language =>
125
+ el('language', { lang: language.lang }, [escapeString(language.value)])
126
+ )
127
+ )
128
+ ),
80
129
  ...program.ratings.map(rating =>
81
130
  el('rating', { system: rating.system }, [
82
131
  el('value', {}, [escapeString(rating.value)]),
83
- el('icon', { src: rating.icon })
132
+ ...rating.icon.map(icon => el('icon', icon))
133
+ ])
134
+ ),
135
+ ...program.starRatings.map(rating =>
136
+ el('star-rating', { system: rating.system }, [
137
+ el('value', {}, [escapeString(rating.value)]),
138
+ ...rating.icon.map(icon => el('icon', icon))
84
139
  ])
85
- )
140
+ ),
141
+ ...program.reviews.map(review =>
142
+ el(
143
+ 'review',
144
+ {
145
+ type: review.type,
146
+ source: review.source,
147
+ reviewer: review.reviewer,
148
+ lang: review.lang
149
+ },
150
+ [escapeString(review.value)]
151
+ )
152
+ ),
153
+ ...program.images.map(createImageElement),
154
+ ...program.icons.map(icon => el('icon', icon))
86
155
  ]
87
156
  )
88
157
  )
@@ -94,12 +163,12 @@ function createElements(channels, programs, date) {
94
163
  function createCastMember(position, data) {
95
164
  return el(position, {}, [
96
165
  escapeString(data.value),
97
- ...data.url.map(createURL),
98
- ...data.image.map(createImage)
166
+ ...data.url.map(createURLElement),
167
+ ...data.image.map(createImageElement)
99
168
  ])
100
169
  }
101
170
 
102
- function createImage(image) {
171
+ function createImageElement(image) {
103
172
  return el(
104
173
  'image',
105
174
  {
@@ -112,16 +181,17 @@ function createImage(image) {
112
181
  )
113
182
  }
114
183
 
115
- function createURL(url) {
184
+ function createURLElement(url) {
116
185
  return el('url', { system: url.system }, [url.value])
117
186
  }
118
187
 
119
- function createElement(name, attrs = {}, children = []) {
188
+ function createElement(name, attrs, children) {
120
189
  return toString({ name, attrs, children })
121
190
  }
122
191
 
123
192
  function toString(elem) {
124
193
  if (typeof elem === 'string' || typeof elem === 'number') return elem
194
+ if (!elem.attrs && !elem.children) return `<${elem.name}/>`
125
195
 
126
196
  let attrs = ''
127
197
  for (let key in elem.attrs) {
@@ -131,17 +201,18 @@ function toString(elem) {
131
201
  }
132
202
  }
133
203
 
134
- if (elem.children.filter(Boolean).length) {
135
- let children = ''
204
+ let children = elem.children || []
205
+ if (children.filter(Boolean).length) {
206
+ let _children = ''
136
207
  elem.children.forEach(childElem => {
137
- children += toString(childElem)
208
+ _children += toString(childElem)
138
209
  })
139
210
 
140
- return `<${elem.name}${attrs}>${children}</${elem.name}>`
211
+ return `<${elem.name}${attrs}>${_children}</${elem.name}>`
141
212
  }
142
213
 
143
214
  if (!attrs) return ''
144
- if (!['icon'].includes(elem.name)) return ''
215
+ if (!['icon', 'previously-shown'].includes(elem.name)) return ''
145
216
 
146
217
  return `<${elem.name}${attrs}/>`
147
218
  }
@@ -11,12 +11,12 @@ it('can create new Channel', () => {
11
11
  })
12
12
 
13
13
  expect(channel).toMatchObject({
14
- name: '1 TV',
14
+ displayNames: [{ lang: 'fr', value: '1 TV' }],
15
15
  xmltv_id: '1TV.com',
16
16
  site_id: '1',
17
17
  site: 'example.com',
18
- url: 'https://example.com',
18
+ urls: [{ system: '', value: 'https://example.com' }],
19
19
  lang: 'fr',
20
- logo: 'https://example.com/logos/1TV.png'
20
+ icons: [{ src: 'https://example.com/logos/1TV.png' }]
21
21
  })
22
22
  })
@@ -53,14 +53,14 @@ it('can create new Program', () => {
53
53
  site: 'example.com',
54
54
  channel: '1tv',
55
55
  titles: [{ value: 'Title', lang: 'fr' }],
56
- sub_titles: [{ value: 'Subtitle', lang: 'fr' }],
56
+ subTitles: [{ value: 'Subtitle', lang: 'fr' }],
57
57
  descriptions: [{ value: 'Description', lang: 'fr' }],
58
58
  urls: [{ system: '', value: 'http://example.com/title.html' }],
59
59
  categories: [
60
60
  { value: 'Category1', lang: 'fr' },
61
61
  { value: 'Category2', lang: 'fr' }
62
62
  ],
63
- icon: { src: 'https://example.com/image.jpg' },
63
+ icons: [{ src: 'https://example.com/image.jpg' }],
64
64
  episodeNumbers: [
65
65
  { system: 'xmltv_ns', value: '8.237.0/1' },
66
66
  { system: 'onscreen', value: 'S09E238' }
@@ -72,7 +72,7 @@ it('can create new Program', () => {
72
72
  {
73
73
  system: 'MPAA',
74
74
  value: 'PG',
75
- icon: 'http://example.com/pg_symbol.png'
75
+ icon: [{ src: 'http://example.com/pg_symbol.png' }]
76
76
  }
77
77
  ],
78
78
  directors: [{ value: 'Director1', url: [], image: [] }],
@@ -154,11 +154,11 @@ it('can create program from exist object', () => {
154
154
  expect(program).toMatchObject({
155
155
  channel: '1tv',
156
156
  titles: [{ value: 'Program 1', lang: 'de' }],
157
- sub_titles: [],
157
+ subTitles: [],
158
158
  descriptions: [],
159
159
  urls: [],
160
160
  categories: [],
161
- icon: {},
161
+ icons: [],
162
162
  episodeNumbers: [],
163
163
  date: null,
164
164
  start: 1616133600000,
@@ -167,7 +167,7 @@ it('can create program from exist object', () => {
167
167
  {
168
168
  system: 'MPAA',
169
169
  value: 'PG',
170
- icon: 'http://example.com/pg_symbol.png'
170
+ icon: [{ src: 'http://example.com/pg_symbol.png' }]
171
171
  }
172
172
  ],
173
173
  directors: [],
@@ -114,7 +114,7 @@ it('can mock epg grabber', done => {
114
114
  site: 'example.com',
115
115
  url: 'http://example.com/20210319/1tv.json',
116
116
  parser: ({ channel, date }) => [
117
- { title: `Test (${channel.name})`, start: '2021-03-19T04:30:00.000Z' }
117
+ { title: `Test (${channel.displayNames[0].value})`, start: '2021-03-19T04:30:00.000Z' }
118
118
  ]
119
119
  }
120
120
  const channel = new Channel({
@@ -138,9 +138,9 @@ it('can mock epg grabber', done => {
138
138
  site: 'example.com',
139
139
  channel: '1TV.fr',
140
140
  titles: [{ value: 'Test (1TV)', lang: 'fr' }],
141
- sub_titles: [],
141
+ subTitles: [],
142
142
  descriptions: [],
143
- icon: { src: '' },
143
+ icons: [],
144
144
  episodeNumbers: [],
145
145
  date: null,
146
146
  start: 1616128200000,
@@ -152,6 +152,21 @@ it('can mock epg grabber', done => {
152
152
  actors: [],
153
153
  writers: [],
154
154
  adapters: [],
155
+ audio: {},
156
+ video: {},
157
+ images: [],
158
+ keywords: [],
159
+ languages: [],
160
+ lastChance: [],
161
+ length: [],
162
+ new: undefined,
163
+ origLanguages: [],
164
+ premiere: [],
165
+ previouslyShown: [],
166
+ reviews: [],
167
+ starRatings: [],
168
+ subtitles: [],
169
+ countries: [],
155
170
  producers: [],
156
171
  composers: [],
157
172
  editors: [],
@@ -17,16 +17,16 @@ it('can parse channels.xml', () => {
17
17
  site_id: '1',
18
18
  xmltv_id: '1TV.com',
19
19
  lang: 'fr',
20
- logo: 'https://example.com/logos/1TV.png',
21
- name: '1 TV'
20
+ icons: [{ src: 'https://example.com/logos/1TV.png' }],
21
+ displayNames: [{ value: '1 TV', lang: 'fr' }]
22
22
  })
23
23
  expect(channels[1]).toMatchObject({
24
24
  site: 'example.com',
25
25
  site_id: '2',
26
26
  lang: '',
27
- logo: '',
27
+ icons: [],
28
28
  xmltv_id: '2TV.com',
29
- name: '2 TV'
29
+ displayNames: [{ value: '2 TV' }]
30
30
  })
31
31
  })
32
32
 
@@ -44,16 +44,16 @@ it('can parse channels.xml with inline site attribute', () => {
44
44
  site_id: '1',
45
45
  xmltv_id: '1TV.com',
46
46
  lang: 'fr',
47
- logo: 'https://example.com/logos/1TV.png',
48
- name: '1 TV'
47
+ icons: [{ src: 'https://example.com/logos/1TV.png' }],
48
+ displayNames: [{ value: '1 TV', lang: 'fr' }]
49
49
  })
50
50
  expect(channels[1]).toMatchObject({
51
51
  site: 'example.com',
52
52
  site_id: '2',
53
53
  lang: '',
54
- logo: '',
54
+ icons: [],
55
55
  xmltv_id: '2TV.com',
56
- name: '2 TV'
56
+ displayNames: [{ value: '2 TV' }]
57
57
  })
58
58
  })
59
59
 
@@ -69,16 +69,16 @@ it('can parse legacy channels.xml', () => {
69
69
  site_id: '1',
70
70
  xmltv_id: '1TV.com',
71
71
  lang: 'fr',
72
- logo: 'https://example.com/logos/1TV.png',
73
- name: '1 TV'
72
+ icons: [{ src: 'https://example.com/logos/1TV.png' }],
73
+ displayNames: [{ value: '1 TV', lang: 'fr' }]
74
74
  })
75
75
  expect(channels[1]).toMatchObject({
76
76
  site: 'example.com',
77
77
  site_id: '2',
78
78
  lang: '',
79
- logo: '',
79
+ icons: [],
80
80
  xmltv_id: '2TV.com',
81
- name: '2 TV'
81
+ displayNames: [{ value: '2 TV' }]
82
82
  })
83
83
  })
84
84
 
@@ -8,14 +8,20 @@ const channels = [
8
8
  new Channel({
9
9
  xmltv_id: '1TV.co',
10
10
  name: '1 TV',
11
- logo: 'https://example.com/logos/1TV.png',
12
- site: 'example.com'
11
+ site: 'example.com',
12
+ icon: [{ src: 'https://example.com/channel_one_icon.jpg', width: '100', height: '100' }],
13
+ url: [
14
+ { system: 'example', value: 'https://example.com/channel_one' },
15
+ { system: 'other_system', value: 'https://example.com/channel_one_alternate' }
16
+ ],
17
+ lcn: [{ value: '36' }]
13
18
  }),
14
19
  new Channel({
15
20
  xmltv_id: '2TV.co',
16
21
  name: '2 TV',
17
22
  site: 'example.com',
18
- lang: 'es'
23
+ lang: 'es',
24
+ logo: 'https://example.com/logos/2TV.png'
19
25
  })
20
26
  ]
21
27
 
@@ -23,22 +29,81 @@ it('can generate xmltv', () => {
23
29
  const programs = [
24
30
  new Program(
25
31
  {
26
- title: 'Program 1',
27
- sub_title: 'Sub-title & 1',
28
- description: 'Description for Program 1',
29
- url: 'http://example.com/title.html',
30
32
  start: '2021-03-19T06:00:00.000Z',
31
33
  stop: '2021-03-19T06:30:00.000Z',
32
- category: 'Test',
34
+ title: 'Program 1',
35
+ subTitle: 'Sub-title & 1',
36
+ description: 'Description for Program 1',
33
37
  date: '2022-05-06',
38
+ category: 'Test',
39
+ keyword: [
40
+ { lang: 'en', value: 'physical-comedy' },
41
+ { lang: 'en', value: 'romantic' }
42
+ ],
43
+ language: [{ value: 'English' }],
44
+ origLanguage: [{ lang: 'en', value: 'French' }],
45
+ length: [{ units: 'minutes', value: '60' }],
46
+ url: 'http://example.com/title.html',
47
+ country: [{ value: 'US' }],
48
+ video: {
49
+ present: 'yes',
50
+ colour: 'no',
51
+ aspect: '16:9',
52
+ quality: 'HDTV'
53
+ },
54
+ audio: {
55
+ present: 'yes',
56
+ stereo: 'Dolby Digital'
57
+ },
34
58
  season: 9,
35
59
  episode: 239,
36
- icon: 'https://example.com/images/Program1.png?x=шеллы&sid=777',
60
+ previouslyShown: [{ start: '20080711000000', channel: 'channel-two.tv' }],
61
+ premiere: [{ value: 'First time on British TV' }],
62
+ lastChance: [{ lang: 'en', value: 'Last time on this channel' }],
63
+ new: true,
64
+ subtitles: [
65
+ { type: 'teletext', language: [{ value: 'English' }] },
66
+ { type: 'onscreen', language: [{ lang: 'en', value: 'Spanish' }] }
67
+ ],
37
68
  rating: {
38
69
  system: 'MPAA',
39
70
  value: 'P&G',
40
71
  icon: 'http://example.com/pg_symbol.png'
41
72
  },
73
+ starRating: [
74
+ {
75
+ system: 'TV Guide',
76
+ value: '4/5',
77
+ icon: [{ src: 'stars.png' }]
78
+ },
79
+ {
80
+ system: 'IMDB',
81
+ value: '8/10'
82
+ }
83
+ ],
84
+ review: [
85
+ {
86
+ type: 'text',
87
+ source: 'Rotten Tomatoes',
88
+ reviewer: 'Joe Bloggs',
89
+ lang: 'en',
90
+ value: 'This is a fantastic show!'
91
+ },
92
+ {
93
+ type: 'text',
94
+ source: 'IDMB',
95
+ reviewer: 'Jane Doe',
96
+ lang: 'en',
97
+ value: 'I love this show!'
98
+ },
99
+ {
100
+ type: 'url',
101
+ source: 'Rotten Tomatoes',
102
+ reviewer: 'Joe Bloggs',
103
+ lang: 'en',
104
+ value: 'https://example.com/programme_one_review'
105
+ }
106
+ ],
42
107
  director: [
43
108
  {
44
109
  value: 'Director 1',
@@ -57,7 +122,38 @@ it('can generate xmltv', () => {
57
122
  'Director 2'
58
123
  ],
59
124
  actor: ['Actor 1', 'Actor 2'],
60
- writer: 'Writer 1'
125
+ writer: 'Writer 1',
126
+ image: [
127
+ {
128
+ type: 'poster',
129
+ size: '1',
130
+ orient: 'P',
131
+ system: 'tvdb',
132
+ value: 'https://tvdb.com/programme_one_poster_1.jpg'
133
+ },
134
+ {
135
+ type: 'poster',
136
+ size: '2',
137
+ orient: 'P',
138
+ system: 'tmdb',
139
+ value: 'https://tmdb.com/programme_one_poster_2.jpg'
140
+ },
141
+ {
142
+ type: 'backdrop',
143
+ size: '3',
144
+ orient: 'L',
145
+ system: 'tvdb',
146
+ value: 'https://tvdb.com/programme_one_backdrop_3.jpg'
147
+ },
148
+ {
149
+ type: 'backdrop',
150
+ size: '3',
151
+ orient: 'L',
152
+ system: 'tmdb',
153
+ value: 'https://tmdb.com/programme_one_backdrop_3.jpg'
154
+ }
155
+ ],
156
+ icon: 'https://example.com/images/Program1.png?x=шеллы&sid=777'
61
157
  },
62
158
  channels[0]
63
159
  ),
@@ -74,6 +170,6 @@ it('can generate xmltv', () => {
74
170
  const output = xmltv.generate({ channels, programs })
75
171
 
76
172
  expect(output).toBe(
77
- '<?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>P&amp;G</value><icon src="http://example.com/pg_symbol.png"/></rating></programme>\r\n<programme start="20210319060000 +0000" stop="20210319063000 +0000" channel="2TV.co"><title lang="es">Program 2</title></programme>\r\n</tv>'
173
+ '<?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/channel_one_icon.jpg" width="100" height="100"/><url system="example">https://example.com/channel_one</url><url system="other_system">https://example.com/channel_one_alternate</url><lcn>36</lcn></channel>\r\n<channel id="2TV.co"><display-name lang="es">2 TV</display-name><icon src="https://example.com/logos/2TV.png"/><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><keyword lang="en">physical-comedy</keyword><keyword lang="en">romantic</keyword><language>English</language><orig-language lang="en">French</orig-language><length units="minutes">60</length><country>US</country><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><video><present>yes</present><colour>no</colour><aspect>16:9</aspect><quality>HDTV</quality></video><audio><present>yes</present><stereo>Dolby Digital</stereo></audio><previously-shown start="20080711000000" channel="channel-two.tv"/><premiere>First time on British TV</premiere><last-chance lang="en">Last time on this channel</last-chance><new/><subtitles type="teletext"><language>English</language></subtitles><subtitles type="onscreen"><language lang="en">Spanish</language></subtitles><rating system="MPAA"><value>P&amp;G</value><icon src="http://example.com/pg_symbol.png"/></rating><star-rating system="TV Guide"><value>4/5</value><icon src="stars.png"/></star-rating><star-rating system="IMDB"><value>8/10</value></star-rating><review type="text" source="Rotten Tomatoes" reviewer="Joe Bloggs" lang="en">This is a fantastic show!</review><review type="text" source="IDMB" reviewer="Jane Doe" lang="en">I love this show!</review><review type="url" source="Rotten Tomatoes" reviewer="Joe Bloggs" lang="en">https://example.com/programme_one_review</review><image type="poster" size="1" orient="P" system="tvdb">https://tvdb.com/programme_one_poster_1.jpg</image><image type="poster" size="2" orient="P" system="tmdb">https://tmdb.com/programme_one_poster_2.jpg</image><image type="backdrop" size="3" orient="L" system="tvdb">https://tvdb.com/programme_one_backdrop_3.jpg</image><image type="backdrop" size="3" orient="L" system="tmdb">https://tmdb.com/programme_one_backdrop_3.jpg</image><icon src="https://example.com/images/Program1.png?x=шеллы&amp;sid=777"/></programme>\r\n<programme start="20210319060000 +0000" stop="20210319063000 +0000" channel="2TV.co"><title lang="es">Program 2</title></programme>\r\n</tv>'
78
174
  )
79
175
  })
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8" ?><tv date="20231125">
2
- <channel id="1TV.com"><display-name>1 TV</display-name><icon src="https://example.com/logos/1TV.png"/><url>https://example.com</url></channel>
3
- <channel id="2TV.com"><display-name>2 TV</display-name><icon src="http://example.com/logos/1TV.png?x=шеллы&amp;sid=777"/><url>https://example.com</url></channel>
4
- <channel id="3TV.com"><display-name>3 TV</display-name><icon src="http://example.com/logos/1TV.png?x=шеллы&amp;sid=777"/><url>https://example2.com</url></channel>
5
- <channel id="4TV.com"><display-name>4 TV</display-name><icon src="http://example.com/logos/1TV.png?x=шеллы&amp;sid=777"/><url>https://example2.com</url></channel>
6
- </tv>