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 +0 -2
- package/dist/cli.js +143 -59
- package/dist/index-DUJ5SdIU.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 +140 -72
- 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 -121
- 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 +199 -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, 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 '
|
|
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")
|
|
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
|
+
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
|
-
|
|
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 (
|
|
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 }, (
|
|
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
|
|
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}] ${
|
|
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 (
|
|
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`);
|