epg-grabber 0.44.0 → 0.46.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
@@ -75,7 +75,6 @@ Arguments:
75
75
  - `-o, --output`: path to output file or path template (example: `guides/{site}.{lang}.xml`; default: `guide.xml`)
76
76
  - `-x, --proxy`: use the specified proxy (example: `socks5://username:password@127.0.0.1:1234`)
77
77
  - `--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`)
78
- - `--lang`: set default language for all programs (default: `en`)
79
78
  - `--days`: number of days for which to grab the program (default: `1`)
80
79
  - `--delay`: delay between requests in milliseconds (default: `3000`)
81
80
  - `--timeout`: set a timeout for each request in milliseconds (default: `5000`)
@@ -94,7 +93,6 @@ module.exports = {
94
93
  site: 'example.com', // site domain name (required)
95
94
  output: 'example.com.guide.xml', // path to output file or path template (example: 'guides/{site}.{lang}.xml'; default: 'guide.xml')
96
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)
97
- lang: 'fr', // default language for all programs (default: 'en')
98
96
  days: 3, // number of days for which to grab the program (default: 1)
99
97
  delay: 5000, // delay between requests (default: 3000)
100
98
  maxConnections: 200, // limit on the number of concurrent requests (default: 1)
package/dist/cli.js CHANGED
@@ -1,93 +1,182 @@
1
1
  #!/usr/bin/env node
2
- import { n as name, v as version, d as description, p as parseNumber, L as Logger, l as loadJs, a as parseProxy, E as EPGGrabber, g as getUTCDate } from './index-DQUOgwl_.js';
3
- import { Template, Collection } from '@freearhey/core';
2
+ import { n as name, v as version, d as description, p as parseNumber, l as loadJs, a as parseProxy, E as EPGGrabberMock, b as EPGGrabber, c as defaultConfig, i as isObject, g as getAbsPath, e as getUTCDate } from './index-DUJ5SdIU.js';
3
+ import { Collection, Template } from '@freearhey/core';
4
4
  import { Command, Option } from 'commander';
5
5
  import { SocksProxyAgent } from 'socks-proxy-agent';
6
+ import { CurlGenerator } from 'curl-generator';
7
+ import winston from 'winston';
8
+ import path from 'path';
6
9
  import { TaskQueue } from 'cwait';
10
+ import merge from 'lodash.merge';
7
11
  import Promise$1 from 'bluebird';
8
- import path from 'node:path';
12
+ import path$1 from 'node:path';
13
+ import { glob } from 'glob';
9
14
  import fs from 'fs-extra';
10
15
  import pako from 'pako';
11
- import _ from 'lodash';
12
- import 'dayjs';
13
16
  import 'dayjs/plugin/utc.js';
17
+ import 'dayjs';
14
18
  import 'node:url';
15
- import 'glob';
19
+ import 'axios-mock-adapter';
20
+ import 'lodash.padstart';
16
21
  import 'axios';
17
- import 'curl-generator';
18
- import 'winston';
19
- import 'path';
20
22
  import 'axios-cache-interceptor';
21
23
  import 'xml-js';
22
24
 
25
+ const { combine, timestamp, printf } = winston.format;
26
+ class Logger {
27
+ #logger;
28
+ constructor(options) {
29
+ options = options || {};
30
+ const fileFormat = printf(({ level, message, timestamp: timestamp2 }) => {
31
+ return `[${timestamp2}] ${level.toUpperCase()}: ${message}`;
32
+ });
33
+ const templateFunction = (info) => {
34
+ if (info.level === "error") return ` Error: ${info.message}`;
35
+ if (typeof info.message === "string") return info.message;
36
+ return "";
37
+ };
38
+ const consoleFormat = printf(templateFunction);
39
+ const transports = [
40
+ new winston.transports.Console({ format: consoleFormat })
41
+ ];
42
+ if (options.log) {
43
+ transports.push(
44
+ new winston.transports.File({
45
+ filename: path.resolve(options.log),
46
+ format: combine(timestamp(), fileFormat),
47
+ options: { flags: "w" }
48
+ })
49
+ );
50
+ }
51
+ this.#logger = winston.createLogger({
52
+ level: options.logLevel,
53
+ transports
54
+ });
55
+ }
56
+ info(message) {
57
+ this.#logger.info(message);
58
+ }
59
+ debug(message) {
60
+ this.#logger.debug(message);
61
+ }
62
+ error(message) {
63
+ this.#logger.error(message);
64
+ }
65
+ }
66
+
23
67
  const program = new Command();
24
68
  program.name(name).version(version, "-v, --version").description(description).addOption(
25
69
  new Option("-c, --config <config>", "Path to [site].config.js file").makeOptionMandatory()
26
- ).addOption(new Option("-o, --output <output>", "Path to output file").default("guide.xml")).addOption(new Option("-x, --proxy <url>", "Use the specified proxy")).addOption(new Option("--channels <channels>", "Path to list of channels")).addOption(new Option("--lang <lang>", "Set default language for all programs")).addOption(
27
- new Option("--days <days>", "Number of days for which to grab the program").argParser(parseNumber).default(1)
70
+ ).addOption(new Option("-o, --output <output>", "Path to output file")).addOption(new Option("-x, --proxy <url>", "Use the specified proxy")).addOption(new Option("--channels <channels>", "Path to list of channels")).addOption(
71
+ new Option("--days <days>", "Number of days for which to grab the program").argParser(
72
+ parseNumber
73
+ )
28
74
  ).addOption(
29
- new Option("--delay <delay>", "Delay between requests (in milliseconds)").argParser(parseNumber).default(3e3)
75
+ new Option("--delay <delay>", "Delay between requests (in milliseconds)").argParser(parseNumber)
30
76
  ).addOption(
31
- new Option("--timeout <timeout>", "Set a timeout for each request (in milliseconds)").argParser(parseNumber).default(5e3)
77
+ new Option("--timeout <timeout>", "Set a timeout for each request (in milliseconds)").argParser(
78
+ parseNumber
79
+ )
32
80
  ).addOption(
33
81
  new Option(
34
82
  "--max-connections <maxConnections>",
35
83
  "Set a limit on the number of concurrent requests per site"
36
- ).argParser(parseNumber).default(1)
84
+ ).argParser(parseNumber)
37
85
  ).addOption(
38
86
  new Option(
39
87
  "--cache-ttl <cacheTtl>",
40
88
  "Maximum time for storing each request (in milliseconds)"
41
89
  ).argParser(parseNumber)
42
- ).addOption(new Option("--gzip", "Compress the output").default(false)).addOption(new Option("--debug", "Enable debug mode").default(false)).addOption(new Option("--curl", "Display request as CURL").default(false)).addOption(new Option("--log <log>", "Path to log file")).addOption(new Option("--log-level <level>", "Set log level").default("info")).parse(process.argv);
90
+ ).addOption(new Option("--gzip", "Compress the output")).addOption(new Option("--debug", "Enable debug mode")).addOption(new Option("--curl", "Display request as CURL")).addOption(new Option("--log <log>", "Path to log file")).addOption(new Option("--log-level <level>", "Set log level")).parse(process.argv);
43
91
  const options = program.opts();
44
92
  const logger = new Logger({
45
93
  log: options.log,
46
- logLevel: options.debug ? "debug" : options.logLevel
94
+ logLevel: options.debug === true ? "debug" : options.logLevel
47
95
  });
48
96
  async function main() {
49
97
  logger.info("Starting...");
50
- logger.debug(`Options: ${JSON.stringify(options, null, 2)}`);
51
98
  logger.info(`Loading '${options.config}'...`);
52
- let configObject = await loadJs(options.config);
53
- configObject = _.merge(configObject, {
54
- filepath: path.resolve(options.config),
55
- channels: typeof options.channels === "string" ? path.resolve(options.channels) : void 0,
56
- request: {}
57
- });
58
- if (configObject.output === void 0) configObject.output = options.output;
59
- if (configObject.days === void 0) configObject.days = options.days;
60
- if (configObject.delay === void 0) configObject.delay = options.delay;
61
- if (configObject.curl === void 0) configObject.curl = options.curl;
62
- if (configObject.debug === void 0) configObject.debug = options.debug;
63
- if (configObject.gzip === void 0) configObject.gzip = options.gzip;
64
- if (configObject.maxConnections === void 0)
65
- configObject.maxConnections = options.maxConnections;
66
- if (configObject.request.timeout === void 0) configObject.request.timeout = options.timeout;
67
- if (options.cacheTtl !== void 0) configObject.request.cache = { ttl: options.cacheTtl };
99
+ let config = await loadJs(options.config);
100
+ config.channels = Array.isArray(config.channels) ? config.channels : typeof config.channels === "string" ? [config.channels] : [];
101
+ if (typeof options.cacheTtl === "number")
102
+ config = merge(config, { request: { cache: { ttl: options.cacheTtl } } });
103
+ if (typeof options.timeout === "number")
104
+ config = merge(config, { request: { timeout: options.timeout } });
68
105
  if (options.proxy !== void 0) {
69
106
  const proxy = parseProxy(options.proxy);
70
107
  if (proxy.protocol && ["socks", "socks5", "socks5h", "socks4", "socks4a"].includes(String(proxy.protocol))) {
71
108
  const socksProxyAgent = new SocksProxyAgent(options.proxy);
72
- configObject.request = {
73
- ...configObject.request,
74
- ...{ httpAgent: socksProxyAgent, httpsAgent: socksProxyAgent }
75
- };
109
+ config = merge(config, {
110
+ request: { httpAgent: socksProxyAgent, httpsAgent: socksProxyAgent }
111
+ });
76
112
  } else {
77
- configObject.request = { ...configObject.request, ...{ proxy } };
113
+ config = merge(config, { request: { proxy } });
78
114
  }
79
115
  }
80
- const grabber = new EPGGrabber(configObject, { logger });
81
- logger.info("Loading channels...");
82
- const channels = await grabber.loadChannels();
83
- const template = new Template(configObject.output);
116
+ if (typeof options.channels === "string") config.channels = await glob(options.channels);
117
+ if (typeof options.output === "string") config.output = options.output;
118
+ if (typeof options.days === "number") config.days = options.days;
119
+ if (typeof options.delay === "number") config.delay = options.delay;
120
+ if (typeof options.maxConnections === "number") config.maxConnections = options.maxConnections;
121
+ if (typeof options.debug === "boolean") config.debug = options.debug;
122
+ if (typeof options.curl === "boolean") config.curl = options.curl;
123
+ if (typeof options.gzip === "boolean") config.gzip = options.gzip;
124
+ logger.debug(`Config: ${JSON.stringify(config, null, 2)}`);
125
+ const grabber = process.env.NODE_ENV === "test" ? new EPGGrabberMock(config) : new EPGGrabber(config);
126
+ grabber.client.instance.interceptors.request.use(
127
+ (request) => {
128
+ logger.debug(`Request: ${JSON.stringify(request, null, 2)}`);
129
+ const curl = config.curl || defaultConfig.curl;
130
+ if (curl) {
131
+ const url = request.url || "";
132
+ const method = request.method ? request.method : "GET";
133
+ const headers = request.headers ? request.headers.toJSON() : void 0;
134
+ const body = request.data ? request.data : void 0;
135
+ const curl2 = CurlGenerator({ url, method, headers, body });
136
+ logger.info(curl2);
137
+ }
138
+ return request;
139
+ },
140
+ (error) => Promise$1.reject(error)
141
+ );
142
+ grabber.client.instance.interceptors.response.use(
143
+ (response) => {
144
+ const data = response.data ? isObject(response.data) || Array.isArray(response.data) ? JSON.stringify(response.data) : response.data.toString() : void 0;
145
+ logger.debug(
146
+ `Response: ${JSON.stringify(
147
+ {
148
+ headers: response.headers,
149
+ data,
150
+ cached: response.cached
151
+ },
152
+ null,
153
+ 2
154
+ )}`
155
+ );
156
+ return response;
157
+ },
158
+ (error) => Promise$1.reject(error)
159
+ );
160
+ if (!Array.isArray(config.channels) || !config.channels.length)
161
+ throw new Error('Path to "*.channels.xml" is missing');
162
+ const channels = new Collection();
163
+ const rootDir = options.channels ? process.cwd() : path$1.dirname(options.config);
164
+ config.channels.forEach((filepath) => {
165
+ const absFilepath = getAbsPath(filepath, rootDir);
166
+ logger.debug(`Loading "${absFilepath}"...`);
167
+ const channelsXML = fs.readFileSync(absFilepath, "utf8");
168
+ const channelsFromXML = EPGGrabber.parseChannelsXML(channelsXML);
169
+ channels.concat(new Collection(channelsFromXML));
170
+ });
171
+ if (channels.isEmpty()) throw new Error("No channels found");
172
+ const days = config.days || defaultConfig.days;
173
+ const maxConnections = config.maxConnections || defaultConfig.maxConnections;
174
+ const gzip = config.gzip || defaultConfig.gzip;
175
+ const defaultOutput = gzip ? defaultConfig.output + ".gz" : defaultConfig.output;
176
+ const output = config.output || defaultOutput;
177
+ const template = new Template(output);
84
178
  const variables = template.variables();
85
- if (!channels.length) {
86
- logger.info("No channels found");
87
- logger.info("Exit");
88
- return;
89
- }
90
- const groups = new Collection(channels).groupBy((channel) => {
179
+ const groups = channels.groupBy((channel) => {
91
180
  let groupId = "";
92
181
  for (const key in channel) {
93
182
  if (variables.includes(key)) {
@@ -98,16 +187,14 @@ async function main() {
98
187
  return groupId;
99
188
  });
100
189
  logger.info("Processing...");
101
- for (let groupId of groups.keys()) {
190
+ for (const groupId of groups.keys()) {
102
191
  const group = groups.get(groupId);
103
192
  const groupChannels = new Collection(group);
104
193
  let programs = new Collection();
105
194
  let index = 1;
106
- let days = configObject.days;
107
- const maxConnections = configObject.maxConnections;
108
195
  const total = groupChannels.count() * days;
109
196
  const utcDate = getUTCDate(process.env.CURR_DATE);
110
- const dates = Array.from({ length: days }, (_2, i) => utcDate.add(i, "d"));
197
+ const dates = Array.from({ length: days }, (_, i) => utcDate.add(i, "d"));
111
198
  let queue = new Collection();
112
199
  groupChannels.forEach((channel) => {
113
200
  for (let date of dates) {
@@ -118,13 +205,13 @@ async function main() {
118
205
  const requests = queue.map(
119
206
  taskQueue.wrap(async (queueItem) => {
120
207
  const { channel, date } = queueItem;
121
- if (!channel.logo && configObject.logo) {
208
+ if (!channel.logo) {
122
209
  channel.logo = await grabber.loadLogo(channel, date);
123
210
  }
124
211
  const _programs = await grabber.grab(channel, date, (context, error) => {
125
212
  const { channel: channel2, date: date2, programs: programs2 } = context;
126
213
  logger.info(
127
- `[${index}/${total}] ${configObject.site} - ${channel2.xmltv_id || channel2.site_id} - ${date2.format("MMM D, YYYY")} (${programs2.length} programs)`
214
+ `[${index}/${total}] ${channel2.site} - ${channel2.xmltv_id || channel2.site_id} - ${date2.format("MMM D, YYYY")} (${programs2.length} programs)`
128
215
  );
129
216
  if (error) logger.error(error.message);
130
217
  if (index < total) index++;
@@ -133,18 +220,15 @@ async function main() {
133
220
  })
134
221
  );
135
222
  await Promise$1.all(requests.all());
136
- programs = programs.uniqBy((program2) => program2.start + program2.channel);
137
223
  const xml = EPGGrabber.generateXMLTV(groupChannels.all(), programs.all(), utcDate);
138
224
  const channelSample = groupChannels.sample();
139
225
  let outputPath = template.format(channelSample.toObject());
140
- const outputDir = path.dirname(outputPath);
226
+ const outputDir = path$1.dirname(outputPath);
141
227
  fs.mkdirSync(outputDir, { recursive: true });
142
- if (options.gzip) {
228
+ if (gzip) {
143
229
  const compressed = pako.gzip(xml);
144
- outputPath = outputPath || "guide.xml.gz";
145
230
  fs.writeFileSync(outputPath, compressed);
146
231
  } else {
147
- outputPath = outputPath || "guide.xml";
148
232
  fs.writeFileSync(outputPath, xml);
149
233
  }
150
234
  logger.info(`File '${outputPath}' successfully saved`);