epg-grabber 0.27.0 → 0.28.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
@@ -166,12 +166,24 @@ module.exports = {
166
166
  title, // program title (required)
167
167
  start, // start time of the program (required)
168
168
  stop, // end time of the program (required)
169
+ sub_title, // program sub-title (optional)
169
170
  description, // description of the program (optional)
170
- category, // program type (optional)
171
+ category, // type of program (optional)
171
172
  season, // season number (optional)
172
173
  episode, // episode number (optional)
174
+ date, // the date the programme or film was finished (optional)
173
175
  icon, // image associated with the program (optional)
174
- lang // language of the description (default: 'en')
176
+ rating, // program rating (optional)
177
+ director, // the name of director (optional)
178
+ actor, // the name of actor (optional)
179
+ writer, // the name of writer (optional)
180
+ adapter, // the name of adapter (optional)
181
+ producer, // the name of producer (optional)
182
+ composer, // the name of composer (optional)
183
+ editor, // the name of editor (optional)
184
+ presenter, // the name of presenter (optional)
185
+ commentator, // the name of commentator (optional)
186
+ guest // the name of guest (optional)
175
187
  },
176
188
  ...
177
189
  ]
@@ -2,15 +2,13 @@
2
2
 
3
3
  const { Command } = require('commander')
4
4
  const program = new Command()
5
- const fs = require('fs')
6
- const path = require('path')
7
- const EPGGrabber = require('../src/index')
8
- const utils = require('../src/utils')
9
- const { name, version, description } = require('../package.json')
10
5
  const { merge } = require('lodash')
11
6
  const { gzip } = require('node-gzip')
12
- const { createLogger, format, transports } = require('winston')
13
- const { combine, timestamp, printf } = format
7
+ const file = require('../src/file')
8
+ const EPGGrabber = require('../src/index')
9
+ const { create: createLogger } = require('../src/logger')
10
+ const { parseInteger, getUTCDate } = require('../src/utils')
11
+ const { name, version, description } = require('../package.json')
14
12
 
15
13
  program
16
14
  .name(name)
@@ -36,39 +34,13 @@ program
36
34
  .parse(process.argv)
37
35
 
38
36
  const options = program.opts()
39
-
40
- const fileFormat = printf(({ level, message, timestamp }) => {
41
- return `[${timestamp}] ${level.toUpperCase()}: ${message}`
42
- })
43
-
44
- const consoleFormat = printf(({ level, message, timestamp }) => {
45
- if (level === 'error') return ` Error: ${message}`
46
-
47
- return message
48
- })
49
-
50
- const t = [new transports.Console({ format: consoleFormat })]
51
-
52
- if (options.log) {
53
- t.push(
54
- new transports.File({
55
- filename: path.resolve(options.log),
56
- format: combine(timestamp(), fileFormat),
57
- options: { flags: 'w' }
58
- })
59
- )
60
- }
61
-
62
- const logger = createLogger({
63
- level: options.logLevel,
64
- transports: t
65
- })
37
+ const logger = createLogger(options)
66
38
 
67
39
  async function main() {
68
40
  logger.info('Starting...')
69
41
 
70
42
  logger.info(`Loading '${options.config}'...`)
71
- let config = require(path.resolve(options.config))
43
+ let config = require(file.resolve(options.config))
72
44
  config = merge(config, {
73
45
  days: options.days,
74
46
  debug: options.debug,
@@ -83,22 +55,27 @@ async function main() {
83
55
  if (options.cacheTtl) config.request.cache.ttl = options.cacheTtl
84
56
  if (options.channels) config.channels = options.channels
85
57
  else if (config.channels)
86
- config.channels = path.join(path.dirname(options.config), config.channels)
58
+ config.channels = file.join(file.dirname(options.config), config.channels)
87
59
  else throw new Error("The required 'channels' property is missing")
88
60
 
89
61
  if (!config.channels) return logger.error('Path to [site].channels.xml is missing')
90
62
  logger.info(`Loading '${config.channels}'...`)
91
- const channelsXML = fs.readFileSync(path.resolve(config.channels), { encoding: 'utf-8' })
92
- const { channels } = utils.parseChannels(channelsXML)
63
+ const grabber = new EPGGrabber(config)
64
+
65
+ const channelsXML = file.read(config.channels)
66
+ const { channels } = grabber.parseChannels(channelsXML)
93
67
 
94
68
  let programs = []
95
69
  let i = 1
96
70
  let days = options.days || 1
97
71
  const total = channels.length * days
98
- const utcDate = utils.getUTCDate()
72
+ const utcDate = getUTCDate()
99
73
  const dates = Array.from({ length: config.days }, (_, i) => utcDate.add(i, 'd'))
100
- const grabber = new EPGGrabber(config)
101
74
  for (let channel of channels) {
75
+ if (!channel.logo && config.logo) {
76
+ channel.logo = await grabber.loadLogo(channel)
77
+ }
78
+
102
79
  for (let date of dates) {
103
80
  await grabber
104
81
  .grab(channel, date, (data, err) => {
@@ -118,15 +95,15 @@ async function main() {
118
95
  }
119
96
  }
120
97
 
121
- const xml = utils.convertToXMLTV({ config, channels, programs })
98
+ const xml = grabber.generateXMLTV({ channels, programs })
122
99
  let outputPath = options.output || config.output
123
100
  if (options.gzip) {
124
101
  outputPath = outputPath || 'guide.xml.gz'
125
102
  const compressed = await gzip(xml)
126
- utils.writeToFile(outputPath, compressed)
103
+ file.write(outputPath, compressed)
127
104
  } else {
128
105
  outputPath = outputPath || 'guide.xml'
129
- utils.writeToFile(outputPath, xml)
106
+ file.write(outputPath, xml)
130
107
  }
131
108
 
132
109
  logger.info(`File '${outputPath}' successfully saved`)
@@ -134,7 +111,3 @@ async function main() {
134
111
  }
135
112
 
136
113
  main()
137
-
138
- function parseInteger(val) {
139
- return val ? parseInt(val) : null
140
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "epg-grabber",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Node.js CLI tool for grabbing EPG from different sites",
5
5
  "main": "src/index.js",
6
6
  "preferGlobal": true,
@@ -0,0 +1,26 @@
1
+ const convert = require('xml-js')
2
+
3
+ module.exports.parse = parse
4
+
5
+ function parse(xml) {
6
+ const result = convert.xml2js(xml)
7
+ const siteTag = result.elements.find(el => el.name === 'site') || {}
8
+ if (!siteTag.elements) return []
9
+ const site = siteTag.attributes.site
10
+
11
+ const channelsTag = siteTag.elements.find(el => el.name === 'channels')
12
+ if (!channelsTag.elements) return []
13
+
14
+ const channels = channelsTag.elements
15
+ .filter(el => el.name === 'channel')
16
+ .map(el => {
17
+ const channel = el.attributes
18
+ if (!el.elements) throw new Error(`Channel '${channel.xmltv_id}' has no valid name`)
19
+ channel.name = el.elements.find(el => el.type === 'text').text
20
+ channel.site = channel.site || site
21
+
22
+ return channel
23
+ })
24
+
25
+ return { site, channels }
26
+ }
package/src/client.js ADDED
@@ -0,0 +1,138 @@
1
+ const { CurlGenerator } = require('curl-generator')
2
+ const axios = require('axios').default
3
+ const axiosCookieJarSupport = require('axios-cookiejar-support').default
4
+ const { setupCache } = require('axios-cache-interceptor')
5
+ const { isObject, isPromise, getUTCDate } = require('./utils')
6
+
7
+ axiosCookieJarSupport(axios)
8
+
9
+ module.exports.create = create
10
+ module.exports.buildRequest = buildRequest
11
+ module.exports.parseResponse = parseResponse
12
+
13
+ let timeout
14
+
15
+ function create(config) {
16
+ const client = setupCache(
17
+ axios.create({
18
+ headers: {
19
+ 'User-Agent':
20
+ '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'
21
+ }
22
+ })
23
+ )
24
+
25
+ client.interceptors.request.use(
26
+ function (request) {
27
+ if (config.debug) {
28
+ console.log('Request:', JSON.stringify(request, null, 2))
29
+ }
30
+ return request
31
+ },
32
+ function (error) {
33
+ return Promise.reject(error)
34
+ }
35
+ )
36
+
37
+ client.interceptors.response.use(
38
+ function (response) {
39
+ if (config.debug) {
40
+ const data =
41
+ isObject(response.data) || Array.isArray(response.data)
42
+ ? JSON.stringify(response.data)
43
+ : response.data.toString()
44
+ console.log(
45
+ 'Response:',
46
+ JSON.stringify(
47
+ {
48
+ headers: response.headers,
49
+ data,
50
+ cached: response.cached
51
+ },
52
+ null,
53
+ 2
54
+ )
55
+ )
56
+ }
57
+
58
+ clearTimeout(timeout)
59
+ return response
60
+ },
61
+ function (error) {
62
+ clearTimeout(timeout)
63
+ return Promise.reject(error)
64
+ }
65
+ )
66
+
67
+ return client
68
+ }
69
+
70
+ async function buildRequest({ channel, date, config }) {
71
+ date = typeof date === 'string' ? getUTCDate(date) : date
72
+ const CancelToken = axios.CancelToken
73
+ const source = CancelToken.source()
74
+ const request = { ...config.request }
75
+ timeout = setTimeout(() => {
76
+ source.cancel('Connection timeout')
77
+ }, request.timeout)
78
+ request.headers = await getRequestHeaders({ channel, date, config })
79
+ request.url = await getRequestUrl({ channel, date, config })
80
+ request.data = await getRequestData({ channel, date, config })
81
+ request.cancelToken = source.token
82
+
83
+ if (config.curl) {
84
+ const curl = CurlGenerator({
85
+ url: request.url,
86
+ method: request.method,
87
+ headers: request.headers,
88
+ body: request.data
89
+ })
90
+ console.log(curl)
91
+ }
92
+
93
+ return request
94
+ }
95
+
96
+ function parseResponse(response) {
97
+ return {
98
+ content: response.data.toString(),
99
+ buffer: response.data,
100
+ headers: response.headers,
101
+ request: response.request,
102
+ cached: response.cached
103
+ }
104
+ }
105
+
106
+ async function getRequestHeaders({ channel, date, config }) {
107
+ if (typeof config.request.headers === 'function') {
108
+ const headers = config.request.headers({ channel, date })
109
+ if (isPromise(headers)) {
110
+ return await headers
111
+ }
112
+ return headers
113
+ }
114
+
115
+ return config.request.headers || null
116
+ }
117
+
118
+ async function getRequestData({ channel, date, config }) {
119
+ if (typeof config.request.data === 'function') {
120
+ const data = config.request.data({ channel, date })
121
+ if (isPromise(data)) {
122
+ return await data
123
+ }
124
+ return data
125
+ }
126
+ return config.request.data || null
127
+ }
128
+
129
+ async function getRequestUrl({ channel, date, config }) {
130
+ if (typeof config.url === 'function') {
131
+ const url = config.url({ channel, date })
132
+ if (isPromise(url)) {
133
+ return await url
134
+ }
135
+ return url
136
+ }
137
+ return config.url
138
+ }
package/src/config.js ADDED
@@ -0,0 +1,34 @@
1
+ const tough = require('tough-cookie')
2
+ const { merge } = require('lodash')
3
+
4
+ module.exports.load = load
5
+
6
+ function load(config) {
7
+ if (!config.site) throw new Error("The required 'site' property is missing")
8
+ if (!config.url) throw new Error("The required 'url' property is missing")
9
+ if (typeof config.url !== 'function' && typeof config.url !== 'string')
10
+ throw new Error("The 'url' property should return the function or string")
11
+ if (!config.parser) throw new Error("The required 'parser' function is missing")
12
+ if (typeof config.parser !== 'function')
13
+ throw new Error("The 'parser' property should return the function")
14
+ if (config.logo && typeof config.logo !== 'function')
15
+ throw new Error("The 'logo' property should return the function")
16
+
17
+ const defaultConfig = {
18
+ days: 1,
19
+ lang: 'en',
20
+ delay: 3000,
21
+ output: 'guide.xml',
22
+ request: {
23
+ method: 'GET',
24
+ maxContentLength: 5 * 1024 * 1024,
25
+ timeout: 5000,
26
+ withCredentials: true,
27
+ jar: new tough.CookieJar(),
28
+ responseType: 'arraybuffer',
29
+ cache: false
30
+ }
31
+ }
32
+
33
+ return merge(defaultConfig, config)
34
+ }
package/src/file.js ADDED
@@ -0,0 +1,33 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+
4
+ module.exports.read = read
5
+ module.exports.write = write
6
+ module.exports.resolve = resolve
7
+ module.exports.join = join
8
+ module.exports.dirname = dirname
9
+
10
+ function read(filepath) {
11
+ return fs.readFileSync(path.resolve(filepath), { encoding: 'utf-8' })
12
+ }
13
+
14
+ function write(filepath, data) {
15
+ const dir = path.resolve(path.dirname(filepath))
16
+ if (!fs.existsSync(dir)) {
17
+ fs.mkdirSync(dir, { recursive: true })
18
+ }
19
+
20
+ fs.writeFileSync(path.resolve(filepath), data)
21
+ }
22
+
23
+ function resolve(filepath) {
24
+ return path.resolve(filepath)
25
+ }
26
+
27
+ function join(path1, path2) {
28
+ return path.join(path1, path2)
29
+ }
30
+
31
+ function dirname(filepath) {
32
+ return path.dirname(filepath)
33
+ }
package/src/index.js CHANGED
@@ -1,41 +1,48 @@
1
- const utils = require('./utils')
1
+ const { merge } = require('lodash')
2
+ const { create: createClient, buildRequest, parseResponse } = require('./client')
3
+ const { generate: generateXMLTV } = require('./xmltv')
4
+ const { parse: parseChannels } = require('./channels')
5
+ const { parse: parsePrograms } = require('./programs')
6
+ const { load: loadConfig } = require('./config')
7
+ const { sleep, isPromise } = require('./utils')
2
8
 
3
9
  class EPGGrabber {
4
10
  constructor(config = {}) {
5
- this.config = utils.loadConfig(config)
6
- this.client = utils.createClient(config)
11
+ this.config = loadConfig(config)
12
+ this.client = createClient(config)
13
+ }
14
+
15
+ async loadLogo(channel) {
16
+ const logo = this.config.logo({ channel })
17
+ if (isPromise(logo)) {
18
+ return await logo
19
+ }
20
+ return logo
7
21
  }
8
22
 
9
23
  async grab(channel, date, cb = () => {}) {
10
- date = typeof date === 'string' ? utils.getUTCDate(date) : date
11
- channel.lang = channel.lang || this.config.lang || null
12
-
13
- let programs = []
14
- const item = { date, channel }
15
- await utils
16
- .buildRequest(item, this.config)
17
- .then(request => utils.fetchData(this.client, request))
18
- .then(response => utils.parseResponse(item, response, this.config))
19
- .then(results => {
20
- item.programs = results
21
- cb(item, null)
22
- programs = programs.concat(results)
23
- })
24
- .catch(error => {
25
- item.programs = []
26
- if (this.config.debug) {
27
- console.log('Error:', JSON.stringify(error, null, 2))
28
- }
29
- cb(item, error)
30
- })
24
+ await sleep(this.config.delay)
31
25
 
32
- await utils.sleep(this.config.delay)
26
+ return buildRequest({ channel, date, config: this.config })
27
+ .then(this.client)
28
+ .then(parseResponse)
29
+ .then(data => merge({ channel, date, config: this.config }, data))
30
+ .then(parsePrograms)
31
+ .then(programs => {
32
+ cb({ channel, date, programs })
33
33
 
34
- return programs
34
+ return programs
35
+ })
36
+ .catch(err => {
37
+ if (this.config.debug) console.log('Error:', JSON.stringify(err, null, 2))
38
+ cb({ channel, date, programs: [] }, err)
39
+
40
+ return []
41
+ })
35
42
  }
36
43
  }
37
44
 
38
- EPGGrabber.convertToXMLTV = utils.convertToXMLTV
39
- EPGGrabber.parseChannels = utils.parseChannels
45
+ EPGGrabber.prototype.generateXMLTV = generateXMLTV
46
+ EPGGrabber.prototype.parseChannels = parseChannels
40
47
 
41
48
  module.exports = EPGGrabber
package/src/logger.js ADDED
@@ -0,0 +1,34 @@
1
+ const { createLogger, format, transports } = require('winston')
2
+ const { combine, timestamp, printf } = format
3
+ const path = require('path')
4
+
5
+ module.exports.create = create
6
+
7
+ function create(options) {
8
+ const fileFormat = printf(({ level, message, timestamp }) => {
9
+ return `[${timestamp}] ${level.toUpperCase()}: ${message}`
10
+ })
11
+
12
+ const consoleFormat = printf(({ level, message }) => {
13
+ if (level === 'error') return ` Error: ${message}`
14
+
15
+ return message
16
+ })
17
+
18
+ const t = [new transports.Console({ format: consoleFormat })]
19
+
20
+ if (options.log) {
21
+ t.push(
22
+ new transports.File({
23
+ filename: path.resolve(options.log),
24
+ format: combine(timestamp(), fileFormat),
25
+ options: { flags: 'w' }
26
+ })
27
+ )
28
+ }
29
+
30
+ return createLogger({
31
+ level: options.logLevel,
32
+ transports: t
33
+ })
34
+ }
@@ -0,0 +1,24 @@
1
+ const { isPromise } = require('./utils')
2
+
3
+ module.exports.parse = parse
4
+
5
+ async function parse(data) {
6
+ const { config, channel } = data
7
+ let programs = config.parser(data)
8
+
9
+ if (isPromise(programs)) {
10
+ programs = await programs
11
+ }
12
+
13
+ if (!Array.isArray(programs)) {
14
+ throw new Error('Parser should return an array')
15
+ }
16
+
17
+ return programs
18
+ .filter(i => i)
19
+ .map(program => {
20
+ program.channel = channel.xmltv_id
21
+
22
+ return program
23
+ })
24
+ }