epg-grabber 0.41.0 → 0.42.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 +16 -9
- package/dist/cli.js +168 -0
- package/dist/index-Bcnk0U8G.js +1018 -0
- package/dist/index.d.ts +337 -0
- package/dist/index.js +15 -0
- package/eslint.config.js +5 -0
- package/package.json +32 -24
- package/src/cli.ts +209 -0
- package/src/core/channelList.ts +60 -0
- package/src/core/channelsParser.ts +30 -0
- package/src/core/client.ts +163 -0
- package/src/core/logger.ts +62 -0
- package/src/core/programsParser.ts +27 -0
- package/src/core/proxyParser.ts +31 -0
- package/src/core/siteConfig.ts +102 -0
- package/src/core/storage.ts +9 -0
- package/src/core/xmltvGenerator.ts +29 -0
- package/src/index.ts +114 -0
- package/src/models/channel.ts +78 -0
- package/src/models/index.ts +2 -0
- package/src/models/program.ts +539 -0
- package/src/types/channel.d.ts +11 -0
- package/src/types/channelList.d.ts +11 -0
- package/src/types/client.d.ts +19 -0
- package/src/types/index.d.ts +10 -0
- package/src/types/program.d.ts +118 -0
- package/src/types/siteConfig.d.ts +151 -0
- package/src/utils.ts +118 -0
- package/tests/cli.test.ts +96 -0
- package/tests/core/channelsParser.test.ts +102 -0
- package/tests/core/client.test.ts +82 -0
- package/tests/core/programsParser.test.ts +48 -0
- package/tests/core/proxyParser.test.ts +12 -0
- package/tests/core/xmltvGenerator.test.ts +194 -0
- package/tests/index.test.ts +124 -0
- package/tests/{Channel.test.js → models/channel.test.ts} +6 -5
- package/tests/{Program.test.js → models/program.test.ts} +34 -23
- package/tests/models/siteConfig.test.ts +63 -0
- package/tests/utils.test.ts +14 -0
- package/tsconfig.json +19 -0
- package/.github/workflows/test.yml +0 -15
- package/bin/epg-grabber.js +0 -192
- package/eslint.config.mjs +0 -25
- package/src/Channel.js +0 -25
- package/src/Program.js +0 -231
- package/src/client.js +0 -128
- package/src/config.js +0 -36
- package/src/file.js +0 -58
- package/src/index.d.ts +0 -124
- package/src/index.js +0 -101
- package/src/logger.js +0 -34
- package/src/parser.js +0 -56
- package/src/utils.js +0 -103
- package/src/xmltv.js +0 -215
- package/tests/__data__/expected/duplicates.guide.xml +0 -6
- package/tests/__data__/input/duplicates.config.js +0 -18
- package/tests/__data__/output/guide.xml +0 -10
- package/tests/__mocks__/axios.js +0 -3
- package/tests/bin.test.js +0 -130
- package/tests/client.test.js +0 -65
- package/tests/config.test.js +0 -26
- package/tests/index.test.js +0 -151
- package/tests/parser.test.js +0 -112
- package/tests/utils.test.js +0 -18
- package/tests/xmltv.test.js +0 -176
- /package/tests/__data__/input/{async.config.js → async.config.cjs} +0 -0
- /package/tests/__data__/input/{example.config.js → example.config.cjs} +0 -0
- /package/tests/__data__/input/{example_channels.config.js → example_channels.config.cjs} +0 -0
- /package/tests/__data__/input/{mini.config.js → mini.config.cjs} +0 -0
package/README.md
CHANGED
|
@@ -118,9 +118,9 @@ module.exports = {
|
|
|
118
118
|
},
|
|
119
119
|
|
|
120
120
|
/**
|
|
121
|
-
* @param {object} context
|
|
121
|
+
* @param {object} Request context
|
|
122
122
|
*
|
|
123
|
-
* @return {
|
|
123
|
+
* @return {object} The function should return headers for each request (optional)
|
|
124
124
|
*/
|
|
125
125
|
headers: function(context) {
|
|
126
126
|
return {
|
|
@@ -130,9 +130,9 @@ module.exports = {
|
|
|
130
130
|
},
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
|
-
* @param {object} context
|
|
133
|
+
* @param {object} Request context
|
|
134
134
|
*
|
|
135
|
-
* @return {
|
|
135
|
+
* @return {any} The function should return data for each request (optional)
|
|
136
136
|
*/
|
|
137
137
|
data: function(context) {
|
|
138
138
|
const { channel, date } = context
|
|
@@ -146,7 +146,7 @@ module.exports = {
|
|
|
146
146
|
},
|
|
147
147
|
|
|
148
148
|
/**
|
|
149
|
-
* @param {object} context
|
|
149
|
+
* @param {object} Request context
|
|
150
150
|
*
|
|
151
151
|
* @return {string} The function should return URL of the program page for the channel
|
|
152
152
|
*/
|
|
@@ -155,7 +155,7 @@ module.exports = {
|
|
|
155
155
|
},
|
|
156
156
|
|
|
157
157
|
/**
|
|
158
|
-
* @param {object} context
|
|
158
|
+
* @param {object} Request context
|
|
159
159
|
*
|
|
160
160
|
* @return {string} The function should return URL of the channel logo (optional)
|
|
161
161
|
*/
|
|
@@ -164,7 +164,7 @@ module.exports = {
|
|
|
164
164
|
},
|
|
165
165
|
|
|
166
166
|
/**
|
|
167
|
-
* @param {object} context
|
|
167
|
+
* @param {object} Parser context
|
|
168
168
|
*
|
|
169
169
|
* @return {array} The function should return an array of programs with their descriptions
|
|
170
170
|
*/
|
|
@@ -184,9 +184,16 @@ module.exports = {
|
|
|
184
184
|
}
|
|
185
185
|
```
|
|
186
186
|
|
|
187
|
-
## Context Object
|
|
187
|
+
## Request Context Object
|
|
188
188
|
|
|
189
|
-
|
|
189
|
+
Inside `url()`, `logo()`, `request.data()`, `request.headers()` functions in `config.js` you can access a `context` object containing the following data:
|
|
190
|
+
|
|
191
|
+
- `channel`: The object describing the current channel (xmltv_id, site_id, name, lang)
|
|
192
|
+
- `date`: The 'dayjs' instance with the requested date
|
|
193
|
+
|
|
194
|
+
## Parser Context Object
|
|
195
|
+
|
|
196
|
+
Inside `parser()` function in `config.js` you can access a `context` object containing the following data:
|
|
190
197
|
|
|
191
198
|
- `channel`: The object describing the current channel (xmltv_id, site_id, name, lang)
|
|
192
199
|
- `date`: The 'dayjs' instance with the requested date
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { n as name, v as version, d as description, p as parseNumber, L as Logger, E as EPGGrabber, g as getUTCDate, a as generateXMLTV } from './index-Bcnk0U8G.js';
|
|
3
|
+
import { Template, Collection } from '@freearhey/core';
|
|
4
|
+
import { Command, Option } from 'commander';
|
|
5
|
+
import { SocksProxyAgent } from 'socks-proxy-agent';
|
|
6
|
+
import { URL } from 'node:url';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { TaskQueue } from 'cwait';
|
|
9
|
+
import Promise$1 from 'bluebird';
|
|
10
|
+
import fs from 'fs-extra';
|
|
11
|
+
import pako from 'pako';
|
|
12
|
+
import _ from 'lodash';
|
|
13
|
+
import 'dayjs';
|
|
14
|
+
import 'dayjs/plugin/utc.js';
|
|
15
|
+
import 'xml-js';
|
|
16
|
+
import 'glob';
|
|
17
|
+
import 'axios';
|
|
18
|
+
import 'curl-generator';
|
|
19
|
+
import 'axios-cache-interceptor';
|
|
20
|
+
import 'winston';
|
|
21
|
+
import 'path';
|
|
22
|
+
|
|
23
|
+
class ProxyParser {
|
|
24
|
+
static parse(proxy) {
|
|
25
|
+
const parsed = new URL(proxy);
|
|
26
|
+
const result = {
|
|
27
|
+
protocol: parsed.protocol.replace(":", "") || null,
|
|
28
|
+
host: parsed.hostname,
|
|
29
|
+
port: parsed.port ? parseInt(parsed.port) : null
|
|
30
|
+
};
|
|
31
|
+
if (parsed.username || parsed.password) {
|
|
32
|
+
result.auth = {};
|
|
33
|
+
if (parsed.username) result.auth.username = parsed.username;
|
|
34
|
+
if (parsed.password) result.auth.password = parsed.password;
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class Storage {
|
|
41
|
+
static async loadJs(filepath) {
|
|
42
|
+
const absPath = path.resolve(filepath);
|
|
43
|
+
return (await import(absPath)).default;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const program = new Command();
|
|
48
|
+
program.name(name).version(version, "-v, --version").description(description).addOption(
|
|
49
|
+
new Option("-c, --config <config>", "Path to [site].config.js file").makeOptionMandatory()
|
|
50
|
+
).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(
|
|
51
|
+
new Option("--days <days>", "Number of days for which to grab the program").argParser(parseNumber).default(1)
|
|
52
|
+
).addOption(
|
|
53
|
+
new Option("--delay <delay>", "Delay between requests (in milliseconds)").argParser(parseNumber).default(3e3)
|
|
54
|
+
).addOption(
|
|
55
|
+
new Option("--timeout <timeout>", "Set a timeout for each request (in milliseconds)").argParser(parseNumber).default(5e3)
|
|
56
|
+
).addOption(
|
|
57
|
+
new Option(
|
|
58
|
+
"--max-connections <maxConnections>",
|
|
59
|
+
"Set a limit on the number of concurrent requests per site"
|
|
60
|
+
).argParser(parseNumber).default(1)
|
|
61
|
+
).addOption(
|
|
62
|
+
new Option(
|
|
63
|
+
"--cache-ttl <cacheTtl>",
|
|
64
|
+
"Maximum time for storing each request (in milliseconds)"
|
|
65
|
+
).argParser(parseNumber)
|
|
66
|
+
).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);
|
|
67
|
+
const options = program.opts();
|
|
68
|
+
const logger = new Logger({
|
|
69
|
+
log: options.log,
|
|
70
|
+
logLevel: options.debug ? "debug" : options.logLevel
|
|
71
|
+
});
|
|
72
|
+
async function main() {
|
|
73
|
+
logger.info("Starting...");
|
|
74
|
+
logger.debug(`Options: ${JSON.stringify(options, null, 2)}`);
|
|
75
|
+
logger.info(`Loading '${options.config}'...`);
|
|
76
|
+
let configObject = await Storage.loadJs(options.config);
|
|
77
|
+
configObject = _.merge(configObject, {
|
|
78
|
+
filepath: path.resolve(options.config),
|
|
79
|
+
channels: typeof options.channels === "string" ? path.resolve(options.channels) : void 0,
|
|
80
|
+
request: {}
|
|
81
|
+
});
|
|
82
|
+
if (configObject.output === void 0) configObject.output = options.output;
|
|
83
|
+
if (configObject.days === void 0) configObject.days = options.days;
|
|
84
|
+
if (configObject.delay === void 0) configObject.delay = options.delay;
|
|
85
|
+
if (configObject.curl === void 0) configObject.curl = options.curl;
|
|
86
|
+
if (configObject.debug === void 0) configObject.debug = options.debug;
|
|
87
|
+
if (configObject.gzip === void 0) configObject.gzip = options.gzip;
|
|
88
|
+
if (configObject.maxConnections === void 0)
|
|
89
|
+
configObject.maxConnections = options.maxConnections;
|
|
90
|
+
if (configObject.request.timeout === void 0) configObject.request.timeout = options.timeout;
|
|
91
|
+
if (options.cacheTtl !== void 0) configObject.request.cache = { ttl: options.cacheTtl };
|
|
92
|
+
if (options.proxy !== void 0) {
|
|
93
|
+
const proxy = ProxyParser.parse(options.proxy);
|
|
94
|
+
if (proxy.protocol && ["socks", "socks5", "socks5h", "socks4", "socks4a"].includes(String(proxy.protocol))) {
|
|
95
|
+
const socksProxyAgent = new SocksProxyAgent(options.proxy);
|
|
96
|
+
configObject.request = {
|
|
97
|
+
...configObject.request,
|
|
98
|
+
...{ httpAgent: socksProxyAgent, httpsAgent: socksProxyAgent }
|
|
99
|
+
};
|
|
100
|
+
} else {
|
|
101
|
+
configObject.request = { ...configObject.request, ...{ proxy } };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const grabber = new EPGGrabber(configObject, { logger });
|
|
105
|
+
logger.info("Loading channel list...");
|
|
106
|
+
const channelList = await grabber.loadChannels();
|
|
107
|
+
const template = new Template(configObject.output);
|
|
108
|
+
const groups = channelList.getGroups({ template });
|
|
109
|
+
if (channelList.getChannels().isEmpty()) {
|
|
110
|
+
logger.info("No channels found");
|
|
111
|
+
logger.info("Exit");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
logger.info("Processing...");
|
|
115
|
+
for (let groupId of groups.keys()) {
|
|
116
|
+
const group = groups.get(groupId);
|
|
117
|
+
const channels = new Collection(group);
|
|
118
|
+
let programs = new Collection();
|
|
119
|
+
let index = 1;
|
|
120
|
+
let days = configObject.days;
|
|
121
|
+
const maxConnections = configObject.maxConnections;
|
|
122
|
+
const total = channels.count() * days;
|
|
123
|
+
const utcDate = getUTCDate(process.env.CURR_DATE);
|
|
124
|
+
const dates = Array.from({ length: days }, (_2, i) => utcDate.add(i, "d"));
|
|
125
|
+
let queue = new Collection();
|
|
126
|
+
channels.forEach((channel) => {
|
|
127
|
+
for (let date of dates) {
|
|
128
|
+
queue.add({ channel, date });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
const taskQueue = new TaskQueue(Promise$1, maxConnections);
|
|
132
|
+
const requests = queue.map(
|
|
133
|
+
taskQueue.wrap(async (queueItem) => {
|
|
134
|
+
const { channel, date } = queueItem;
|
|
135
|
+
if (!channel.logo && configObject.logo) {
|
|
136
|
+
channel.logo = await grabber.loadLogo({ channel, date });
|
|
137
|
+
}
|
|
138
|
+
const _programs = await grabber.grab(channel, date, (context, error) => {
|
|
139
|
+
const { channel: channel2, date: date2, programs: programs2 } = context;
|
|
140
|
+
logger.info(
|
|
141
|
+
`[${index}/${total}] ${configObject.site} - ${channel2.xmltv_id || channel2.site_id} - ${date2.format("MMM D, YYYY")} (${programs2.length} programs)`
|
|
142
|
+
);
|
|
143
|
+
if (error) logger.error(error.message);
|
|
144
|
+
if (index < total) index++;
|
|
145
|
+
});
|
|
146
|
+
programs.concat(new Collection(_programs));
|
|
147
|
+
})
|
|
148
|
+
);
|
|
149
|
+
await Promise$1.all(requests.all());
|
|
150
|
+
programs = programs.uniqBy((program2) => program2.start + program2.channel);
|
|
151
|
+
const xml = generateXMLTV(channels.all(), programs.all(), utcDate);
|
|
152
|
+
const channelSample = channels.sample();
|
|
153
|
+
let outputPath = template.format(channelSample.toObject());
|
|
154
|
+
const outputDir = path.dirname(outputPath);
|
|
155
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
156
|
+
if (options.gzip) {
|
|
157
|
+
const compressed = pako.gzip(xml);
|
|
158
|
+
outputPath = outputPath || "guide.xml.gz";
|
|
159
|
+
fs.writeFileSync(outputPath, compressed);
|
|
160
|
+
} else {
|
|
161
|
+
outputPath = outputPath || "guide.xml";
|
|
162
|
+
fs.writeFileSync(outputPath, xml);
|
|
163
|
+
}
|
|
164
|
+
logger.info(`File '${outputPath}' successfully saved`);
|
|
165
|
+
}
|
|
166
|
+
logger.info("Finished");
|
|
167
|
+
}
|
|
168
|
+
main();
|