epg-grabber 0.28.1 → 0.28.4

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.4",
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,23 @@
1
+ class Channel {
2
+ constructor(c) {
3
+ const data = {
4
+ id: c.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.url || toURL(c.site)
11
+ }
12
+
13
+ for (let key in data) {
14
+ this[key] = data[key]
15
+ }
16
+ }
17
+ }
18
+
19
+ module.exports = Channel
20
+
21
+ function toURL(site) {
22
+ return site ? `https://${site}` : ''
23
+ }
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) {
6
+ const data = {
7
+ channel: p.channel,
8
+ title: p.title,
9
+ sub_title: p.sub_title || '',
10
+ description: [p.description, p.desc, ''].find(i => i !== undefined),
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.urls || 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,51 @@
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
+ const c = el.attributes
22
+ c.name = el.elements.find(el => el.type === 'text').text
23
+ c.site = c.site || rootSite
24
+ if (!c.name) throw new Error(`Channel '${c.xmltv_id}' has no valid name`)
25
+
26
+ return new Channel(c)
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
45
+ .filter(i => i)
46
+ .map(p => {
47
+ p.channel = p.channel || channel.id
48
+
49
+ return new Program(p)
50
+ })
51
+ }
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 })
@@ -80,49 +64,20 @@ function createElements(channels, programs, date) {
80
64
  ]
81
65
  )
82
66
  )
83
- })
67
+ }),
68
+ '\r\n'
84
69
  ])
85
70
  }
86
71
 
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
72
  function createCastMember(position, data) {
116
- data = toObject(data)
117
73
  return el(position, {}, [
118
74
  escapeString(data.value),
119
- ...toArray(data.url).map(createURL),
120
- ...toArray(data.image).map(createImage)
75
+ ...data.url.map(createURL),
76
+ ...data.image.map(createImage)
121
77
  ])
122
78
  }
123
79
 
124
80
  function createImage(image) {
125
- image = toObject(image)
126
81
  return el(
127
82
  'image',
128
83
  {
@@ -136,32 +91,15 @@ function createImage(image) {
136
91
  }
137
92
 
138
93
  function createURL(url) {
139
- url = toObject(url)
140
94
  return el('url', { system: url.system }, [url.value])
141
95
  }
142
96
 
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
97
  function createElement(name, attrs = {}, children = []) {
160
98
  return toString({ name, attrs, children })
161
99
  }
162
100
 
163
101
  function toString(elem) {
164
- if (typeof elem === 'string') return elem
102
+ if (typeof elem === 'string' || typeof elem === 'number') return elem
165
103
 
166
104
  let attrs = ''
167
105
  for (let key in elem.attrs) {
@@ -0,0 +1,43 @@
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
+ })
23
+
24
+ it('can create channel from exist object', () => {
25
+ const channel = new Channel({
26
+ name: '1 TV',
27
+ id: '1TV.com',
28
+ site_id: '1',
29
+ site: 'example.com',
30
+ lang: 'fr',
31
+ logo: 'https://example.com/logos/1TV.png'
32
+ })
33
+
34
+ expect(channel).toMatchObject({
35
+ name: '1 TV',
36
+ id: '1TV.com',
37
+ site_id: '1',
38
+ site: 'example.com',
39
+ url: 'https://example.com',
40
+ lang: 'fr',
41
+ logo: 'https://example.com/logos/1TV.png'
42
+ })
43
+ })
@@ -0,0 +1,203 @@
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
+ channel: channel.id,
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
+
50
+ expect(program).toMatchObject({
51
+ channel: '1tv',
52
+ title: 'Title',
53
+ sub_title: 'Subtitle',
54
+ description: 'Description',
55
+ urls: [{ system: '', value: 'http://example.com/title.html' }],
56
+ categories: ['Category1', 'Category2'],
57
+ icon: { src: 'https://example.com/image.jpg' },
58
+ episodeNumbers: [
59
+ { system: 'xmltv_ns', value: '8.237.0/1' },
60
+ { system: 'onscreen', value: 'S09E238' }
61
+ ],
62
+ date: 1651795200000,
63
+ start: 1616133600000,
64
+ stop: 1616135400000,
65
+ ratings: [
66
+ {
67
+ system: 'MPAA',
68
+ value: 'PG',
69
+ icon: 'http://example.com/pg_symbol.png'
70
+ }
71
+ ],
72
+ directors: [{ value: 'Director1', url: [], image: [] }],
73
+ actors: [
74
+ { value: 'Actor1', url: [], image: [] },
75
+ {
76
+ value: 'Actor2',
77
+ url: [{ system: '', value: 'http://actor2.com' }],
78
+ image: [
79
+ { type: '', size: '', orient: '', system: '', value: 'http://actor2.com/image.png' }
80
+ ]
81
+ }
82
+ ],
83
+ writers: [
84
+ {
85
+ value: 'Writer1',
86
+ url: [{ system: 'imdb', value: 'http://imdb.com/p/writer1' }],
87
+ image: [
88
+ {
89
+ value: 'https://example.com/image.jpg',
90
+ type: 'person',
91
+ size: '2',
92
+ system: 'TestSystem',
93
+ orient: 'P'
94
+ }
95
+ ]
96
+ }
97
+ ],
98
+ adapters: [
99
+ {
100
+ value: 'Adapter1',
101
+ url: [
102
+ { system: '', value: 'http://imdb.com/p/adapter1' },
103
+ { system: '', value: 'http://imdb.com/p/adapter2' }
104
+ ],
105
+ image: [
106
+ {
107
+ value: 'https://example.com/image1.jpg',
108
+ type: '',
109
+ size: '',
110
+ system: '',
111
+ orient: ''
112
+ },
113
+ {
114
+ value: 'https://example.com/image2.jpg',
115
+ type: '',
116
+ size: '',
117
+ system: '',
118
+ orient: ''
119
+ }
120
+ ]
121
+ }
122
+ ],
123
+ producers: [],
124
+ composers: [],
125
+ editors: [],
126
+ presenters: [],
127
+ commentators: [],
128
+ guests: []
129
+ })
130
+ })
131
+
132
+ it('can create program from exist object', () => {
133
+ const program = new Program({
134
+ channel: channel.id,
135
+ title: 'Program 1',
136
+ start: '2021-03-19T06:00:00.000Z',
137
+ stop: '2021-03-19T06:30:00.000Z',
138
+ ratings: {
139
+ system: 'MPAA',
140
+ value: 'PG',
141
+ icon: 'http://example.com/pg_symbol.png'
142
+ },
143
+ actors: [{ value: 'Actor1', url: [], image: [] }]
144
+ })
145
+
146
+ expect(program).toMatchObject({
147
+ channel: '1tv',
148
+ title: 'Program 1',
149
+ sub_title: '',
150
+ description: '',
151
+ urls: [],
152
+ categories: [],
153
+ icon: {},
154
+ episodeNumbers: [],
155
+ date: null,
156
+ start: 1616133600000,
157
+ stop: 1616135400000,
158
+ ratings: [
159
+ {
160
+ system: 'MPAA',
161
+ value: 'PG',
162
+ icon: 'http://example.com/pg_symbol.png'
163
+ }
164
+ ],
165
+ directors: [],
166
+ actors: [{ value: 'Actor1', url: [], image: [] }],
167
+ writers: [],
168
+ adapters: [],
169
+ producers: [],
170
+ composers: [],
171
+ editors: [],
172
+ presenters: [],
173
+ commentators: [],
174
+ guests: []
175
+ })
176
+ })
177
+
178
+ it('can create program without season number', () => {
179
+ const program = new Program({
180
+ channel: channel.id,
181
+ title: 'Program 1',
182
+ start: '2021-03-19T06:00:00.000Z',
183
+ stop: '2021-03-19T06:30:00.000Z',
184
+ episode: 238
185
+ })
186
+
187
+ expect(program.episodeNumbers).toMatchObject([
188
+ { system: 'xmltv_ns', value: '0.237.0/1' },
189
+ { system: 'onscreen', value: 'S01E238' }
190
+ ])
191
+ })
192
+
193
+ it('can create program without episode number', () => {
194
+ const program = new Program({
195
+ channel: channel.id,
196
+ title: 'Program 1',
197
+ start: '2021-03-19T06:00:00.000Z',
198
+ stop: '2021-03-19T06:30:00.000Z',
199
+ season: 3
200
+ })
201
+
202
+ expect(program.episodeNumbers).toMatchObject([])
203
+ })
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>\r\n</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
- })