epg-grabber 0.44.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.
- package/README.md +0 -2
- package/dist/cli.js +152 -60
- package/dist/index-CHTSs3QX.js +857 -0
- package/dist/index.d.ts +464 -226
- package/dist/index.js +5 -9
- package/package.json +7 -3
- package/src/cli.ts +148 -73
- package/src/core/client.ts +29 -105
- package/src/core/utils.ts +12 -5
- package/src/default.config.ts +23 -0
- package/src/index.ts +58 -122
- package/src/models/channel.ts +6 -6
- package/src/models/program.ts +283 -247
- package/src/types/client.d.ts +3 -3
- package/src/types/program.d.ts +438 -54
- package/src/types/siteConfig.d.ts +11 -111
- package/src/types/utils.d.ts +1 -1
- package/tests/__data__/expected/channels_array.guide.xml +14 -0
- package/tests/__data__/expected/example.guide.xml +8 -0
- package/tests/__data__/expected/fr/1TV.com.xml +3 -0
- package/tests/__data__/expected/mini.guide.xml +4 -0
- package/tests/__data__/expected/mini.guide.xml.gz +0 -0
- package/tests/__data__/expected/undefined/2TV.com.xml +3 -0
- package/tests/__data__/expected/wildcard.guide.xml +10 -0
- package/tests/__data__/input/example.config.cjs +1 -1
- package/tests/__data__/input/example_channels.config.cjs +1 -1
- package/tests/__data__/output/channels_array.guide.xml +14 -0
- package/tests/cli.test.ts +257 -18
- package/tests/core/client.test.ts +6 -6
- package/tests/index.test.ts +31 -81
- package/tests/models/program.test.ts +14 -13
- package/dist/index-DQUOgwl_.js +0 -1056
- package/src/core/siteConfig.ts +0 -130
- 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,
|
|
3
|
-
import {
|
|
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 '
|
|
16
|
-
import '
|
|
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")
|
|
27
|
-
new Option("--days <days>", "Number of days for which to grab the program").argParser(
|
|
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)
|
|
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(
|
|
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)
|
|
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")
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
};
|
|
109
|
+
config = merge(config, {
|
|
110
|
+
request: { httpAgent: socksProxyAgent, httpsAgent: socksProxyAgent }
|
|
111
|
+
});
|
|
76
112
|
} else {
|
|
77
|
-
|
|
113
|
+
config = merge(config, { request: { proxy } });
|
|
78
114
|
}
|
|
79
115
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 }, (
|
|
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
|
|
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}] ${
|
|
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 (
|
|
234
|
+
if (globalConfig.gzip) {
|
|
143
235
|
const compressed = pako.gzip(xml);
|
|
144
236
|
outputPath = outputPath || "guide.xml.gz";
|
|
145
237
|
fs.writeFileSync(outputPath, compressed);
|