epg-grabber 0.28.4 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/Program.js +30 -10
- package/src/client.js +1 -2
- package/src/index.js +6 -1
- package/src/parser.js +1 -7
- package/src/utils.js +5 -0
- package/src/xmltv.js +27 -5
- package/tests/Program.test.js +87 -74
- package/tests/parser.test.js +2 -2
- package/tests/xmltv.test.js +12 -4
package/package.json
CHANGED
package/src/Program.js
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
+
),
|
|
11
19
|
icon: toIconObject(p.icon),
|
|
12
|
-
episodeNumbers: getEpisodeNumbers(p.season, p.episode),
|
|
20
|
+
episodeNumbers: p.episodeNumbers || getEpisodeNumbers(p.season, p.episode),
|
|
13
21
|
date: p.date ? toUnix(p.date) : null,
|
|
14
|
-
start: toUnix(p.start),
|
|
15
|
-
stop: toUnix(p.stop),
|
|
22
|
+
start: p.start ? toUnix(p.start) : null,
|
|
23
|
+
stop: p.stop ? toUnix(p.stop) : null,
|
|
16
24
|
urls: toArray(p.urls || p.url).map(toUrlObject),
|
|
17
25
|
ratings: toArray(p.ratings || p.rating).map(toRatingObject),
|
|
18
|
-
categories: toArray(p.categories || p.category),
|
|
26
|
+
categories: toArray(p.categories || p.category).map(text => toTextObject(text, c.lang)),
|
|
19
27
|
directors: toArray(p.directors || p.director).map(toPersonObject),
|
|
20
28
|
actors: toArray(p.actors || p.actor).map(toPersonObject),
|
|
21
29
|
writers: toArray(p.writers || p.writer).map(toPersonObject),
|
|
@@ -36,6 +44,17 @@ class Program {
|
|
|
36
44
|
|
|
37
45
|
module.exports = Program
|
|
38
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
|
+
|
|
39
58
|
function toPersonObject(person) {
|
|
40
59
|
if (typeof person === 'string') {
|
|
41
60
|
return {
|
|
@@ -84,7 +103,8 @@ function toUrlObject(url) {
|
|
|
84
103
|
}
|
|
85
104
|
|
|
86
105
|
function toIconObject(icon) {
|
|
87
|
-
if (!icon
|
|
106
|
+
if (!icon) return { src: '' }
|
|
107
|
+
if (typeof icon === 'string') return { src: icon }
|
|
88
108
|
|
|
89
109
|
return {
|
|
90
110
|
src: icon.src || ''
|
package/src/client.js
CHANGED
|
@@ -2,7 +2,7 @@ const { CurlGenerator } = require('curl-generator')
|
|
|
2
2
|
const axios = require('axios').default
|
|
3
3
|
const axiosCookieJarSupport = require('axios-cookiejar-support').default
|
|
4
4
|
const { setupCache } = require('axios-cache-interceptor')
|
|
5
|
-
const { isObject, isPromise
|
|
5
|
+
const { isObject, isPromise } = require('./utils')
|
|
6
6
|
|
|
7
7
|
axiosCookieJarSupport(axios)
|
|
8
8
|
|
|
@@ -68,7 +68,6 @@ function create(config) {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
async function buildRequest({ channel, date, config }) {
|
|
71
|
-
date = typeof date === 'string' ? getUTCDate(date) : date
|
|
72
71
|
const CancelToken = axios.CancelToken
|
|
73
72
|
const source = CancelToken.source()
|
|
74
73
|
const request = { ...config.request }
|
package/src/index.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
const { merge } = require('lodash')
|
|
2
2
|
const { create: createClient, buildRequest, parseResponse } = require('./client')
|
|
3
3
|
const { parseChannels, parsePrograms } = require('./parser')
|
|
4
|
+
const { sleep, isPromise, getUTCDate } = require('./utils')
|
|
4
5
|
const { generate: generateXMLTV } = require('./xmltv')
|
|
5
6
|
const { load: loadConfig } = require('./config')
|
|
6
|
-
const
|
|
7
|
+
const Channel = require('./Channel')
|
|
8
|
+
const Program = require('./Program')
|
|
7
9
|
|
|
8
10
|
module.exports.generateXMLTV = generateXMLTV
|
|
9
11
|
module.exports.parseChannels = parseChannels
|
|
12
|
+
module.exports.Channel = Channel
|
|
13
|
+
module.exports.Program = Program
|
|
10
14
|
|
|
11
15
|
class EPGGrabber {
|
|
12
16
|
constructor(config = {}) {
|
|
@@ -25,6 +29,7 @@ class EPGGrabber {
|
|
|
25
29
|
async grab(channel, date, cb = () => {}) {
|
|
26
30
|
await sleep(this.config.delay)
|
|
27
31
|
|
|
32
|
+
date = typeof date === 'string' ? getUTCDate(date) : date
|
|
28
33
|
return buildRequest({ channel, date, config: this.config })
|
|
29
34
|
.then(this.client)
|
|
30
35
|
.then(parseResponse)
|
package/src/parser.js
CHANGED
|
@@ -41,11 +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.channel = p.channel || channel.id
|
|
48
|
-
|
|
49
|
-
return new Program(p)
|
|
50
|
-
})
|
|
44
|
+
return programs.filter(i => i).map(p => new Program(p, channel))
|
|
51
45
|
}
|
package/src/utils.js
CHANGED
|
@@ -12,11 +12,16 @@ module.exports.parseNumber = parseNumber
|
|
|
12
12
|
module.exports.formatDate = formatDate
|
|
13
13
|
module.exports.toArray = toArray
|
|
14
14
|
module.exports.toUnix = toUnix
|
|
15
|
+
module.exports.isDate = isDate
|
|
15
16
|
|
|
16
17
|
function sleep(ms) {
|
|
17
18
|
return new Promise(resolve => setTimeout(resolve, ms))
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
function isDate(d) {
|
|
22
|
+
return dayjs(d).isValid()
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
function isObject(a) {
|
|
21
26
|
return !!a && a.constructor === Object
|
|
22
27
|
}
|
package/src/xmltv.js
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
const
|
|
1
|
+
const Channel = require('./Channel')
|
|
2
|
+
const Program = require('./Program')
|
|
3
|
+
const { escapeString, getUTCDate, formatDate, isDate } = require('./utils')
|
|
2
4
|
const el = createElement
|
|
3
5
|
|
|
4
6
|
module.exports.generate = generate
|
|
5
7
|
|
|
6
8
|
function generate({ channels, programs, date = getUTCDate() }) {
|
|
9
|
+
if (!channels.every(c => c instanceof Channel)) {
|
|
10
|
+
throw new Error('"channels" must be an array of Channels')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!programs.every(p => p instanceof Program)) {
|
|
14
|
+
throw new Error('"programs" must be an array of Programs')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!isDate(date)) {
|
|
18
|
+
throw new Error('"date" must be a valid date')
|
|
19
|
+
}
|
|
20
|
+
|
|
7
21
|
let output = `<?xml version="1.0" encoding="UTF-8" ?>`
|
|
8
22
|
output += createElements(channels, programs, date)
|
|
9
23
|
|
|
@@ -33,9 +47,15 @@ function createElements(channels, programs, date) {
|
|
|
33
47
|
channel: program.channel
|
|
34
48
|
},
|
|
35
49
|
[
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
+
),
|
|
39
59
|
el('credits', {}, [
|
|
40
60
|
...program.directors.map(data => createCastMember('director', data)),
|
|
41
61
|
...program.actors.map(data => createCastMember('actor', data)),
|
|
@@ -49,7 +69,9 @@ function createElements(channels, programs, date) {
|
|
|
49
69
|
...program.guests.map(data => createCastMember('guest', data))
|
|
50
70
|
]),
|
|
51
71
|
el('date', {}, [formatDate(program.date, 'YYYYMMDD')]),
|
|
52
|
-
...program.categories.map(category =>
|
|
72
|
+
...program.categories.map(category =>
|
|
73
|
+
el('category', { lang: category.lang }, [escapeString(category.value)])
|
|
74
|
+
),
|
|
53
75
|
el('icon', { src: program.icon.src }),
|
|
54
76
|
...program.urls.map(createURL),
|
|
55
77
|
...program.episodeNumbers.map(episode =>
|
package/tests/Program.test.js
CHANGED
|
@@ -1,59 +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: '
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
+
]
|
|
40
48
|
},
|
|
41
|
-
|
|
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
|
+
channel
|
|
50
|
+
)
|
|
49
51
|
|
|
50
52
|
expect(program).toMatchObject({
|
|
53
|
+
site: 'example.com',
|
|
51
54
|
channel: '1tv',
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
titles: [{ value: 'Title', lang: 'fr' }],
|
|
56
|
+
sub_titles: [{ value: 'Subtitle', lang: 'fr' }],
|
|
57
|
+
descriptions: [{ value: 'Description', lang: 'fr' }],
|
|
55
58
|
urls: [{ system: '', value: 'http://example.com/title.html' }],
|
|
56
|
-
categories: [
|
|
59
|
+
categories: [
|
|
60
|
+
{ value: 'Category1', lang: 'fr' },
|
|
61
|
+
{ value: 'Category2', lang: 'fr' }
|
|
62
|
+
],
|
|
57
63
|
icon: { src: 'https://example.com/image.jpg' },
|
|
58
64
|
episodeNumbers: [
|
|
59
65
|
{ system: 'xmltv_ns', value: '8.237.0/1' },
|
|
@@ -130,24 +136,26 @@ it('can create new Program', () => {
|
|
|
130
136
|
})
|
|
131
137
|
|
|
132
138
|
it('can create program from exist object', () => {
|
|
133
|
-
const program = new Program(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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: [] }]
|
|
142
150
|
},
|
|
143
|
-
|
|
144
|
-
|
|
151
|
+
channel
|
|
152
|
+
)
|
|
145
153
|
|
|
146
154
|
expect(program).toMatchObject({
|
|
147
155
|
channel: '1tv',
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
156
|
+
titles: [{ value: 'Program 1', lang: 'de' }],
|
|
157
|
+
sub_titles: [],
|
|
158
|
+
descriptions: [],
|
|
151
159
|
urls: [],
|
|
152
160
|
categories: [],
|
|
153
161
|
icon: {},
|
|
@@ -176,13 +184,15 @@ it('can create program from exist object', () => {
|
|
|
176
184
|
})
|
|
177
185
|
|
|
178
186
|
it('can create program without season number', () => {
|
|
179
|
-
const program = new Program(
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
+
)
|
|
186
196
|
|
|
187
197
|
expect(program.episodeNumbers).toMatchObject([
|
|
188
198
|
{ system: 'xmltv_ns', value: '0.237.0/1' },
|
|
@@ -191,13 +201,16 @@ it('can create program without season number', () => {
|
|
|
191
201
|
})
|
|
192
202
|
|
|
193
203
|
it('can create program without episode number', () => {
|
|
194
|
-
const program = new Program(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
+
)
|
|
201
214
|
|
|
202
215
|
expect(program.episodeNumbers).toMatchObject([])
|
|
203
216
|
})
|
package/tests/parser.test.js
CHANGED
|
@@ -14,7 +14,7 @@ it('can parse valid channels.xml', () => {
|
|
|
14
14
|
})
|
|
15
15
|
|
|
16
16
|
it('can parse programs', done => {
|
|
17
|
-
const channel = { xmltv_id: '1tv' }
|
|
17
|
+
const channel = new Channel({ xmltv_id: '1tv' })
|
|
18
18
|
const config = require('./input/example.config.js')
|
|
19
19
|
|
|
20
20
|
parsePrograms({ channel, config })
|
|
@@ -27,7 +27,7 @@ it('can parse programs', done => {
|
|
|
27
27
|
})
|
|
28
28
|
|
|
29
29
|
it('can parse programs async', done => {
|
|
30
|
-
const channel = { xmltv_id: '1tv' }
|
|
30
|
+
const channel = new Channel({ xmltv_id: '1tv' })
|
|
31
31
|
const config = require('./input/async.config.js')
|
|
32
32
|
|
|
33
33
|
parsePrograms({ channel, config })
|
package/tests/xmltv.test.js
CHANGED
|
@@ -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
|
-
|
|
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 & 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>'
|
|
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 & 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<programme start="20210319060000 +0000" stop="20210319063000 +0000" channel="2TV.co"><title lang="es">Program 2</title></programme>\r\n</tv>'
|
|
70
78
|
)
|
|
71
79
|
})
|