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.
- package/bin/epg-grabber.js +13 -9
- package/package.json +1 -1
- package/src/Channel.js +23 -0
- package/src/Program.js +122 -0
- package/src/index.js +1 -2
- package/src/parser.js +51 -0
- package/src/utils.js +19 -2
- package/src/xmltv.js +27 -89
- package/tests/Channel.test.js +43 -0
- package/tests/Program.test.js +203 -0
- package/tests/bin.test.js +3 -3
- package/tests/config.test.js +1 -1
- package/tests/input/async.config.js +8 -2
- package/tests/input/{example.com.channels.xml → example.channels.xml} +0 -0
- package/tests/input/{example.com.config.js → example.config.js} +4 -10
- package/tests/parser.test.js +40 -0
- package/tests/xmltv.test.js +48 -123
- package/src/channels.js +0 -26
- package/src/programs.js +0 -24
- package/tests/channels.test.js +0 -26
- package/tests/programs.test.js +0 -63
package/bin/epg-grabber.js
CHANGED
|
@@ -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
|
|
8
|
+
const { EPGGrabber, parseChannels, generateXMLTV } = require('../src/index')
|
|
9
9
|
const { create: createLogger } = require('../src/logger')
|
|
10
|
-
const {
|
|
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',
|
|
22
|
-
.option('--delay <delay>', 'Delay between requests (in mileseconds)',
|
|
23
|
-
.option('--timeout <timeout>', 'Set a timeout for each request (in mileseconds)',
|
|
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
|
-
|
|
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.
|
|
84
|
-
|
|
85
|
-
|
|
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
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.
|
|
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
|
|
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 {
|
|
2
|
-
const
|
|
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
|
|
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.
|
|
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
|
-
...
|
|
52
|
-
...
|
|
53
|
-
...
|
|
54
|
-
...
|
|
55
|
-
...
|
|
56
|
-
...
|
|
57
|
-
...
|
|
58
|
-
...
|
|
59
|
-
...
|
|
60
|
-
...
|
|
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', {}, [
|
|
63
|
-
...
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
120
|
-
...
|
|
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.
|
|
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.
|
|
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.
|
|
46
|
+
--channels=tests/input/example.channels.xml \
|
|
47
47
|
--output=tests/output/mini.guide.xml.gz \
|
|
48
48
|
--gzip`,
|
|
49
49
|
{
|
package/tests/config.test.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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')
|
|
File without changes
|
|
@@ -5,7 +5,7 @@ dayjs.extend(utc)
|
|
|
5
5
|
|
|
6
6
|
module.exports = {
|
|
7
7
|
site: 'example.com',
|
|
8
|
-
channels: 'example.
|
|
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: '
|
|
25
|
-
|
|
26
|
-
|
|
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
|
+
})
|
package/tests/xmltv.test.js
CHANGED
|
@@ -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
|
-
|
|
21
|
+
fit('can generate xmltv', () => {
|
|
20
22
|
const programs = [
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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>
|
|
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 & 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=шеллы&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
|
-
}
|
package/tests/channels.test.js
DELETED
|
@@ -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
|
-
})
|
package/tests/programs.test.js
DELETED
|
@@ -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
|
-
})
|