epg-grabber 0.43.0 → 0.45.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.
Files changed (36) hide show
  1. package/README.md +0 -2
  2. package/dist/cli.js +152 -60
  3. package/dist/index-CHTSs3QX.js +857 -0
  4. package/dist/index.d.ts +471 -227
  5. package/dist/index.js +5 -9
  6. package/package.json +8 -3
  7. package/src/cli.ts +148 -73
  8. package/src/core/client.ts +29 -105
  9. package/src/core/utils.ts +23 -17
  10. package/src/default.config.ts +23 -0
  11. package/src/index.ts +90 -94
  12. package/src/models/channel.ts +6 -6
  13. package/src/models/program.ts +283 -247
  14. package/src/types/client.d.ts +3 -3
  15. package/src/types/index.d.ts +1 -0
  16. package/src/types/program.d.ts +438 -54
  17. package/src/types/siteConfig.d.ts +13 -113
  18. package/src/types/utils.d.ts +1 -11
  19. package/tests/__data__/expected/channels_array.guide.xml +14 -0
  20. package/tests/__data__/expected/example.guide.xml +8 -0
  21. package/tests/__data__/expected/fr/1TV.com.xml +3 -0
  22. package/tests/__data__/expected/mini.guide.xml +4 -0
  23. package/tests/__data__/expected/mini.guide.xml.gz +0 -0
  24. package/tests/__data__/expected/undefined/2TV.com.xml +3 -0
  25. package/tests/__data__/expected/wildcard.guide.xml +10 -0
  26. package/tests/__data__/input/example.config.cjs +1 -1
  27. package/tests/__data__/input/example_channels.config.cjs +1 -1
  28. package/tests/__data__/output/channels_array.guide.xml +14 -0
  29. package/tests/cli.test.ts +257 -18
  30. package/tests/core/client.test.ts +6 -6
  31. package/tests/core/utils.test.ts +9 -1
  32. package/tests/index.test.ts +31 -81
  33. package/tests/models/program.test.ts +14 -13
  34. package/dist/index-_okTrZbD.js +0 -1015
  35. package/src/core/siteConfig.ts +0 -130
  36. package/tests/core/siteConfig.test.ts +0 -61
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-_okTrZbD.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, i as isObject, g as getAbsPath, c as getUTCDate } from './index-CHTSs3QX.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';
9
+ import { AxiosHeaders } from 'axios';
6
10
  import { TaskQueue } from 'cwait';
11
+ import merge from 'lodash.merge';
7
12
  import Promise$1 from 'bluebird';
8
- import path from 'node:path';
13
+ import path$1 from 'node:path';
14
+ import { glob } from 'glob';
9
15
  import fs from 'fs-extra';
10
16
  import pako from 'pako';
11
- import _ from 'lodash';
12
- import 'dayjs';
13
17
  import 'dayjs/plugin/utc.js';
18
+ import 'dayjs';
14
19
  import 'node:url';
15
- import 'glob';
16
- import 'axios';
17
- import 'curl-generator';
18
- import 'winston';
19
- import 'path';
20
+ import 'axios-mock-adapter';
21
+ import 'lodash.padstart';
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
+ const grabber = process.env.NODE_ENV === "test" ? new EPGGrabberMock(config) : new EPGGrabber(config);
125
+ const globalConfig = grabber.globalConfig;
126
+ logger.debug(`Config: ${JSON.stringify(globalConfig, null, 2)}`);
127
+ grabber.client.instance.interceptors.request.use(
128
+ (request) => {
129
+ logger.debug(`Request: ${JSON.stringify(request, null, 2)}`);
130
+ if (globalConfig.curl) {
131
+ const headers = request.headers instanceof AxiosHeaders ? request.headers : {};
132
+ const method = request.method || "GET";
133
+ const curl = CurlGenerator({
134
+ url: request.url || "",
135
+ method,
136
+ headers,
137
+ body: request.data
138
+ });
139
+ logger.info(curl);
140
+ }
141
+ return request;
142
+ },
143
+ (error) => Promise$1.reject(error)
144
+ );
145
+ grabber.client.instance.interceptors.response.use(
146
+ (response) => {
147
+ const data = response.data ? isObject(response.data) || Array.isArray(response.data) ? JSON.stringify(response.data) : response.data.toString() : void 0;
148
+ logger.debug(
149
+ `Response: ${JSON.stringify(
150
+ {
151
+ headers: response.headers,
152
+ data,
153
+ cached: response.cached
154
+ },
155
+ null,
156
+ 2
157
+ )}`
158
+ );
159
+ return response;
160
+ },
161
+ (error) => Promise$1.reject(error)
162
+ );
163
+ if (!Array.isArray(globalConfig.channels) || !globalConfig.channels.length)
164
+ throw new Error('Path to "*.channels.xml" is missing');
165
+ const channels = new Collection();
166
+ const rootDir = options.channels ? process.cwd() : path$1.dirname(options.config);
167
+ globalConfig.channels.forEach((filepath) => {
168
+ const absFilepath = getAbsPath(filepath, rootDir);
169
+ logger.debug(`Loading "${absFilepath}"...`);
170
+ const channelsXML = fs.readFileSync(absFilepath, "utf8");
171
+ const channelsFromXML = EPGGrabber.parseChannelsXML(channelsXML);
172
+ channels.concat(new Collection(channelsFromXML));
173
+ });
174
+ if (channels.isEmpty()) throw new Error("No channels found");
175
+ if (typeof globalConfig.output !== "string")
176
+ throw new Error('The "output" property should return the string');
177
+ const template = new Template(globalConfig.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,33 +187,37 @@ async function main() {
98
187
  return groupId;
99
188
  });
100
189
  logger.info("Processing...");
101
- for (let groupId of groups.keys()) {
190
+ if (typeof globalConfig.days !== "number")
191
+ throw new Error('The "days" property should return the number');
192
+ if (typeof globalConfig.maxConnections !== "number")
193
+ throw new Error('The "maxConnections" property should return the number');
194
+ if (typeof globalConfig.gzip !== "boolean")
195
+ throw new Error('The "gzip" property should return the boolean');
196
+ for (const groupId of groups.keys()) {
102
197
  const group = groups.get(groupId);
103
198
  const groupChannels = new Collection(group);
104
199
  let programs = new Collection();
105
200
  let index = 1;
106
- let days = configObject.days;
107
- const maxConnections = configObject.maxConnections;
108
- const total = groupChannels.count() * days;
201
+ const total = groupChannels.count() * globalConfig.days;
109
202
  const utcDate = getUTCDate(process.env.CURR_DATE);
110
- const dates = Array.from({ length: days }, (_2, i) => utcDate.add(i, "d"));
203
+ const dates = Array.from({ length: globalConfig.days }, (_, i) => utcDate.add(i, "d"));
111
204
  let queue = new Collection();
112
205
  groupChannels.forEach((channel) => {
113
206
  for (let date of dates) {
114
207
  queue.add({ channel, date });
115
208
  }
116
209
  });
117
- const taskQueue = new TaskQueue(Promise$1, maxConnections);
210
+ const taskQueue = new TaskQueue(Promise$1, globalConfig.maxConnections);
118
211
  const requests = queue.map(
119
212
  taskQueue.wrap(async (queueItem) => {
120
213
  const { channel, date } = queueItem;
121
- if (!channel.logo && configObject.logo) {
214
+ if (!channel.logo) {
122
215
  channel.logo = await grabber.loadLogo(channel, date);
123
216
  }
124
217
  const _programs = await grabber.grab(channel, date, (context, error) => {
125
218
  const { channel: channel2, date: date2, programs: programs2 } = context;
126
219
  logger.info(
127
- `[${index}/${total}] ${configObject.site} - ${channel2.xmltv_id || channel2.site_id} - ${date2.format("MMM D, YYYY")} (${programs2.length} programs)`
220
+ `[${index}/${total}] ${channel2.site} - ${channel2.xmltv_id || channel2.site_id} - ${date2.format("MMM D, YYYY")} (${programs2.length} programs)`
128
221
  );
129
222
  if (error) logger.error(error.message);
130
223
  if (index < total) index++;
@@ -133,13 +226,12 @@ async function main() {
133
226
  })
134
227
  );
135
228
  await Promise$1.all(requests.all());
136
- programs = programs.uniqBy((program2) => program2.start + program2.channel);
137
229
  const xml = EPGGrabber.generateXMLTV(groupChannels.all(), programs.all(), utcDate);
138
230
  const channelSample = groupChannels.sample();
139
231
  let outputPath = template.format(channelSample.toObject());
140
- const outputDir = path.dirname(outputPath);
232
+ const outputDir = path$1.dirname(outputPath);
141
233
  fs.mkdirSync(outputDir, { recursive: true });
142
- if (options.gzip) {
234
+ if (globalConfig.gzip) {
143
235
  const compressed = pako.gzip(xml);
144
236
  outputPath = outputPath || "guide.xml.gz";
145
237
  fs.writeFileSync(outputPath, compressed);