epg-grabber 0.28.5 → 0.29.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/package.json +1 -1
- package/src/Program.js +25 -7
- package/src/client.js +1 -2
- package/src/index.js +6 -1
- package/src/parser.js +1 -8
- package/src/xmltv.js +12 -4
- package/tests/Program.test.js +86 -75
- package/tests/index.test.js +5 -5
- package/tests/parser.test.js +2 -2
- package/tests/xmltv.test.js +12 -4
package/package.json
CHANGED
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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/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,9 +1,9 @@
|
|
|
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 { sleep, isPromise } = require('./utils')
|
|
7
7
|
const Channel = require('./Channel')
|
|
8
8
|
const Program = require('./Program')
|
|
9
9
|
|
|
@@ -27,8 +27,13 @@ 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
|
|
|
36
|
+
date = typeof date === 'string' ? getUTCDate(date) : date
|
|
32
37
|
return buildRequest({ channel, date, config: this.config })
|
|
33
38
|
.then(this.client)
|
|
34
39
|
.then(parseResponse)
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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 =>
|
|
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 =>
|
package/tests/Program.test.js
CHANGED
|
@@ -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: '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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: [
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
146
|
-
|
|
151
|
+
channel
|
|
152
|
+
)
|
|
147
153
|
|
|
148
154
|
expect(program).toMatchObject({
|
|
149
155
|
channel: '1tv',
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
})
|
package/tests/index.test.js
CHANGED
|
@@ -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) => {
|
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
|
})
|