epg-grabber 0.28.6 → 0.29.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,7 @@ const { EPGGrabber, parseChannels, generateXMLTV } = require('../src/index')
9
9
  const { create: createLogger } = require('../src/logger')
10
10
  const { parseNumber, getUTCDate } = require('../src/utils')
11
11
  const { name, version, description } = require('../package.json')
12
+ const _ = require('lodash')
12
13
  const dayjs = require('dayjs')
13
14
  const utc = require('dayjs/plugin/utc')
14
15
 
@@ -99,6 +100,8 @@ async function main() {
99
100
  }
100
101
  }
101
102
 
103
+ programs = _.uniqBy(programs, p => p.start + p.channel)
104
+
102
105
  const xml = generateXMLTV({ channels, programs })
103
106
  let outputPath = options.output || config.output
104
107
  if (options.gzip) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "epg-grabber",
3
- "version": "0.28.6",
3
+ "version": "0.29.2",
4
4
  "description": "Node.js CLI tool for grabbing EPG from different sites",
5
5
  "main": "src/index.js",
6
6
  "preferGlobal": true,
@@ -35,6 +35,7 @@
35
35
  "commander": "^7.1.0",
36
36
  "curl-generator": "^0.2.0",
37
37
  "dayjs": "^1.10.4",
38
+ "epg-parser": "^0.1.6",
38
39
  "glob": "^7.1.6",
39
40
  "lodash": "^4.17.21",
40
41
  "node-gzip": "^1.1.2",
package/src/Program.js CHANGED
@@ -1,14 +1,21 @@
1
1
  const { padStart } = require('lodash')
2
2
  const { toArray, toUnix, parseNumber } = require('./utils')
3
+ const Channel = require('./Channel')
3
4
 
4
5
  class Program {
5
- constructor(p) {
6
+ constructor(p, c) {
7
+ if (!(c instanceof Channel)) {
8
+ throw new Error('The second argument in the constructor must be the "Channel" class')
9
+ }
10
+
6
11
  const data = {
7
- site: p.site || '',
8
- channel: p.channel || '',
9
- title: p.title || '',
10
- sub_title: p.sub_title || '',
11
- description: [p.description, p.desc].find(i => i) || '',
12
+ site: p.site || c.site || '',
13
+ channel: p.channel || c.id || '',
14
+ titles: toArray(p.titles || p.title).map(text => toTextObject(text, c.lang)),
15
+ sub_titles: toArray(p.sub_titles || p.sub_title).map(text => toTextObject(text, c.lang)),
16
+ descriptions: toArray(p.descriptions || p.description || p.desc).map(text =>
17
+ toTextObject(text, c.lang)
18
+ ),
12
19
  icon: toIconObject(p.icon),
13
20
  episodeNumbers: p.episodeNumbers || getEpisodeNumbers(p.season, p.episode),
14
21
  date: p.date ? toUnix(p.date) : null,
@@ -16,7 +23,7 @@ class Program {
16
23
  stop: p.stop ? toUnix(p.stop) : null,
17
24
  urls: toArray(p.urls || p.url).map(toUrlObject),
18
25
  ratings: toArray(p.ratings || p.rating).map(toRatingObject),
19
- categories: toArray(p.categories || p.category),
26
+ categories: toArray(p.categories || p.category).map(text => toTextObject(text, c.lang)),
20
27
  directors: toArray(p.directors || p.director).map(toPersonObject),
21
28
  actors: toArray(p.actors || p.actor).map(toPersonObject),
22
29
  writers: toArray(p.writers || p.writer).map(toPersonObject),
@@ -37,6 +44,17 @@ class Program {
37
44
 
38
45
  module.exports = Program
39
46
 
47
+ function toTextObject(text, lang) {
48
+ if (typeof text === 'string') {
49
+ return { value: text, lang }
50
+ }
51
+
52
+ return {
53
+ value: text.value,
54
+ lang: text.lang
55
+ }
56
+ }
57
+
40
58
  function toPersonObject(person) {
41
59
  if (typeof person === 'string') {
42
60
  return {
package/src/index.js CHANGED
@@ -27,6 +27,10 @@ class EPGGrabber {
27
27
  }
28
28
 
29
29
  async grab(channel, date, cb = () => {}) {
30
+ if (!(channel instanceof Channel)) {
31
+ throw new Error('The first argument must be the "Channel" class')
32
+ }
33
+
30
34
  await sleep(this.config.delay)
31
35
 
32
36
  date = typeof date === 'string' ? getUTCDate(date) : date
package/src/parser.js CHANGED
@@ -41,12 +41,5 @@ async function parsePrograms(data) {
41
41
  throw new Error('Parser should return an array')
42
42
  }
43
43
 
44
- return programs
45
- .filter(i => i)
46
- .map(p => {
47
- p.site = channel.site
48
- p.channel = p.channel || channel.id
49
-
50
- return new Program(p)
51
- })
44
+ return programs.filter(i => i).map(p => new Program(p, channel))
52
45
  }
package/src/xmltv.js CHANGED
@@ -47,9 +47,15 @@ function createElements(channels, programs, date) {
47
47
  channel: program.channel
48
48
  },
49
49
  [
50
- el('title', {}, [escapeString(program.title)]),
51
- el('sub-title', {}, [escapeString(program.sub_title)]),
52
- el('desc', {}, [escapeString(program.description)]),
50
+ ...program.titles.map(title =>
51
+ el('title', { lang: title.lang }, [escapeString(title.value)])
52
+ ),
53
+ ...program.sub_titles.map(sub_title =>
54
+ el('sub-title', { lang: sub_title.lang }, [escapeString(sub_title.value)])
55
+ ),
56
+ ...program.descriptions.map(desc =>
57
+ el('desc', { lang: desc.lang }, [escapeString(desc.value)])
58
+ ),
53
59
  el('credits', {}, [
54
60
  ...program.directors.map(data => createCastMember('director', data)),
55
61
  ...program.actors.map(data => createCastMember('actor', data)),
@@ -63,7 +69,9 @@ function createElements(channels, programs, date) {
63
69
  ...program.guests.map(data => createCastMember('guest', data))
64
70
  ]),
65
71
  el('date', {}, [formatDate(program.date, 'YYYYMMDD')]),
66
- ...program.categories.map(category => el('category', {}, [escapeString(category)])),
72
+ ...program.categories.map(category =>
73
+ el('category', { lang: category.lang }, [escapeString(category.value)])
74
+ ),
67
75
  el('icon', { src: program.icon.src }),
68
76
  ...program.urls.map(createURL),
69
77
  ...program.episodeNumbers.map(episode =>
@@ -1,61 +1,65 @@
1
1
  import Channel from '../src/Channel'
2
2
  import Program from '../src/Program'
3
3
 
4
- const channel = new Channel({ xmltv_id: '1tv', lang: 'en', site: 'example.com' })
4
+ const channel = new Channel({ xmltv_id: '1tv', lang: 'fr', site: 'example.com' })
5
5
 
6
6
  it('can create new Program', () => {
7
- const program = new Program({
8
- site: channel.site,
9
- channel: channel.id,
10
- title: 'Title',
11
- sub_title: 'Subtitle',
12
- description: 'Description',
13
- icon: 'https://example.com/image.jpg',
14
- season: 9,
15
- episode: 238,
16
- date: '20220506',
17
- start: 1616133600000,
18
- stop: '2021-03-19T06:30:00.000Z',
19
- url: 'http://example.com/title.html',
20
- category: ['Category1', 'Category2'],
21
- rating: {
22
- system: 'MPAA',
23
- value: 'PG',
24
- icon: 'http://example.com/pg_symbol.png'
25
- },
26
- directors: 'Director1',
27
- actors: [
28
- 'Actor1',
29
- { value: 'Actor2', url: 'http://actor2.com', image: 'http://actor2.com/image.png' }
30
- ],
31
- writer: {
32
- value: 'Writer1',
33
- url: { system: 'imdb', value: 'http://imdb.com/p/writer1' },
34
- image: {
35
- value: 'https://example.com/image.jpg',
36
- type: 'person',
37
- size: '2',
38
- system: 'TestSystem',
39
- orient: 'P'
40
- }
7
+ const program = new Program(
8
+ {
9
+ title: 'Title',
10
+ sub_title: 'Subtitle',
11
+ description: 'Description',
12
+ icon: 'https://example.com/image.jpg',
13
+ season: 9,
14
+ episode: 238,
15
+ date: '20220506',
16
+ start: 1616133600000,
17
+ stop: '2021-03-19T06:30:00.000Z',
18
+ url: 'http://example.com/title.html',
19
+ category: ['Category1', 'Category2'],
20
+ rating: {
21
+ system: 'MPAA',
22
+ value: 'PG',
23
+ icon: 'http://example.com/pg_symbol.png'
24
+ },
25
+ directors: 'Director1',
26
+ actors: [
27
+ 'Actor1',
28
+ { value: 'Actor2', url: 'http://actor2.com', image: 'http://actor2.com/image.png' }
29
+ ],
30
+ writer: {
31
+ value: 'Writer1',
32
+ url: { system: 'imdb', value: 'http://imdb.com/p/writer1' },
33
+ image: {
34
+ value: 'https://example.com/image.jpg',
35
+ type: 'person',
36
+ size: '2',
37
+ system: 'TestSystem',
38
+ orient: 'P'
39
+ }
40
+ },
41
+ adapters: [
42
+ {
43
+ value: 'Adapter1',
44
+ url: ['http://imdb.com/p/adapter1', 'http://imdb.com/p/adapter2'],
45
+ image: ['https://example.com/image1.jpg', 'https://example.com/image2.jpg']
46
+ }
47
+ ]
41
48
  },
42
- adapters: [
43
- {
44
- value: 'Adapter1',
45
- url: ['http://imdb.com/p/adapter1', 'http://imdb.com/p/adapter2'],
46
- image: ['https://example.com/image1.jpg', 'https://example.com/image2.jpg']
47
- }
48
- ]
49
- })
49
+ channel
50
+ )
50
51
 
51
52
  expect(program).toMatchObject({
52
53
  site: 'example.com',
53
54
  channel: '1tv',
54
- title: 'Title',
55
- sub_title: 'Subtitle',
56
- description: 'Description',
55
+ titles: [{ value: 'Title', lang: 'fr' }],
56
+ sub_titles: [{ value: 'Subtitle', lang: 'fr' }],
57
+ descriptions: [{ value: 'Description', lang: 'fr' }],
57
58
  urls: [{ system: '', value: 'http://example.com/title.html' }],
58
- categories: ['Category1', 'Category2'],
59
+ categories: [
60
+ { value: 'Category1', lang: 'fr' },
61
+ { value: 'Category2', lang: 'fr' }
62
+ ],
59
63
  icon: { src: 'https://example.com/image.jpg' },
60
64
  episodeNumbers: [
61
65
  { system: 'xmltv_ns', value: '8.237.0/1' },
@@ -132,24 +136,26 @@ it('can create new Program', () => {
132
136
  })
133
137
 
134
138
  it('can create program from exist object', () => {
135
- const program = new Program({
136
- channel: channel.id,
137
- title: 'Program 1',
138
- start: '2021-03-19T06:00:00.000Z',
139
- stop: '2021-03-19T06:30:00.000Z',
140
- ratings: {
141
- system: 'MPAA',
142
- value: 'PG',
143
- icon: 'http://example.com/pg_symbol.png'
139
+ const program = new Program(
140
+ {
141
+ titles: [{ value: 'Program 1', lang: 'de' }],
142
+ start: '2021-03-19T06:00:00.000Z',
143
+ stop: '2021-03-19T06:30:00.000Z',
144
+ ratings: {
145
+ system: 'MPAA',
146
+ value: 'PG',
147
+ icon: 'http://example.com/pg_symbol.png'
148
+ },
149
+ actors: [{ value: 'Actor1', url: [], image: [] }]
144
150
  },
145
- actors: [{ value: 'Actor1', url: [], image: [] }]
146
- })
151
+ channel
152
+ )
147
153
 
148
154
  expect(program).toMatchObject({
149
155
  channel: '1tv',
150
- title: 'Program 1',
151
- sub_title: '',
152
- description: '',
156
+ titles: [{ value: 'Program 1', lang: 'de' }],
157
+ sub_titles: [],
158
+ descriptions: [],
153
159
  urls: [],
154
160
  categories: [],
155
161
  icon: {},
@@ -178,13 +184,15 @@ it('can create program from exist object', () => {
178
184
  })
179
185
 
180
186
  it('can create program without season number', () => {
181
- const program = new Program({
182
- channel: channel.id,
183
- title: 'Program 1',
184
- start: '2021-03-19T06:00:00.000Z',
185
- stop: '2021-03-19T06:30:00.000Z',
186
- episode: 238
187
- })
187
+ const program = new Program(
188
+ {
189
+ title: 'Program 1',
190
+ start: '2021-03-19T06:00:00.000Z',
191
+ stop: '2021-03-19T06:30:00.000Z',
192
+ episode: 238
193
+ },
194
+ channel
195
+ )
188
196
 
189
197
  expect(program.episodeNumbers).toMatchObject([
190
198
  { system: 'xmltv_ns', value: '0.237.0/1' },
@@ -193,13 +201,16 @@ it('can create program without season number', () => {
193
201
  })
194
202
 
195
203
  it('can create program without episode number', () => {
196
- const program = new Program({
197
- channel: channel.id,
198
- title: 'Program 1',
199
- start: '2021-03-19T06:00:00.000Z',
200
- stop: '2021-03-19T06:30:00.000Z',
201
- season: 3
202
- })
204
+ const program = new Program(
205
+ {
206
+ channel: channel.id,
207
+ title: 'Program 1',
208
+ start: '2021-03-19T06:00:00.000Z',
209
+ stop: '2021-03-19T06:30:00.000Z',
210
+ season: 3
211
+ },
212
+ channel
213
+ )
203
214
 
204
215
  expect(program.episodeNumbers).toMatchObject([])
205
216
  })
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8" ?><tv date="20220828">
2
+ <channel id="1TV.com"><display-name>1 TV</display-name><icon src="https://example.com/logos/1TV.png"/><url>https://example.com</url></channel>
3
+ <channel id="2TV.com"><display-name>2 TV</display-name><url>https://example.com</url></channel>
4
+ <programme start="20220101000000 +0000" stop="20220101010000 +0000" channel="1TV.com"><title lang="fr">Program1</title></programme>
5
+ <programme start="20220101000000 +0000" stop="20220101010000 +0000" channel="2TV.com"><title>Program1</title></programme>
6
+ </tv>
File without changes
@@ -0,0 +1,18 @@
1
+ module.exports = {
2
+ site: 'example.com',
3
+ url: 'https://google.com',
4
+ parser() {
5
+ return [
6
+ {
7
+ title: 'Program1',
8
+ start: 1640995200000,
9
+ stop: 1640998800000
10
+ },
11
+ {
12
+ title: 'Program1',
13
+ start: 1640995200000,
14
+ stop: 1640998900000
15
+ }
16
+ ]
17
+ }
18
+ }
File without changes
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8" ?><tv date="20220829">
2
+ <channel id="1TV.com"><display-name>1 TV</display-name><icon src="https://example.com/logos/1TV.png"/><url>https://example.com</url></channel>
3
+ <channel id="2TV.com"><display-name>2 TV</display-name><url>https://example.com</url></channel>
4
+ <programme start="20220101000000 +0000" stop="20220101010000 +0000" channel="1TV.com"><title lang="fr">Program1</title></programme>
5
+ <programme start="20220101000000 +0000" stop="20220101010000 +0000" channel="2TV.com"><title>Program1</title></programme>
6
+ </tv>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="UTF-8" ?><tv date="20220829">
2
+ <channel id="1TV.com"><display-name>1 TV</display-name><icon src="https://example.com/logos/1TV.png"/><url>https://example.com</url></channel>
3
+ <channel id="2TV.com"><display-name>2 TV</display-name><url>https://example.com</url></channel>
4
+ </tv>
package/tests/bin.test.js CHANGED
@@ -1,4 +1,7 @@
1
1
  const { execSync } = require('child_process')
2
+ const fs = require('fs')
3
+ const path = require('path')
4
+ const epgParser = require('epg-parser')
2
5
 
3
6
  const pwd = `${__dirname}/..`
4
7
 
@@ -10,7 +13,7 @@ function stdoutResultTester(stdout) {
10
13
 
11
14
  it('can load config', () => {
12
15
  const result = execSync(
13
- `node ${pwd}/bin/epg-grabber.js --config=tests/input/example.config.js --delay=0`,
16
+ `node ${pwd}/bin/epg-grabber.js --config=tests/__data__/input/example.config.js --delay=0`,
14
17
  {
15
18
  encoding: 'utf8'
16
19
  }
@@ -22,9 +25,9 @@ it('can load config', () => {
22
25
  it('can load mini config', () => {
23
26
  const result = execSync(
24
27
  `node ${pwd}/bin/epg-grabber.js \
25
- --config=tests/input/mini.config.js \
26
- --channels=tests/input/example.channels.xml \
27
- --output=tests/output/mini.guide.xml \
28
+ --config=tests/__data__/input/mini.config.js \
29
+ --channels=tests/__data__/input/example.channels.xml \
30
+ --output=tests/__data__/output/mini.guide.xml \
28
31
  --lang=fr \
29
32
  --days=3 \
30
33
  --delay=0 \
@@ -36,15 +39,17 @@ it('can load mini config', () => {
36
39
  )
37
40
 
38
41
  expect(stdoutResultTester(result)).toBe(true)
39
- expect(result.includes("File 'tests/output/mini.guide.xml' successfully saved")).toBe(true)
42
+ expect(result.includes("File 'tests/__data__/output/mini.guide.xml' successfully saved")).toBe(
43
+ true
44
+ )
40
45
  })
41
46
 
42
47
  it('can generate gzip version', () => {
43
48
  const result = execSync(
44
49
  `node ${pwd}/bin/epg-grabber.js \
45
- --config=tests/input/mini.config.js \
46
- --channels=tests/input/example.channels.xml \
47
- --output=tests/output/mini.guide.xml.gz \
50
+ --config=tests/__data__/input/mini.config.js \
51
+ --channels=tests/__data__/input/example.channels.xml \
52
+ --output=tests/__data__/output/mini.guide.xml.gz \
48
53
  --gzip`,
49
54
  {
50
55
  encoding: 'utf8'
@@ -52,5 +57,27 @@ it('can generate gzip version', () => {
52
57
  )
53
58
 
54
59
  expect(stdoutResultTester(result)).toBe(true)
55
- expect(result.includes("File 'tests/output/mini.guide.xml.gz' successfully saved")).toBe(true)
60
+ expect(result.includes("File 'tests/__data__/output/mini.guide.xml.gz' successfully saved")).toBe(
61
+ true
62
+ )
63
+ })
64
+
65
+ it('removes duplicates of the program', () => {
66
+ const result = execSync(
67
+ `node ${pwd}/bin/epg-grabber.js \
68
+ --config=tests/__data__/input/duplicates.config.js \
69
+ --channels=tests/__data__/input/example.channels.xml \
70
+ --output=tests/__data__/output/duplicates.guide.xml`,
71
+ {
72
+ encoding: 'utf8'
73
+ }
74
+ )
75
+
76
+ let output = fs.readFileSync(path.resolve(__dirname, '__data__/output/duplicates.guide.xml'))
77
+ let expected = fs.readFileSync(path.resolve(__dirname, '__data__/expected/duplicates.guide.xml'))
78
+
79
+ output = epgParser.parse(output)
80
+ expected = epgParser.parse(expected)
81
+
82
+ expect(output.programs).toEqual(expected.programs)
56
83
  })
@@ -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.config.js')))
6
+ const config = loadConfig(require(path.resolve('./tests/__data__/input/example.config.js')))
7
7
  expect(config).toMatchObject({
8
8
  days: 1,
9
9
  delay: 3000,
@@ -2,7 +2,7 @@
2
2
  * @jest-environment node
3
3
  */
4
4
 
5
- import { EPGGrabber } from '../src/index'
5
+ import { EPGGrabber, Channel } from '../src/index'
6
6
  import axios from 'axios'
7
7
 
8
8
  jest.mock('axios')
@@ -20,13 +20,13 @@ it('return "Connection timeout" error if server does not response', done => {
20
20
  },
21
21
  parser: () => []
22
22
  }
23
- const channel = {
23
+ const channel = new Channel({
24
24
  site: 'example.com',
25
25
  site_id: 'cnn',
26
26
  xmltv_id: 'CNN.us',
27
27
  lang: 'en',
28
28
  name: 'CNN'
29
- }
29
+ })
30
30
  const grabber = new EPGGrabber(config)
31
31
  grabber.grab(channel, '2022-01-01', (data, err) => {
32
32
  expect(err.message).toBe('Connection timeout')
@@ -47,13 +47,13 @@ it('can grab single channel programs', done => {
47
47
  url: 'http://example.com/20210319/1tv.json',
48
48
  parser: () => []
49
49
  }
50
- const channel = {
50
+ const channel = new Channel({
51
51
  site: 'example.com',
52
52
  site_id: '1',
53
53
  xmltv_id: '1TV.fr',
54
54
  lang: 'fr',
55
55
  name: '1TV'
56
- }
56
+ })
57
57
  const grabber = new EPGGrabber(config)
58
58
  grabber
59
59
  .grab(channel, '2022-01-01', (data, err) => {
@@ -4,7 +4,7 @@ import Program from '../src/Program'
4
4
  import fs from 'fs'
5
5
 
6
6
  it('can parse valid channels.xml', () => {
7
- const file = fs.readFileSync('./tests/input/example.channels.xml', { encoding: 'utf-8' })
7
+ const file = fs.readFileSync('./tests/__data__/input/example.channels.xml', { encoding: 'utf-8' })
8
8
  const { channels, site } = parseChannels(file)
9
9
 
10
10
  expect(typeof site).toBe('string')
@@ -14,8 +14,8 @@ it('can parse valid channels.xml', () => {
14
14
  })
15
15
 
16
16
  it('can parse programs', done => {
17
- const channel = { xmltv_id: '1tv' }
18
- const config = require('./input/example.config.js')
17
+ const channel = new Channel({ xmltv_id: '1tv' })
18
+ const config = require('./__data__/input/example.config.js')
19
19
 
20
20
  parsePrograms({ channel, config })
21
21
  .then(programs => {
@@ -27,8 +27,8 @@ it('can parse programs', done => {
27
27
  })
28
28
 
29
29
  it('can parse programs async', done => {
30
- const channel = { xmltv_id: '1tv' }
31
- const config = require('./input/async.config.js')
30
+ const channel = new Channel({ xmltv_id: '1tv' })
31
+ const config = require('./__data__/input/async.config.js')
32
32
 
33
33
  parsePrograms({ channel, config })
34
34
  .then(programs => {
@@ -14,11 +14,12 @@ const channels = [
14
14
  new Channel({
15
15
  xmltv_id: '2TV.co',
16
16
  name: '2 TV',
17
- site: 'example.com'
17
+ site: 'example.com',
18
+ lang: 'es'
18
19
  })
19
20
  ]
20
21
 
21
- fit('can generate xmltv', () => {
22
+ it('can generate xmltv', () => {
22
23
  const programs = [
23
24
  new Program(
24
25
  {
@@ -33,7 +34,6 @@ fit('can generate xmltv', () => {
33
34
  season: 9,
34
35
  episode: 239,
35
36
  icon: 'https://example.com/images/Program1.png?x=шеллы&sid=777',
36
- channel: '1TV.co',
37
37
  rating: {
38
38
  system: 'MPAA',
39
39
  value: 'PG',
@@ -60,12 +60,20 @@ fit('can generate xmltv', () => {
60
60
  writer: 'Writer 1'
61
61
  },
62
62
  channels[0]
63
+ ),
64
+ new Program(
65
+ {
66
+ title: 'Program 2',
67
+ start: '2021-03-19T06:00:00.000Z',
68
+ stop: '2021-03-19T06:30:00.000Z'
69
+ },
70
+ channels[1]
63
71
  )
64
72
  ]
65
73
 
66
74
  const output = xmltv.generate({ channels, programs })
67
75
 
68
76
  expect(output).toBe(
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>'
77
+ '<?xml version="1.0" encoding="UTF-8" ?><tv date="20220505">\r\n<channel id="1TV.co"><display-name>1 TV</display-name><icon src="https://example.com/logos/1TV.png"/><url>https://example.com</url></channel>\r\n<channel id="2TV.co"><display-name>2 TV</display-name><url>https://example.com</url></channel>\r\n<programme start="20210319060000 +0000" stop="20210319063000 +0000" channel="1TV.co"><title>Program 1</title><sub-title>Sub-title &amp; 1</sub-title><desc>Description for Program 1</desc><credits><director>Director 1<url system="TestSystem">http://example.com/director1.html</url><image>https://example.com/image1.jpg</image><image type="person" size="2" orient="P" system="TestSystem">https://example.com/image2.jpg</image></director><director>Director 2</director><actor>Actor 1</actor><actor>Actor 2</actor><writer>Writer 1</writer></credits><date>20220506</date><category>Test</category><icon src="https://example.com/images/Program1.png?x=шеллы&amp;sid=777"/><url>http://example.com/title.html</url><episode-num system="xmltv_ns">8.238.0/1</episode-num><episode-num system="onscreen">S09E239</episode-num><rating system="MPAA"><value>PG</value><icon src="http://example.com/pg_symbol.png"/></rating></programme>\r\n<programme start="20210319060000 +0000" stop="20210319063000 +0000" channel="2TV.co"><title lang="es">Program 2</title></programme>\r\n</tv>'
70
78
  )
71
79
  })