epg-grabber 0.31.0 → 0.33.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/README.md CHANGED
@@ -43,11 +43,9 @@ module.exports = {
43
43
 
44
44
  ```xml
45
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>
46
+ <channels site="example.com">
47
+ <channel site_id="cnn-23" xmltv_id="CNN.us">CNN</channel>
48
+ </channels>
51
49
  ```
52
50
 
53
51
  ## Example Output
@@ -75,7 +73,7 @@ Arguments:
75
73
 
76
74
  - `-c, --config`: path to config file
77
75
  - `-o, --output`: path to output file or path template (example: `guides/{site}.{lang}.xml`; default: `guide.xml`)
78
- - `--channels`: path to list of channels
76
+ - `--channels`: path to list of channels; you can also use wildcard to specify the path to multiple files at once (example: `example.com_*.channels.xml`)
79
77
  - `--lang`: set default language for all programs (default: `en`)
80
78
  - `--days`: number of days for which to grab the program (default: `1`)
81
79
  - `--delay`: delay between requests in milliseconds (default: `3000`)
@@ -94,7 +92,7 @@ Arguments:
94
92
  module.exports = {
95
93
  site: 'example.com', // site domain name (required)
96
94
  output: 'example.com.guide.xml', // path to output file or path template (example: 'guides/{site}.{lang}.xml'; default: 'guide.xml')
97
- channels: 'example.com.channels.xml', // path to channels.xml file (required)
95
+ channels: 'example.com.channels.xml', // path to list of channels; you can also use an array to specify the path to multiple files at once (example: ['channels1.xml', 'channels2.xml']; required)
98
96
  lang: 'fr', // default language for all programs (default: 'en')
99
97
  days: 3, // number of days for which to grab the program (default: 1)
100
98
  delay: 5000, // delay between requests (default: 3000)
@@ -218,18 +216,17 @@ From each function in `config.js` you can access a `context` object containing t
218
216
 
219
217
  ```xml
220
218
  <?xml version="1.0" ?>
221
- <site site="example.com">
222
- <channels>
223
- <channel site_id="cnn-23" xmltv_id="CNN.us">CNN</channel>
224
- ...
225
- </channels>
226
- </site>
219
+ <channels site="example.com">
220
+ <channel site_id="cnn-23" xmltv_id="CNN.us">CNN</channel>
221
+ ...
222
+ </channels>
227
223
  ```
228
224
 
229
- You can also specify the language and logo for each channel individually, like so:
225
+ You can also specify the language, site and logo for each channel individually, like so:
230
226
 
231
227
  ```xml
232
228
  <channel
229
+ site="example.com"
233
230
  site_id="france-24"
234
231
  xmltv_id="France24.fr"
235
232
  lang="fr"
@@ -22,7 +22,7 @@ program
22
22
  .description(description)
23
23
  .requiredOption('-c, --config <config>', 'Path to [site].config.js file')
24
24
  .option('-o, --output <output>', 'Path to output file')
25
- .option('--channels <channels>', 'Path to channels.xml file')
25
+ .option('--channels <channels>', 'Path to list of channels')
26
26
  .option('--lang <lang>', 'Set default language for all programs')
27
27
  .option('--days <days>', 'Number of days for which to grab the program', parseNumber)
28
28
  .option('--delay <delay>', 'Delay between requests (in milliseconds)', parseNumber)
@@ -65,17 +65,31 @@ async function main() {
65
65
 
66
66
  if (options.timeout) config.request.timeout = options.timeout
67
67
  if (options.cacheTtl) config.request.cache.ttl = options.cacheTtl
68
+
68
69
  if (options.channels) config.channels = options.channels
69
- else if (config.channels)
70
- config.channels = file.join(file.dirname(options.config), config.channels)
71
- else throw new Error("The required 'channels' property is missing")
72
70
 
73
- if (!config.channels) return logger.error('Path to [site].channels.xml is missing')
74
- logger.info(`Loading '${config.channels}'...`)
75
- const grabber = new EPGGrabber(config)
71
+ let parsedChannels = []
72
+ if (config.channels) {
73
+ const dir = file.dirname(options.config)
74
+
75
+ let files = []
76
+ if (Array.isArray(config.channels)) {
77
+ files = config.channels.map(path => file.join(dir, path))
78
+ } else if (typeof config.channels === 'string') {
79
+ files = await file.list(config.channels)
80
+ } else {
81
+ throw new Error('The "channels" attribute must be of type array or string')
82
+ }
83
+
84
+ for (let filepath of files) {
85
+ logger.info(`Loading '${filepath}'...`)
86
+ const channelsXML = file.read(filepath)
87
+ const channels = parseChannels(channelsXML)
88
+ parsedChannels = parsedChannels.concat(channels)
89
+ }
90
+ } else throw new Error('Path to "channels" is missing')
76
91
 
77
- const channelsXML = file.read(config.channels)
78
- const { channels: parsedChannels } = parseChannels(channelsXML)
92
+ const grabber = new EPGGrabber(config)
79
93
 
80
94
  let template = options.output || config.output
81
95
  const variables = file.templateVariables(template)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "epg-grabber",
3
- "version": "0.31.0",
3
+ "version": "0.33.0",
4
4
  "description": "Node.js CLI tool for grabbing EPG from different sites",
5
5
  "main": "src/index.js",
6
6
  "preferGlobal": true,
@@ -8,7 +8,7 @@
8
8
  "epg-grabber": "bin/epg-grabber.js"
9
9
  },
10
10
  "scripts": {
11
- "lint": "npx eslint ./src/**/*.js",
11
+ "lint": "npx eslint ./src/**/*.js ./tests/**/*.js",
12
12
  "test": "npx jest"
13
13
  },
14
14
  "publishConfig": {
package/src/client.js CHANGED
@@ -15,6 +15,7 @@ let timeout
15
15
  function create(config) {
16
16
  const client = setupCache(
17
17
  axios.create({
18
+ ignoreCookieErrors: true,
18
19
  headers: {
19
20
  'User-Agent':
20
21
  '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'
package/src/file.js CHANGED
@@ -1,6 +1,8 @@
1
1
  const fs = require('fs')
2
2
  const path = require('path')
3
+ const glob = require('glob')
3
4
 
5
+ module.exports.list = list
4
6
  module.exports.read = read
5
7
  module.exports.write = write
6
8
  module.exports.resolve = resolve
@@ -9,6 +11,14 @@ module.exports.dirname = dirname
9
11
  module.exports.templateVariables = templateVariables
10
12
  module.exports.templateFormat = templateFormat
11
13
 
14
+ function list(pattern) {
15
+ return new Promise(resolve => {
16
+ glob(pattern, function (err, files) {
17
+ resolve(files)
18
+ })
19
+ })
20
+ }
21
+
12
22
  function read(filepath) {
13
23
  return fs.readFileSync(path.resolve(filepath), { encoding: 'utf-8' })
14
24
  }
package/src/index.d.ts ADDED
@@ -0,0 +1,110 @@
1
+ import dayjs from 'dayjs'
2
+
3
+ export declare class Channel {
4
+ xmltv_id?: string
5
+ name: string
6
+ site?: string
7
+ site_id: string
8
+ lang?: string
9
+ logo?: string
10
+ }
11
+
12
+ type TextObject = {
13
+ lang: string
14
+ value: string
15
+ }
16
+
17
+ type IconObject = {
18
+ src: string
19
+ }
20
+
21
+ type UrlObject = {
22
+ system: string
23
+ value: string
24
+ }
25
+
26
+ type RatingObject = {
27
+ system: string
28
+ icon: string
29
+ value: string
30
+ }
31
+
32
+ type ImageObject = {
33
+ type: string
34
+ size: string
35
+ orient: string
36
+ system: string
37
+ value: string
38
+ }
39
+
40
+ type PersonObject = {
41
+ value: string
42
+ url: UrlObject[]
43
+ image: ImageObject[]
44
+ }
45
+
46
+ export declare class Program {
47
+ site?: string
48
+ channel?: string
49
+ titles: TextObject[]
50
+ sub_titles?: TextObject[]
51
+ descriptions?: TextObject[]
52
+ icon?: IconObject
53
+ episodeNumbers?: string[]
54
+ date?: number
55
+ start: number
56
+ stop?: number
57
+ urls?: UrlObject[]
58
+ ratings?: RatingObject[]
59
+ categories?: TextObject[]
60
+ directors?: PersonObject[]
61
+ actors?: PersonObject[]
62
+ writers?: PersonObject[]
63
+ adapters?: PersonObject[]
64
+ producers?: PersonObject[]
65
+ composers?: PersonObject[]
66
+ editors?: PersonObject[]
67
+ presenters?: PersonObject[]
68
+ commentators?: PersonObject[]
69
+ guests?: PersonObject[]
70
+ }
71
+
72
+ export declare type SiteConfig = {
73
+ site: string
74
+ days?: number
75
+ output?: string
76
+ channels?: () => object[] | string | Promise<object[]>
77
+ delay?: number
78
+ maxConnections?: number
79
+ request?: object
80
+ url: () => string | string | Promise<string>
81
+ logo?: () => string | string | Promise<string>
82
+ parser: () => object[] | Promise<object[]>
83
+ }
84
+
85
+ export declare function generateXMLTV({
86
+ channels,
87
+ programs,
88
+ date
89
+ }: {
90
+ channels: Channel[]
91
+ programs: Program[]
92
+ date?: string | null
93
+ }): string
94
+
95
+ export declare function parseChannels(xml: string): { site: string; channels: Channel[] }
96
+
97
+ export type GrabCallbackData = {
98
+ channel: Channel
99
+ programs: Program[]
100
+ date: dayjs.Dayjs
101
+ }
102
+
103
+ export declare class EPGGrabber {
104
+ constructor(config: object)
105
+ grab(
106
+ channel: Channel,
107
+ date: string | dayjs.Dayjs,
108
+ cb: (data: GrabCallbackData, err: Error) => void
109
+ ): Promise<Program[]>
110
+ }
package/src/parser.js CHANGED
@@ -9,11 +9,19 @@ module.exports.parsePrograms = parsePrograms
9
9
  function parseChannels(xml) {
10
10
  const result = convert.xml2js(xml)
11
11
  const siteTag = result.elements.find(el => el.name === 'site') || {}
12
- if (!siteTag.elements) return []
13
- const rootSite = siteTag.attributes.site
14
12
 
15
- const channelsTag = siteTag.elements.find(el => el.name === 'channels')
16
- if (!channelsTag.elements) return []
13
+ const channelsTag =
14
+ siteTag && Array.isArray(siteTag.elements)
15
+ ? siteTag.elements.find(el => el.name === 'channels')
16
+ : result.elements.find(el => el.name === 'channels')
17
+ if (!channelsTag || !channelsTag.elements) return []
18
+
19
+ let rootSite = ''
20
+ if (siteTag && siteTag.attributes && siteTag.attributes.site) {
21
+ rootSite = siteTag.attributes.site
22
+ } else if (channelsTag && channelsTag.attributes && channelsTag.attributes.site) {
23
+ rootSite = channelsTag.attributes.site
24
+ }
17
25
 
18
26
  const channels = channelsTag.elements
19
27
  .filter(el => el.name === 'channel')
@@ -26,7 +34,7 @@ function parseChannels(xml) {
26
34
  return new Channel(c)
27
35
  })
28
36
 
29
- return { site: rootSite, channels }
37
+ return channels
30
38
  }
31
39
 
32
40
  async function parsePrograms(data) {
@@ -1,7 +1,5 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
- <site site="example.com">
3
- <channels>
4
- <channel xmltv_id="1TV.com" site_id="1" lang="fr" logo="https://example.com/logos/1TV.png">1 TV</channel>
5
- <channel xmltv_id="2TV.com" site="example.com" site_id="2">2 TV</channel>
6
- </channels>
7
- </site>
2
+ <channels site="example.com">
3
+ <channel xmltv_id="1TV.com" site_id="1" lang="fr" logo="https://example.com/logos/1TV.png">1 TV</channel>
4
+ <channel xmltv_id="2TV.com" site_id="2">2 TV</channel>
5
+ </channels>
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <site site="example2.com">
3
+ <channels>
4
+ <channel xmltv_id="3TV.com" site_id="3">3 TV</channel>
5
+ <channel xmltv_id="4TV.com" site_id="4">4 TV</channel>
6
+ </channels>
7
+ </site>
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <channels>
3
+ <channel site="example.com" xmltv_id="1TV.com" site_id="1" lang="fr" logo="https://example.com/logos/1TV.png">1 TV</channel>
4
+ <channel site="example.com" xmltv_id="2TV.com" site_id="2">2 TV</channel>
5
+ </channels>
@@ -0,0 +1,32 @@
1
+ const dayjs = require('dayjs')
2
+ const utc = require('dayjs/plugin/utc')
3
+
4
+ dayjs.extend(utc)
5
+
6
+ module.exports = {
7
+ site: 'example.com',
8
+ days: 2,
9
+ channels: ['example.channels.xml', 'example_2.channels.xml'],
10
+ output: 'tests/__data__/output/guide.xml',
11
+ url: () => 'http://example.com/20210319/1tv.json',
12
+ request: {
13
+ method: 'POST',
14
+ headers: {
15
+ 'Content-Type': 'application/json',
16
+ Cookie: 'abc=123'
17
+ },
18
+ data() {
19
+ return { accountID: '123' }
20
+ }
21
+ },
22
+ parser: () => {
23
+ return [
24
+ {
25
+ title: 'Program1',
26
+ start: 1640995200000,
27
+ stop: 1640998800000
28
+ }
29
+ ]
30
+ },
31
+ logo: () => 'http://example.com/logos/1TV.png?x=шеллы&sid=777'
32
+ }
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <site site="example.com">
3
+ <channels>
4
+ <channel xmltv_id="1TV.com" site_id="1" lang="fr" logo="https://example.com/logos/1TV.png">1 TV</channel>
5
+ <channel xmltv_id="2TV.com" site_id="2">2 TV</channel>
6
+ </channels>
7
+ </site>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8" ?><tv date="20230927">
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><icon src="http://example.com/logos/1TV.png?x=шеллы&amp;sid=777"/><url>https://example.com</url></channel>
4
+ <channel id="3TV.com"><display-name>3 TV</display-name><icon src="http://example.com/logos/1TV.png?x=шеллы&amp;sid=777"/><url>https://example2.com</url></channel>
5
+ <channel id="4TV.com"><display-name>4 TV</display-name><icon src="http://example.com/logos/1TV.png?x=шеллы&amp;sid=777"/><url>https://example2.com</url></channel>
6
+ </tv>
@@ -1,2 +1,3 @@
1
- import mockAxios from 'jest-mock-axios'
2
- export default mockAxios
1
+ const mockAxios = require('jest-mock-axios')
2
+
3
+ module.exports = mockAxios
package/tests/bin.test.js CHANGED
@@ -87,7 +87,7 @@ it('can produce multiple outputs', () => {
87
87
  })
88
88
 
89
89
  it('removes duplicates of the program', () => {
90
- const stdout = execSync(
90
+ execSync(
91
91
  `node ${pwd}/bin/epg-grabber.js \
92
92
  --config=tests/__data__/input/duplicates.config.js \
93
93
  --channels=tests/__data__/input/example.channels.xml \
@@ -105,3 +105,25 @@ it('removes duplicates of the program', () => {
105
105
 
106
106
  expect(output.programs).toEqual(expected.programs)
107
107
  })
108
+
109
+ it('can load multiple "channels.xml" files at once', () => {
110
+ const stdout = execSync(
111
+ `node ${pwd}/bin/epg-grabber.js --config=tests/__data__/input/example.config.js --channels=tests/__data__/input/example*.channels.xml --timeout=1`,
112
+ {
113
+ encoding: 'utf8'
114
+ }
115
+ )
116
+
117
+ expect(stdoutResultTester(stdout)).toBe(true)
118
+ })
119
+
120
+ it('can parse list of "channels.xml" from array', () => {
121
+ const stdout = execSync(
122
+ `node ${pwd}/bin/epg-grabber.js --config=tests/__data__/input/example_channels.config.js --timeout=1`,
123
+ {
124
+ encoding: 'utf8'
125
+ }
126
+ )
127
+
128
+ expect(stdoutResultTester(stdout)).toBe(true)
129
+ })
@@ -3,14 +3,83 @@ import Channel from '../src/Channel'
3
3
  import Program from '../src/Program'
4
4
  import fs from 'fs'
5
5
 
6
- it('can parse valid channels.xml', () => {
7
- const file = fs.readFileSync('./tests/__data__/input/example.channels.xml', { encoding: 'utf-8' })
8
- const { channels, site } = parseChannels(file)
6
+ it('can parse channels.xml', () => {
7
+ const file = fs.readFileSync('./tests/__data__/input/example.channels.xml', {
8
+ encoding: 'utf-8'
9
+ })
10
+ const channels = parseChannels(file)
9
11
 
10
- expect(typeof site).toBe('string')
11
12
  expect(channels.length).toBe(2)
12
13
  expect(channels[0]).toBeInstanceOf(Channel)
13
14
  expect(channels[1]).toBeInstanceOf(Channel)
15
+ expect(channels[0]).toMatchObject({
16
+ site: 'example.com',
17
+ site_id: '1',
18
+ xmltv_id: '1TV.com',
19
+ lang: 'fr',
20
+ logo: 'https://example.com/logos/1TV.png',
21
+ name: '1 TV'
22
+ })
23
+ expect(channels[1]).toMatchObject({
24
+ site: 'example.com',
25
+ site_id: '2',
26
+ lang: '',
27
+ logo: '',
28
+ xmltv_id: '2TV.com',
29
+ name: '2 TV'
30
+ })
31
+ })
32
+
33
+ it('can parse channels.xml with inline site attribute', () => {
34
+ const file = fs.readFileSync('./tests/__data__/input/example_3.channels.xml', {
35
+ encoding: 'utf-8'
36
+ })
37
+ const channels = parseChannels(file)
38
+
39
+ expect(channels.length).toBe(2)
40
+ expect(channels[0]).toBeInstanceOf(Channel)
41
+ expect(channels[1]).toBeInstanceOf(Channel)
42
+ expect(channels[0]).toMatchObject({
43
+ site: 'example.com',
44
+ site_id: '1',
45
+ xmltv_id: '1TV.com',
46
+ lang: 'fr',
47
+ logo: 'https://example.com/logos/1TV.png',
48
+ name: '1 TV'
49
+ })
50
+ expect(channels[1]).toMatchObject({
51
+ site: 'example.com',
52
+ site_id: '2',
53
+ lang: '',
54
+ logo: '',
55
+ xmltv_id: '2TV.com',
56
+ name: '2 TV'
57
+ })
58
+ })
59
+
60
+ it('can parse legacy channels.xml', () => {
61
+ const file = fs.readFileSync('./tests/__data__/input/legacy.channels.xml', { encoding: 'utf-8' })
62
+ const channels = parseChannels(file)
63
+
64
+ expect(channels.length).toBe(2)
65
+ expect(channels[0]).toBeInstanceOf(Channel)
66
+ expect(channels[1]).toBeInstanceOf(Channel)
67
+ expect(channels[0]).toMatchObject({
68
+ site: 'example.com',
69
+ site_id: '1',
70
+ xmltv_id: '1TV.com',
71
+ lang: 'fr',
72
+ logo: 'https://example.com/logos/1TV.png',
73
+ name: '1 TV'
74
+ })
75
+ expect(channels[1]).toMatchObject({
76
+ site: 'example.com',
77
+ site_id: '2',
78
+ lang: '',
79
+ logo: '',
80
+ xmltv_id: '2TV.com',
81
+ name: '2 TV'
82
+ })
14
83
  })
15
84
 
16
85
  it('can parse programs', done => {
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8" ?><tv date="20230521">
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>