epg-grabber 0.37.4 → 0.38.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 +3 -24
- package/bin/epg-grabber.js +32 -11
- package/eslint.config.mjs +25 -0
- package/package.json +6 -1
- package/src/Channel.js +0 -2
- package/src/client.js +4 -56
- package/src/config.js +5 -3
- package/src/index.d.ts +6 -4
- package/src/index.js +9 -1
- package/src/utils.js +16 -0
- package/tests/__data__/output/guide.xml +5 -1
- package/tests/bin.test.js +1 -0
- package/tests/utils.test.js +8 -1
- package/.eslintrc.js +0 -11
package/README.md
CHANGED
|
@@ -73,6 +73,7 @@ Arguments:
|
|
|
73
73
|
|
|
74
74
|
- `-c, --config`: path to config file
|
|
75
75
|
- `-o, --output`: path to output file or path template (example: `guides/{site}.{lang}.xml`; default: `guide.xml`)
|
|
76
|
+
- `-x, --proxy`: use the specified proxy
|
|
76
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`)
|
|
77
78
|
- `--lang`: set default language for all programs (default: `en`)
|
|
78
79
|
- `--days`: number of days for which to grab the program (default: `1`)
|
|
@@ -196,7 +197,7 @@ From each function in `config.js` you can access a `context` object containing t
|
|
|
196
197
|
|
|
197
198
|
## Program Object
|
|
198
199
|
|
|
199
|
-
| Property
|
|
200
|
+
| Property | Aliases | Type | Required |
|
|
200
201
|
| --------------- | -------------------------------- | ------------------------------------------------ | -------- |
|
|
201
202
|
| start | | `String` or `Number` or `Date()` | true |
|
|
202
203
|
| stop | | `String` or `Number` or `Date()` | true |
|
|
@@ -390,30 +391,8 @@ You can also specify the language, site, url, logo and LCN (Logical Channel Numb
|
|
|
390
391
|
|
|
391
392
|
## How to use SOCKS proxy?
|
|
392
393
|
|
|
393
|
-
First, you need to install [socks-proxy-agent](https://www.npmjs.com/package/socks-proxy-agent):
|
|
394
|
-
|
|
395
|
-
```sh
|
|
396
|
-
npm install socks-proxy-agent
|
|
397
394
|
```
|
|
398
|
-
|
|
399
|
-
Then you can use it to create an agent that acts as a SOCKS proxy. Here is an example of how to do it with the Tor SOCKS proxy:
|
|
400
|
-
|
|
401
|
-
```js
|
|
402
|
-
const { SocksProxyAgent } = require('socks-proxy-agent')
|
|
403
|
-
|
|
404
|
-
const torProxyAgent = new SocksProxyAgent('socks://localhost:9050')
|
|
405
|
-
|
|
406
|
-
module.exports = {
|
|
407
|
-
site: 'example.com',
|
|
408
|
-
url: 'https://example.com/epg.json',
|
|
409
|
-
request: {
|
|
410
|
-
httpsAgent: torProxyAgent,
|
|
411
|
-
httpAgent: torProxyAgent
|
|
412
|
-
},
|
|
413
|
-
parser(context) {
|
|
414
|
-
// ...
|
|
415
|
-
}
|
|
416
|
-
}
|
|
395
|
+
epg-grabber --config=example.com.config.js --proxy=socks://localhost:9050
|
|
417
396
|
```
|
|
418
397
|
|
|
419
398
|
## Contribution
|
package/bin/epg-grabber.js
CHANGED
|
@@ -5,14 +5,15 @@ const program = new Command()
|
|
|
5
5
|
const { merge } = require('lodash')
|
|
6
6
|
const { gzip } = require('node-gzip')
|
|
7
7
|
const file = require('../src/file')
|
|
8
|
-
const { EPGGrabber, parseChannels, generateXMLTV } = require('../src/index')
|
|
8
|
+
const { EPGGrabber, EPGGrabberMock, parseChannels, generateXMLTV } = require('../src/index')
|
|
9
9
|
const { create: createLogger } = require('../src/logger')
|
|
10
|
-
const { parseNumber, getUTCDate } = require('../src/utils')
|
|
10
|
+
const { parseNumber, getUTCDate, parseProxy } = require('../src/utils')
|
|
11
11
|
const { name, version, description } = require('../package.json')
|
|
12
12
|
const _ = require('lodash')
|
|
13
13
|
const dayjs = require('dayjs')
|
|
14
14
|
const utc = require('dayjs/plugin/utc')
|
|
15
15
|
const { TaskQueue } = require('cwait')
|
|
16
|
+
const { SocksProxyAgent } = require('socks-proxy-agent')
|
|
16
17
|
|
|
17
18
|
dayjs.extend(utc)
|
|
18
19
|
|
|
@@ -22,6 +23,7 @@ program
|
|
|
22
23
|
.description(description)
|
|
23
24
|
.requiredOption('-c, --config <config>', 'Path to [site].config.js file')
|
|
24
25
|
.option('-o, --output <output>', 'Path to output file')
|
|
26
|
+
.option('-x, --proxy <url>', 'Use the specified proxy')
|
|
25
27
|
.option('--channels <channels>', 'Path to list of channels')
|
|
26
28
|
.option('--lang <lang>', 'Set default language for all programs')
|
|
27
29
|
.option('--days <days>', 'Number of days for which to grab the program', parseNumber)
|
|
@@ -63,10 +65,26 @@ async function main() {
|
|
|
63
65
|
request: {}
|
|
64
66
|
})
|
|
65
67
|
|
|
66
|
-
if (options.timeout) config.request.timeout = options.timeout
|
|
67
|
-
if (options.cacheTtl) config.request.cache.ttl = options.cacheTtl
|
|
68
|
-
|
|
69
|
-
if (options.
|
|
68
|
+
if (options.timeout !== undefined) config.request.timeout = options.timeout
|
|
69
|
+
if (options.cacheTtl !== undefined) config.request.cache.ttl = options.cacheTtl
|
|
70
|
+
if (options.channels !== undefined) config.channels = options.channels
|
|
71
|
+
if (options.proxy !== undefined) {
|
|
72
|
+
const proxy = parseProxy(options.proxy)
|
|
73
|
+
|
|
74
|
+
if (
|
|
75
|
+
proxy.protocol &&
|
|
76
|
+
['socks', 'socks5', 'socks5h', 'socks4', 'socks4a'].includes(String(proxy.protocol))
|
|
77
|
+
) {
|
|
78
|
+
const socksProxyAgent = new SocksProxyAgent(options.proxy)
|
|
79
|
+
|
|
80
|
+
config.request = {
|
|
81
|
+
...config.request,
|
|
82
|
+
...{ httpAgent: socksProxyAgent, httpsAgent: socksProxyAgent }
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
config.request = { ...config.request, ...{ proxy } }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
70
88
|
|
|
71
89
|
let parsedChannels = []
|
|
72
90
|
if (config.channels) {
|
|
@@ -89,9 +107,10 @@ async function main() {
|
|
|
89
107
|
}
|
|
90
108
|
} else throw new Error('Path to "channels" is missing')
|
|
91
109
|
|
|
92
|
-
const grabber =
|
|
110
|
+
const grabber =
|
|
111
|
+
process.env.NODE_ENV === 'test' ? new EPGGrabberMock(config) : new EPGGrabber(config)
|
|
93
112
|
|
|
94
|
-
let template = options.output || config.output
|
|
113
|
+
let template = options.output || config.output || 'guide.xml'
|
|
95
114
|
const variables = file.templateVariables(template)
|
|
96
115
|
|
|
97
116
|
const groups = _.groupBy(parsedChannels, channel => {
|
|
@@ -133,9 +152,11 @@ async function main() {
|
|
|
133
152
|
await grabber
|
|
134
153
|
.grab(channel, date, (data, err) => {
|
|
135
154
|
logger.info(
|
|
136
|
-
`[${i}/${total}] ${config.site} - ${
|
|
137
|
-
.
|
|
138
|
-
|
|
155
|
+
`[${i}/${total}] ${config.site} - ${
|
|
156
|
+
data.channel.xmltv_id || data.channel.site_id
|
|
157
|
+
} - ${dayjs.utc(data.date).format('MMM D, YYYY')} (${
|
|
158
|
+
data.programs.length
|
|
159
|
+
} programs)`
|
|
139
160
|
)
|
|
140
161
|
|
|
141
162
|
if (err) logger.error(err.message)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import globals from "globals";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import js from "@eslint/js";
|
|
5
|
+
import { FlatCompat } from "@eslint/eslintrc";
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const compat = new FlatCompat({
|
|
10
|
+
baseDirectory: __dirname,
|
|
11
|
+
recommendedConfig: js.configs.recommended,
|
|
12
|
+
allConfig: js.configs.all
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export default [...compat.extends("eslint:recommended"), {
|
|
16
|
+
languageOptions: {
|
|
17
|
+
globals: {
|
|
18
|
+
...Object.fromEntries(Object.entries(globals.browser).map(([key]) => [key, "off"])),
|
|
19
|
+
...globals.node,
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
ecmaVersion: 12,
|
|
23
|
+
sourceType: "commonjs",
|
|
24
|
+
},
|
|
25
|
+
}];
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "epg-grabber",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "Node.js CLI tool for grabbing EPG from different sites",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"preferGlobal": true,
|
|
7
|
+
"homepage": "https://github.com/freearhey/epg-grabber",
|
|
7
8
|
"bin": {
|
|
8
9
|
"epg-grabber": "bin/epg-grabber.js"
|
|
9
10
|
},
|
|
@@ -41,6 +42,7 @@
|
|
|
41
42
|
"http-cookie-agent": "^6.0.8",
|
|
42
43
|
"lodash": "^4.17.21",
|
|
43
44
|
"node-gzip": "^1.1.2",
|
|
45
|
+
"socks-proxy-agent": "^8.0.5",
|
|
44
46
|
"tough-cookie": "^5.0.0",
|
|
45
47
|
"winston": "^3.3.3",
|
|
46
48
|
"xml-js": "^1.6.11"
|
|
@@ -48,8 +50,11 @@
|
|
|
48
50
|
"devDependencies": {
|
|
49
51
|
"@babel/core": "^7.13.14",
|
|
50
52
|
"@babel/preset-env": "^7.13.12",
|
|
53
|
+
"@eslint/eslintrc": "^3.2.0",
|
|
54
|
+
"@eslint/js": "^9.18.0",
|
|
51
55
|
"babel-jest": "^29.7.0",
|
|
52
56
|
"eslint": "^9.17.0",
|
|
57
|
+
"globals": "^15.14.0",
|
|
53
58
|
"jest": "^29.7.0",
|
|
54
59
|
"jest-mock-axios": "^4.4.1"
|
|
55
60
|
},
|
package/src/Channel.js
CHANGED
package/src/client.js
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
const { CurlGenerator } = require('curl-generator')
|
|
2
2
|
const { default: axios } = require('axios')
|
|
3
|
-
const { CookieJar } = require('tough-cookie')
|
|
4
3
|
const { setupCache } = require('axios-cache-interceptor')
|
|
5
4
|
const { isObject, isPromise } = require('./utils')
|
|
6
|
-
const { HttpCookieAgent, HttpsCookieAgent } = require('http-cookie-agent/http')
|
|
7
|
-
|
|
8
|
-
const jar = new CookieJar()
|
|
9
5
|
|
|
10
6
|
module.exports.create = create
|
|
11
7
|
module.exports.buildRequest = buildRequest
|
|
@@ -16,17 +12,10 @@ let timeout
|
|
|
16
12
|
function create(config) {
|
|
17
13
|
const client = axios.defaults.cache
|
|
18
14
|
? axios
|
|
19
|
-
:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
ignoreCookieErrors: true,
|
|
24
|
-
headers: {
|
|
25
|
-
'User-Agent':
|
|
26
|
-
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36 Edg/79.0.309.71'
|
|
27
|
-
}
|
|
28
|
-
})
|
|
29
|
-
)
|
|
15
|
+
: setupCache(
|
|
16
|
+
axios.create({
|
|
17
|
+
ignoreCookieErrors: true
|
|
18
|
+
})
|
|
30
19
|
)
|
|
31
20
|
|
|
32
21
|
client.interceptors.request.use(
|
|
@@ -142,44 +131,3 @@ async function getRequestUrl({ channel, date, config }) {
|
|
|
142
131
|
}
|
|
143
132
|
return config.url
|
|
144
133
|
}
|
|
145
|
-
|
|
146
|
-
const AGENT_CREATED_BY_AXIOS_COOKIEJAR_SUPPORT = Symbol('AGENT_CREATED_BY_AXIOS_COOKIEJAR_SUPPORT')
|
|
147
|
-
|
|
148
|
-
function requestInterceptor(config) {
|
|
149
|
-
if (!config.jar) {
|
|
150
|
-
return config
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
config.httpAgent = new HttpCookieAgent({ cookies: { jar: config.jar } })
|
|
154
|
-
Object.defineProperty(config.httpAgent, AGENT_CREATED_BY_AXIOS_COOKIEJAR_SUPPORT, {
|
|
155
|
-
configurable: false,
|
|
156
|
-
enumerable: false,
|
|
157
|
-
value: true,
|
|
158
|
-
writable: false
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
config.httpsAgent = new HttpsCookieAgent({ cookies: { jar: config.jar } })
|
|
162
|
-
Object.defineProperty(config.httpsAgent, AGENT_CREATED_BY_AXIOS_COOKIEJAR_SUPPORT, {
|
|
163
|
-
configurable: false,
|
|
164
|
-
enumerable: false,
|
|
165
|
-
value: true,
|
|
166
|
-
writable: false
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
return config
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function setupCookie(axios) {
|
|
173
|
-
axios.interceptors.request.use(requestInterceptor)
|
|
174
|
-
|
|
175
|
-
if ('create' in axios) {
|
|
176
|
-
const create = axios.create
|
|
177
|
-
axios.create = (...args) => {
|
|
178
|
-
const instance = create.apply(axios, args)
|
|
179
|
-
instance.interceptors.request.use(requestInterceptor)
|
|
180
|
-
return instance
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return axios
|
|
185
|
-
}
|
package/src/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
const tough = require('tough-cookie')
|
|
2
1
|
const { merge } = require('lodash')
|
|
2
|
+
const { version, homepage } = require('../package.json')
|
|
3
3
|
|
|
4
4
|
module.exports.parse = parse
|
|
5
5
|
|
|
@@ -24,9 +24,11 @@ function parse(config) {
|
|
|
24
24
|
maxContentLength: 5 * 1024 * 1024,
|
|
25
25
|
timeout: 5000,
|
|
26
26
|
withCredentials: true,
|
|
27
|
-
jar: new tough.CookieJar(),
|
|
28
27
|
responseType: 'arraybuffer',
|
|
29
|
-
cache: false
|
|
28
|
+
cache: false,
|
|
29
|
+
headers: {
|
|
30
|
+
'User-Agent': `EPGGrabber/${version} (${homepage})`
|
|
31
|
+
}
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
|
package/src/index.d.ts
CHANGED
|
@@ -101,19 +101,21 @@ export type GrabCallbackData = {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
export declare class EPGGrabber {
|
|
104
|
-
constructor(config
|
|
104
|
+
constructor(config?: SiteConfig)
|
|
105
105
|
grab(
|
|
106
106
|
channel: Channel,
|
|
107
107
|
date: string | dayjs.Dayjs,
|
|
108
|
-
|
|
108
|
+
config?: SiteConfig,
|
|
109
|
+
cb?: (data: GrabCallbackData, err: Error | null) => void
|
|
109
110
|
): Promise<Program[]>
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
export declare class EPGGrabberMock {
|
|
113
|
-
constructor(config
|
|
114
|
+
constructor(config?: SiteConfig)
|
|
114
115
|
grab(
|
|
115
116
|
channel: Channel,
|
|
116
117
|
date: string | dayjs.Dayjs,
|
|
117
|
-
|
|
118
|
+
config?: SiteConfig,
|
|
119
|
+
cb?: (data: GrabCallbackData, err: Error | null) => void
|
|
118
120
|
): Promise<Program[]>
|
|
119
121
|
}
|
package/src/index.js
CHANGED
|
@@ -60,10 +60,18 @@ class EPGGrabber {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
class EPGGrabberMock {
|
|
63
|
-
constructor(config) {
|
|
63
|
+
constructor(config = {}) {
|
|
64
64
|
this.config = config
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
async loadLogo(channel) {
|
|
68
|
+
const logo = this.config.logo({ channel })
|
|
69
|
+
if (isPromise(logo)) {
|
|
70
|
+
return await logo
|
|
71
|
+
}
|
|
72
|
+
return logo
|
|
73
|
+
}
|
|
74
|
+
|
|
67
75
|
async grab(channel, date, config = {}, cb = () => {}) {
|
|
68
76
|
let _date = getUTCDate(date)
|
|
69
77
|
if (typeof config == 'function') {
|
package/src/utils.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const { URL } = require('node:url')
|
|
1
2
|
const dayjs = require('dayjs')
|
|
2
3
|
const utc = require('dayjs/plugin/utc')
|
|
3
4
|
|
|
@@ -13,6 +14,21 @@ module.exports.formatDate = formatDate
|
|
|
13
14
|
module.exports.toArray = toArray
|
|
14
15
|
module.exports.toUnix = toUnix
|
|
15
16
|
module.exports.isDate = isDate
|
|
17
|
+
module.exports.parseProxy = parseProxy
|
|
18
|
+
|
|
19
|
+
function parseProxy(_url) {
|
|
20
|
+
const parsed = new URL(_url)
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
protocol: parsed.protocol.replace(':', '') || null,
|
|
24
|
+
auth: {
|
|
25
|
+
username: parsed.username || null,
|
|
26
|
+
password: parsed.password || null
|
|
27
|
+
},
|
|
28
|
+
host: parsed.hostname,
|
|
29
|
+
port: parsed.port ? parseInt(parsed.port) : null
|
|
30
|
+
}
|
|
31
|
+
}
|
|
16
32
|
|
|
17
33
|
function sleep(ms) {
|
|
18
34
|
return new Promise(resolve => setTimeout(resolve, ms))
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8" ?><tv date="
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" ?><tv date="20250131">
|
|
2
2
|
<channel id="1TV.com"><display-name>1 TV</display-name><icon src="https://example.com/logos/1TV.png"/><url>https://example.com</url></channel>
|
|
3
3
|
<channel id="2TV.com"><display-name>2 TV</display-name><url>https://example.com</url></channel>
|
|
4
4
|
<channel id="3TV.com"><display-name>3 TV</display-name><url>https://example2.com</url></channel>
|
|
5
5
|
<channel id="4TV.com"><display-name>4 TV</display-name><url>https://example2.com</url></channel>
|
|
6
|
+
<programme start="20220101000000 +0000" stop="20220101010000 +0000" channel="1TV.com"><title lang="fr">Program1</title></programme>
|
|
7
|
+
<programme start="20220101000000 +0000" stop="20220101010000 +0000" channel="4TV.com"><title>Program1</title></programme>
|
|
8
|
+
<programme start="20220101000000 +0000" stop="20220101010000 +0000" channel="3TV.com"><title>Program1</title></programme>
|
|
9
|
+
<programme start="20220101000000 +0000" stop="20220101010000 +0000" channel="2TV.com"><title>Program1</title></programme>
|
|
6
10
|
</tv>
|
package/tests/bin.test.js
CHANGED
package/tests/utils.test.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { escapeString } from '../src/utils'
|
|
1
|
+
import { escapeString, parseProxy } from '../src/utils'
|
|
2
2
|
|
|
3
3
|
it('can escape string', () => {
|
|
4
4
|
const string = 'Música тест dun. &<>"\'\r\n'
|
|
@@ -9,3 +9,10 @@ it('can escape url', () => {
|
|
|
9
9
|
const string = 'http://example.com/logos/1TV.png?param1=val¶m2=val'
|
|
10
10
|
expect(escapeString(string)).toBe('http://example.com/logos/1TV.png?param1=val&param2=val')
|
|
11
11
|
})
|
|
12
|
+
|
|
13
|
+
it('can parse proxy', () => {
|
|
14
|
+
const string = 'socks://127.0.0.1:1234'
|
|
15
|
+
expect(parseProxy(string)).toMatchObject({
|
|
16
|
+
protocol: 'socks'
|
|
17
|
+
})
|
|
18
|
+
})
|