epg-grabber 0.12.1 → 0.15.1
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 +86 -20
- package/bin/epg-grabber.js +60 -18
- package/package.json +2 -1
- package/src/utils.js +14 -6
- package/tests/bin.test.js +7 -1
- package/tests/input/example.com.channels.xml +1 -1
- package/tests/utils.test.js +12 -17
package/README.md
CHANGED
|
@@ -8,7 +8,64 @@ Node.js CLI tool for grabbing EPG from different websites.
|
|
|
8
8
|
npm install -g epg-grabber
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
epg-grabber --config=example.com.config.js
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
#### example.com.config.js
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
module.exports = {
|
|
21
|
+
site: 'example.com',
|
|
22
|
+
channels: 'example.com.channels.xml',
|
|
23
|
+
url: function (context) {
|
|
24
|
+
const { date, channel } = context
|
|
25
|
+
|
|
26
|
+
return `https://api.example.com/${date.format('YYYY-MM-DD')}/channel/${channel.site_id}`
|
|
27
|
+
},
|
|
28
|
+
parser: function (context) {
|
|
29
|
+
const programs = JSON.parse(context.content)
|
|
30
|
+
|
|
31
|
+
return programs.map(program => {
|
|
32
|
+
return {
|
|
33
|
+
title: program.title,
|
|
34
|
+
start: program.start,
|
|
35
|
+
stop: program.stop
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
#### example.com.channels.xml
|
|
43
|
+
|
|
44
|
+
```xml
|
|
45
|
+
<?xml version="1.0" ?>
|
|
46
|
+
<site site="example.com">
|
|
47
|
+
<channels>
|
|
48
|
+
<channel site_id="cnn-23" xmltv_id="CNN.us">CNN</channel>
|
|
49
|
+
</channels>
|
|
50
|
+
</site>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Example Output
|
|
54
|
+
|
|
55
|
+
```xml
|
|
56
|
+
<tv>
|
|
57
|
+
<channel id="CNN.us">
|
|
58
|
+
<display-name>CNN</display-name>
|
|
59
|
+
<url>https://example.com</url>
|
|
60
|
+
</channel>
|
|
61
|
+
<programme start="20211116040000 +0000" stop="20211116050000 +0000" channel="CNN.us">
|
|
62
|
+
<title lang="en">News at 10PM</title>
|
|
63
|
+
</programme>
|
|
64
|
+
// ...
|
|
65
|
+
</tv>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## CLI
|
|
12
69
|
|
|
13
70
|
```sh
|
|
14
71
|
epg-grabber --config=example.com.config.js
|
|
@@ -22,9 +79,12 @@ Arguments:
|
|
|
22
79
|
- `--lang`: set default language for all programs (default: 'en')
|
|
23
80
|
- `--days`: number of days for which to grab the program (default: 1)
|
|
24
81
|
- `--delay`: delay between requests (default: 3000)
|
|
82
|
+
- `--timeout`: set a timeout for each request (default: 5000)
|
|
25
83
|
- `--debug`: enable debug mode (default: false)
|
|
84
|
+
- `--log`: path to log file (optional)
|
|
85
|
+
- `--log-level`: set the log level (default: 'info')
|
|
26
86
|
|
|
27
|
-
|
|
87
|
+
## Site Config
|
|
28
88
|
|
|
29
89
|
```js
|
|
30
90
|
module.exports = {
|
|
@@ -41,12 +101,11 @@ module.exports = {
|
|
|
41
101
|
timeout: 5000,
|
|
42
102
|
|
|
43
103
|
/**
|
|
44
|
-
* @param {object}
|
|
45
|
-
* @param {object} channel Data about the requested channel
|
|
104
|
+
* @param {object} context
|
|
46
105
|
*
|
|
47
106
|
* @return {string} The function should return headers for each request (optional)
|
|
48
107
|
*/
|
|
49
|
-
headers: function(
|
|
108
|
+
headers: function(context) {
|
|
50
109
|
return {
|
|
51
110
|
'User-Agent':
|
|
52
111
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36 Edg/79.0.309.71'
|
|
@@ -54,12 +113,13 @@ module.exports = {
|
|
|
54
113
|
},
|
|
55
114
|
|
|
56
115
|
/**
|
|
57
|
-
* @param {object}
|
|
58
|
-
* @param {object} channel Data about the requested channel
|
|
116
|
+
* @param {object} context
|
|
59
117
|
*
|
|
60
118
|
* @return {string} The function should return data for each request (optional)
|
|
61
119
|
*/
|
|
62
|
-
data: function(
|
|
120
|
+
data: function(context) {
|
|
121
|
+
const { channel, date } = context
|
|
122
|
+
|
|
63
123
|
return {
|
|
64
124
|
channels: [channel.site_id],
|
|
65
125
|
dateStart: date.format('YYYY-MM-DDT00:00:00-00:00'),
|
|
@@ -69,32 +129,29 @@ module.exports = {
|
|
|
69
129
|
},
|
|
70
130
|
|
|
71
131
|
/**
|
|
72
|
-
* @param {object}
|
|
73
|
-
* @param {object} channel Data about the requested channel
|
|
132
|
+
* @param {object} context
|
|
74
133
|
*
|
|
75
134
|
* @return {string} The function should return URL of the program page for the channel
|
|
76
135
|
*/
|
|
77
|
-
url: function (
|
|
78
|
-
return `https://example.com/${date.format('YYYY-MM-DD')}/channel/${channel.site_id}.html`
|
|
136
|
+
url: function (context) {
|
|
137
|
+
return `https://example.com/${context.date.format('YYYY-MM-DD')}/channel/${context.channel.site_id}.html`
|
|
79
138
|
},
|
|
80
139
|
|
|
81
140
|
/**
|
|
82
|
-
* @param {object}
|
|
83
|
-
* @param {string} content The response received after the request at the above url
|
|
141
|
+
* @param {object} context
|
|
84
142
|
*
|
|
85
143
|
* @return {string} The function should return URL of the channel logo (optional)
|
|
86
144
|
*/
|
|
87
|
-
logo: function (
|
|
88
|
-
return `https://example.com/logos/${channel.site_id}.png`
|
|
145
|
+
logo: function (context) {
|
|
146
|
+
return `https://example.com/logos/${context.channel.site_id}.png`
|
|
89
147
|
},
|
|
90
148
|
|
|
91
149
|
/**
|
|
92
|
-
* @param {object}
|
|
93
|
-
* @param {string} content The response received after the request at the above url
|
|
150
|
+
* @param {object} context
|
|
94
151
|
*
|
|
95
152
|
* @return {array} The function should return an array of programs with their descriptions
|
|
96
153
|
*/
|
|
97
|
-
parser: function (
|
|
154
|
+
parser: function (context) {
|
|
98
155
|
|
|
99
156
|
// content parsing...
|
|
100
157
|
|
|
@@ -114,7 +171,16 @@ module.exports = {
|
|
|
114
171
|
}
|
|
115
172
|
```
|
|
116
173
|
|
|
117
|
-
|
|
174
|
+
## Context Object
|
|
175
|
+
|
|
176
|
+
From each function in `config.js` you can access a `context` object containing the following data:
|
|
177
|
+
|
|
178
|
+
- `channel`: The object describing the current channel (xmltv_id, site_id, name, lang)
|
|
179
|
+
- `date`: The 'dayjs' instance with the requested date
|
|
180
|
+
- `content`: The response data as a String
|
|
181
|
+
- `buffer`: The response data as an ArrayBuffer
|
|
182
|
+
|
|
183
|
+
## Channels List
|
|
118
184
|
|
|
119
185
|
```xml
|
|
120
186
|
<?xml version="1.0" ?>
|
package/bin/epg-grabber.js
CHANGED
|
@@ -8,6 +8,8 @@ const grabber = require('../src/index')
|
|
|
8
8
|
const utils = require('../src/utils')
|
|
9
9
|
const { name, version, description } = require('../package.json')
|
|
10
10
|
const merge = require('lodash.merge')
|
|
11
|
+
const { createLogger, format, transports } = require('winston')
|
|
12
|
+
const { combine, timestamp, printf } = format
|
|
11
13
|
|
|
12
14
|
program
|
|
13
15
|
.name(name)
|
|
@@ -17,44 +19,84 @@ program
|
|
|
17
19
|
.option('-o, --output <output>', 'Path to output file')
|
|
18
20
|
.option('--channels <channels>', 'Path to channels.xml file')
|
|
19
21
|
.option('--lang <lang>', 'Set default language for all programs')
|
|
20
|
-
.option('--days <days>', 'Number of days for which to grab the program', parseInteger)
|
|
22
|
+
.option('--days <days>', 'Number of days for which to grab the program', parseInteger, 1)
|
|
21
23
|
.option('--delay <delay>', 'Delay between requests (in mileseconds)', parseInteger)
|
|
24
|
+
.option('--timeout <timeout>', 'Set a timeout for each request (in mileseconds)', parseInteger)
|
|
22
25
|
.option('--debug', 'Enable debug mode', false)
|
|
26
|
+
.option('--log <log>', 'Path to log file')
|
|
27
|
+
.option('--log-level <level>', 'Set log level', 'info')
|
|
23
28
|
.parse(process.argv)
|
|
24
29
|
|
|
25
30
|
const options = program.opts()
|
|
26
31
|
|
|
32
|
+
const fileFormat = printf(({ level, message, timestamp }) => {
|
|
33
|
+
return `[${timestamp}] ${level.toUpperCase()}: ${message}`
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const consoleFormat = printf(({ level, message, timestamp }) => {
|
|
37
|
+
if (level === 'error') return ` Error: ${message}`
|
|
38
|
+
|
|
39
|
+
return message
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const t = [new transports.Console({ format: consoleFormat })]
|
|
43
|
+
|
|
44
|
+
if (options.log) {
|
|
45
|
+
t.push(
|
|
46
|
+
new transports.File({
|
|
47
|
+
filename: path.resolve(options.log),
|
|
48
|
+
format: combine(timestamp(), fileFormat),
|
|
49
|
+
options: { flags: 'w' }
|
|
50
|
+
})
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const logger = createLogger({
|
|
55
|
+
level: options.logLevel,
|
|
56
|
+
transports: t
|
|
57
|
+
})
|
|
58
|
+
|
|
27
59
|
async function main() {
|
|
28
|
-
|
|
60
|
+
logger.info('Starting...')
|
|
29
61
|
|
|
30
|
-
|
|
62
|
+
logger.info(`Loading '${options.config}'...`)
|
|
31
63
|
let config = require(path.resolve(options.config))
|
|
32
|
-
config = merge(config,
|
|
64
|
+
config = merge(config, {
|
|
65
|
+
days: options.days,
|
|
66
|
+
debug: options.debug,
|
|
67
|
+
lang: options.lang,
|
|
68
|
+
delay: options.delay,
|
|
69
|
+
request: {
|
|
70
|
+
timeout: options.timeout
|
|
71
|
+
}
|
|
72
|
+
})
|
|
33
73
|
|
|
34
74
|
if (options.channels) config.channels = options.channels
|
|
35
75
|
else if (config.channels)
|
|
36
76
|
config.channels = path.join(path.dirname(options.config), config.channels)
|
|
37
77
|
else throw new Error("The required 'channels' property is missing")
|
|
38
78
|
|
|
39
|
-
if (!config.channels)
|
|
40
|
-
|
|
79
|
+
if (!config.channels) return logger.error('Path to [site].channels.xml is missing')
|
|
80
|
+
logger.info(`Loading '${config.channels}'...`)
|
|
41
81
|
const channelsXML = fs.readFileSync(path.resolve(config.channels), { encoding: 'utf-8' })
|
|
42
|
-
const
|
|
43
|
-
const channels = parsed.channels || []
|
|
82
|
+
const channels = utils.parseChannels(channelsXML)
|
|
44
83
|
|
|
45
84
|
let programs = []
|
|
85
|
+
let i = 1
|
|
86
|
+
let days = options.days || 1
|
|
87
|
+
const total = channels.length * days
|
|
46
88
|
for (let channel of channels) {
|
|
47
89
|
await grabber
|
|
48
90
|
.grab(channel, config, (data, err) => {
|
|
49
|
-
|
|
50
|
-
`
|
|
51
|
-
|
|
52
|
-
} programs)`
|
|
91
|
+
logger.info(
|
|
92
|
+
`[${i}/${total}] ${config.site} - ${data.channel.xmltv_id} - ${data.date.format(
|
|
93
|
+
'MMM D, YYYY'
|
|
94
|
+
)} (${data.programs.length} programs)`
|
|
53
95
|
)
|
|
54
96
|
|
|
55
|
-
if (err)
|
|
56
|
-
|
|
57
|
-
|
|
97
|
+
if (err) logger.error(err.message)
|
|
98
|
+
|
|
99
|
+
if (i < total) i++
|
|
58
100
|
})
|
|
59
101
|
.then(results => {
|
|
60
102
|
programs = programs.concat(results)
|
|
@@ -62,11 +104,11 @@ async function main() {
|
|
|
62
104
|
}
|
|
63
105
|
|
|
64
106
|
const xml = utils.convertToXMLTV({ config, channels, programs })
|
|
65
|
-
const outputPath = config.output || 'guide.xml'
|
|
107
|
+
const outputPath = options.output || config.output || 'guide.xml'
|
|
66
108
|
utils.writeToFile(outputPath, xml)
|
|
67
109
|
|
|
68
|
-
|
|
69
|
-
|
|
110
|
+
logger.info(`File '${outputPath}' successfully saved`)
|
|
111
|
+
logger.info('Finish')
|
|
70
112
|
}
|
|
71
113
|
|
|
72
114
|
main()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "epg-grabber",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"description": "Node.js CLI tool for grabbing EPG from different sites",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"preferGlobal": true,
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"glob": "^7.1.6",
|
|
37
37
|
"lodash.merge": "^4.6.2",
|
|
38
38
|
"tough-cookie": "^4.0.0",
|
|
39
|
+
"winston": "^3.3.3",
|
|
39
40
|
"xml-js": "^1.6.11"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
package/src/utils.js
CHANGED
|
@@ -59,11 +59,12 @@ utils.parseChannels = function (xml) {
|
|
|
59
59
|
const channel = el.attributes
|
|
60
60
|
if (!el.elements) throw new Error(`Channel '${channel.xmltv_id}' has no valid name`)
|
|
61
61
|
channel.name = el.elements.find(el => el.type === 'text').text
|
|
62
|
+
channel.site = channel.site || site
|
|
62
63
|
|
|
63
64
|
return channel
|
|
64
65
|
})
|
|
65
66
|
|
|
66
|
-
return
|
|
67
|
+
return channels
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
utils.sleep = function (ms) {
|
|
@@ -122,8 +123,8 @@ utils.convertToXMLTV = function ({ channels, programs }) {
|
|
|
122
123
|
const title = utils.escapeString(program.title)
|
|
123
124
|
const description = utils.escapeString(program.description)
|
|
124
125
|
const categories = Array.isArray(program.category) ? program.category : [program.category]
|
|
125
|
-
const start = program.start ? dayjs.
|
|
126
|
-
const stop = program.stop ? dayjs.
|
|
126
|
+
const start = program.start ? dayjs.unix(program.start).utc().format('YYYYMMDDHHmmss ZZ') : ''
|
|
127
|
+
const stop = program.stop ? dayjs.unix(program.stop).utc().format('YYYYMMDDHHmmss ZZ') : ''
|
|
127
128
|
const lang = program.lang || 'en'
|
|
128
129
|
const icon = utils.escapeString(program.icon)
|
|
129
130
|
|
|
@@ -264,9 +265,16 @@ utils.parsePrograms = async function (data, config) {
|
|
|
264
265
|
return programs
|
|
265
266
|
.filter(i => i)
|
|
266
267
|
.map(program => {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
268
|
+
return {
|
|
269
|
+
title: program.title,
|
|
270
|
+
description: program.description || null,
|
|
271
|
+
category: program.category || null,
|
|
272
|
+
icon: program.icon || null,
|
|
273
|
+
channel: channel.xmltv_id,
|
|
274
|
+
lang: program.lang || channel.lang || config.lang || 'en',
|
|
275
|
+
start: program.start ? dayjs(program.start).unix() : null,
|
|
276
|
+
stop: program.stop ? dayjs(program.stop).unix() : null
|
|
277
|
+
}
|
|
270
278
|
})
|
|
271
279
|
}
|
|
272
280
|
|
package/tests/bin.test.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
const { execSync } = require('child_process')
|
|
2
2
|
const pwd = `${__dirname}/..`
|
|
3
|
+
import axios from 'axios'
|
|
4
|
+
|
|
5
|
+
jest.mock('axios')
|
|
3
6
|
|
|
4
7
|
function stdoutResultTester(stdout) {
|
|
5
8
|
return [`Finish`].every(val => {
|
|
@@ -19,6 +22,8 @@ it('can load config', () => {
|
|
|
19
22
|
})
|
|
20
23
|
|
|
21
24
|
it('can load mini config', () => {
|
|
25
|
+
axios.mockImplementation(() => Promise.resolve({ data: '' }))
|
|
26
|
+
|
|
22
27
|
const result = execSync(
|
|
23
28
|
`node ${pwd}/bin/epg-grabber.js \
|
|
24
29
|
--config=tests/input/mini.config.js \
|
|
@@ -26,7 +31,8 @@ it('can load mini config', () => {
|
|
|
26
31
|
--output=tests/output/mini.guide.xml \
|
|
27
32
|
--lang=fr \
|
|
28
33
|
--days=3 \
|
|
29
|
-
--delay=0
|
|
34
|
+
--delay=0 \
|
|
35
|
+
--timeout=10000`,
|
|
30
36
|
{
|
|
31
37
|
encoding: 'utf8'
|
|
32
38
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
2
|
<site site="example.com">
|
|
3
3
|
<channels>
|
|
4
|
-
<channel xmltv_id="1TV.com"
|
|
4
|
+
<channel xmltv_id="1TV.com" site_id="1" lang="fr" logo="https://example.com/logos/1TV.png">1 TV</channel>
|
|
5
5
|
<channel xmltv_id="2TV.com" site="example.com" site_id="2">2 TV</channel>
|
|
6
6
|
</channels>
|
|
7
7
|
</site>
|
package/tests/utils.test.js
CHANGED
|
@@ -28,9 +28,8 @@ it('can load valid config.js', () => {
|
|
|
28
28
|
|
|
29
29
|
it('can parse valid channels.xml', () => {
|
|
30
30
|
const file = fs.readFileSync('./tests/input/example.com.channels.xml', { encoding: 'utf-8' })
|
|
31
|
-
const
|
|
32
|
-
expect(
|
|
33
|
-
expect(parsed.channels).toEqual([
|
|
31
|
+
const channels = utils.parseChannels(file)
|
|
32
|
+
expect(channels).toEqual([
|
|
34
33
|
{
|
|
35
34
|
name: '1 TV',
|
|
36
35
|
xmltv_id: '1TV.com',
|
|
@@ -52,22 +51,20 @@ it('can parse valid channels.xml', () => {
|
|
|
52
51
|
|
|
53
52
|
it('can convert object to xmltv string', () => {
|
|
54
53
|
const file = fs.readFileSync('./tests/input/example.com.channels.xml', { encoding: 'utf-8' })
|
|
55
|
-
const
|
|
56
|
-
const channels = parsed.channels
|
|
54
|
+
const channels = utils.parseChannels(file)
|
|
57
55
|
const programs = [
|
|
58
56
|
{
|
|
59
57
|
title: 'Program 1',
|
|
60
58
|
description: 'Description for Program 1',
|
|
61
|
-
start:
|
|
62
|
-
stop:
|
|
59
|
+
start: 1616133600,
|
|
60
|
+
stop: 1616135400,
|
|
63
61
|
category: 'Test',
|
|
64
62
|
icon: 'https://example.com/images/Program1.png?x=шеллы&sid=777',
|
|
65
63
|
channel: '1TV.com',
|
|
66
64
|
lang: 'it'
|
|
67
65
|
}
|
|
68
66
|
]
|
|
69
|
-
const
|
|
70
|
-
const output = utils.convertToXMLTV({ config, channels, programs })
|
|
67
|
+
const output = utils.convertToXMLTV({ channels, programs })
|
|
71
68
|
expect(output).toBe(
|
|
72
69
|
'<?xml version="1.0" encoding="UTF-8" ?><tv>\r\n<channel id="1TV.com"><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.com"><display-name>2 TV</display-name><url>https://example.com</url></channel>\r\n<programme start="20210319060000 +0000" stop="20210319063000 +0000" channel="1TV.com"><title lang="it">Program 1</title><desc lang="it">Description for Program 1</desc><category lang="it">Test</category><icon src="https://example.com/images/Program1.png?x=шеллы&sid=777"/></programme>\r\n</tv>'
|
|
73
70
|
)
|
|
@@ -87,8 +84,8 @@ it('can convert object to xmltv string without categories', () => {
|
|
|
87
84
|
const programs = [
|
|
88
85
|
{
|
|
89
86
|
title: 'Program 1',
|
|
90
|
-
start:
|
|
91
|
-
stop:
|
|
87
|
+
start: 1616133600,
|
|
88
|
+
stop: 1616135400,
|
|
92
89
|
channel: '1TV.com',
|
|
93
90
|
lang: 'it'
|
|
94
91
|
}
|
|
@@ -102,22 +99,20 @@ it('can convert object to xmltv string without categories', () => {
|
|
|
102
99
|
|
|
103
100
|
it('can convert object to xmltv string with multiple categories', () => {
|
|
104
101
|
const file = fs.readFileSync('./tests/input/example.com.channels.xml', { encoding: 'utf-8' })
|
|
105
|
-
const
|
|
106
|
-
const channels = parsed.channels
|
|
102
|
+
const channels = utils.parseChannels(file)
|
|
107
103
|
const programs = [
|
|
108
104
|
{
|
|
109
105
|
title: 'Program 1',
|
|
110
106
|
description: 'Description for Program 1',
|
|
111
|
-
start:
|
|
112
|
-
stop:
|
|
107
|
+
start: 1616133600,
|
|
108
|
+
stop: 1616135400,
|
|
113
109
|
category: ['Test1', 'Test2'],
|
|
114
110
|
icon: 'https://example.com/images/Program1.png?x=шеллы&sid=777',
|
|
115
111
|
channel: '1TV.com',
|
|
116
112
|
lang: 'it'
|
|
117
113
|
}
|
|
118
114
|
]
|
|
119
|
-
const
|
|
120
|
-
const output = utils.convertToXMLTV({ config, channels, programs })
|
|
115
|
+
const output = utils.convertToXMLTV({ channels, programs })
|
|
121
116
|
expect(output).toBe(
|
|
122
117
|
'<?xml version="1.0" encoding="UTF-8" ?><tv>\r\n<channel id="1TV.com"><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.com"><display-name>2 TV</display-name><url>https://example.com</url></channel>\r\n<programme start="20210319060000 +0000" stop="20210319063000 +0000" channel="1TV.com"><title lang="it">Program 1</title><desc lang="it">Description for Program 1</desc><category lang="it">Test1</category><category lang="it">Test2</category><icon src="https://example.com/images/Program1.png?x=шеллы&sid=777"/></programme>\r\n</tv>'
|
|
123
118
|
)
|