mirror of
https://github.com/iptv-org/epg.git
synced 2025-05-09 08:30:06 -04:00
Merge pull request #2653 from iptv-org/patch-2025.01.7
This commit is contained in:
commit
f7e1c1558c
24 changed files with 311 additions and 90 deletions
|
@ -3,6 +3,5 @@
|
|||
channels_changed="$(git diff --staged --name-only --diff-filter=ACMR -- 'sites/**/*.channels.xml' | sed 's| |\\ |g')"
|
||||
|
||||
if [ ! -z "$channels_changed" ]; then
|
||||
echo "npx eslint $channels_changed"
|
||||
npm run channels:lint -- $channels_changed
|
||||
fi
|
7
.husky/channels_validate.sh
Executable file
7
.husky/channels_validate.sh
Executable file
|
@ -0,0 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
channels_changed="$(git diff --staged --name-only --diff-filter=ACMR -- 'sites/**/*.channels.xml' | sed 's| |\\ |g')"
|
||||
|
||||
if [ ! -z "$channels_changed" ]; then
|
||||
npm run channels:validate -- $channels_changed
|
||||
fi
|
|
@ -1,2 +1,3 @@
|
|||
.husky/check_scripts.sh
|
||||
.husky/check_channels.sh
|
||||
.husky/scripts_lint.sh
|
||||
.husky/channels_lint.sh
|
||||
.husky/channels_validate.sh
|
|
@ -3,6 +3,5 @@
|
|||
scripts_changed="$(git diff --staged --name-only --diff-filter=ACMR -- 'tests/**/*.ts' 'tests/**/*.js' 'scripts/**/*.ts' 'scripts/**/*.mts' 'scripts/**/*.js' 'sites/**/*.js' 'sites/**/*.ts' | sed 's| |\\ |g')"
|
||||
|
||||
if [ ! -z "$scripts_changed" ]; then
|
||||
echo "npx eslint $scripts_changed"
|
||||
npx eslint $scripts_changed
|
||||
fi
|
|
@ -58,6 +58,7 @@ Options:
|
|||
-l, --lang <code> Filter channels by language (ISO 639-2 code)
|
||||
-t, --timeout <milliseconds> Override the default timeout for each request
|
||||
-d, --delay <milliseconds> Override the default delay between request
|
||||
-x, --proxy <url> Use the specified proxy (example: "socks5://username:password@127.0.0.1:1234")
|
||||
--days <days> Override the number of days for which the program will be loaded
|
||||
(defaults to the value from the site config)
|
||||
--maxConnections <number> Limit on the number of concurrent requests (default: 1)
|
||||
|
|
147
package-lock.json
generated
147
package-lock.json
generated
|
@ -11,8 +11,8 @@
|
|||
"@alex_neo/jest-expect-message": "^1.0.5",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@freearhey/core": "^0.5.0",
|
||||
"@ntlab/sfetch": "^1.0.0",
|
||||
"@freearhey/core": "^0.5.1",
|
||||
"@ntlab/sfetch": "^1.2.0",
|
||||
"@octokit/core": "^6.1.3",
|
||||
"@octokit/plugin-paginate-rest": "^11.3.6",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^13.2.6",
|
||||
|
@ -38,7 +38,7 @@
|
|||
"csv-parser": "^3.0.0",
|
||||
"cwait": "^1.1.2",
|
||||
"dayjs": "^1.11.10",
|
||||
"epg-grabber": "^0.37.4",
|
||||
"epg-grabber": "^0.38.0",
|
||||
"epg-parser": "^0.3.1",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
|
@ -69,6 +69,7 @@
|
|||
"serve": "^14.2.4",
|
||||
"signale": "^1.4.0",
|
||||
"skip-postinstall": "^1.0.0",
|
||||
"socks-proxy-agent": "^8.0.5",
|
||||
"srcset": "^4.0.0",
|
||||
"table2array": "^0.0.2",
|
||||
"tabletojson": "^2.0.7",
|
||||
|
@ -1203,9 +1204,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@freearhey/core": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@freearhey/core/-/core-0.5.0.tgz",
|
||||
"integrity": "sha512-FcA5Pv9RvFvLYAwNmD/2vlSR49Rx+kihJ+xbIUgIACHY6lBUptfbNznm00DQoUyWRJG/cfT3dkYCwIxSUsdP+w==",
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@freearhey/core/-/core-0.5.1.tgz",
|
||||
"integrity": "sha512-UDKIOyrtcUXaiAeIvjNFTI6DlempiOQaRB83CqHNF1VPRHNBiNhGhERWyInHE2cjLp/cc0CA/IykOYS39kBK7Q==",
|
||||
"dependencies": {
|
||||
"@types/fs-extra": "^11.0.2",
|
||||
"@types/lodash": "^4.14.198",
|
||||
|
@ -1863,9 +1864,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@ntlab/sfetch": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@ntlab/sfetch/-/sfetch-1.0.0.tgz",
|
||||
"integrity": "sha512-AWrC43z1TncvB7S7dl9Wn8xZpCqdKFBfXqaN3BXPfJeS3gxV9Fm86eAsW95YdXTOgPWbCC/GAgVuXi6Aot6DkQ==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ntlab/sfetch/-/sfetch-1.2.0.tgz",
|
||||
"integrity": "sha512-9SE4NnqWo8l6mG0rnAkgng6ozSamIpF3EC+GOTQGGa6eAC0tNJvzrylMz6YRjjEGH6mOfn7ZBAuKj5WIZUul6A==",
|
||||
"dependencies": {
|
||||
"axios": "^1.7.9"
|
||||
}
|
||||
|
@ -4192,9 +4193,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/epg-grabber": {
|
||||
"version": "0.37.4",
|
||||
"resolved": "https://registry.npmjs.org/epg-grabber/-/epg-grabber-0.37.4.tgz",
|
||||
"integrity": "sha512-PS104bH9tHRa9kivSwx47AKMkfHwKy51XQTx+GO6sIXvIp2Z4LBpwMEXGcfPoAsdIGxgs2Wrl0dZ/QGL+7x6YQ==",
|
||||
"version": "0.38.0",
|
||||
"resolved": "https://registry.npmjs.org/epg-grabber/-/epg-grabber-0.38.0.tgz",
|
||||
"integrity": "sha512-jbwTgi6G7e+zrb2oNC0C7mcQYoRkFnvhXCurexeICaEy4avRB6WS5rD/yfqYoiqaXOM3x1BNBpCKFYoS7Ob5YA==",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.1",
|
||||
"axios-cache-interceptor": "^0.10.3",
|
||||
|
@ -4209,6 +4210,7 @@
|
|||
"http-cookie-agent": "^6.0.8",
|
||||
"lodash": "^4.17.21",
|
||||
"node-gzip": "^1.1.2",
|
||||
"socks-proxy-agent": "^8.0.5",
|
||||
"tough-cookie": "^5.0.0",
|
||||
"winston": "^3.3.3",
|
||||
"xml-js": "^1.6.11"
|
||||
|
@ -5247,6 +5249,23 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
|
||||
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
|
||||
"dependencies": {
|
||||
"jsbn": "1.1.0",
|
||||
"sprintf-js": "^1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address/node_modules/sprintf-js": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
||||
|
@ -6141,6 +6160,11 @@
|
|||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsbn": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
||||
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
|
||||
|
@ -7641,6 +7665,41 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/smart-buffer": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||
"engines": {
|
||||
"node": ">= 6.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socks": {
|
||||
"version": "2.8.3",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
|
||||
"integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==",
|
||||
"dependencies": {
|
||||
"ip-address": "^9.0.5",
|
||||
"smart-buffer": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socks-proxy-agent": {
|
||||
"version": "8.0.5",
|
||||
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
|
||||
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "^4.3.4",
|
||||
"socks": "^2.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
|
@ -9172,9 +9231,9 @@
|
|||
}
|
||||
},
|
||||
"@freearhey/core": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@freearhey/core/-/core-0.5.0.tgz",
|
||||
"integrity": "sha512-FcA5Pv9RvFvLYAwNmD/2vlSR49Rx+kihJ+xbIUgIACHY6lBUptfbNznm00DQoUyWRJG/cfT3dkYCwIxSUsdP+w==",
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@freearhey/core/-/core-0.5.1.tgz",
|
||||
"integrity": "sha512-UDKIOyrtcUXaiAeIvjNFTI6DlempiOQaRB83CqHNF1VPRHNBiNhGhERWyInHE2cjLp/cc0CA/IykOYS39kBK7Q==",
|
||||
"requires": {
|
||||
"@types/fs-extra": "^11.0.2",
|
||||
"@types/lodash": "^4.14.198",
|
||||
|
@ -9661,9 +9720,9 @@
|
|||
}
|
||||
},
|
||||
"@ntlab/sfetch": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@ntlab/sfetch/-/sfetch-1.0.0.tgz",
|
||||
"integrity": "sha512-AWrC43z1TncvB7S7dl9Wn8xZpCqdKFBfXqaN3BXPfJeS3gxV9Fm86eAsW95YdXTOgPWbCC/GAgVuXi6Aot6DkQ==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ntlab/sfetch/-/sfetch-1.2.0.tgz",
|
||||
"integrity": "sha512-9SE4NnqWo8l6mG0rnAkgng6ozSamIpF3EC+GOTQGGa6eAC0tNJvzrylMz6YRjjEGH6mOfn7ZBAuKj5WIZUul6A==",
|
||||
"requires": {
|
||||
"axios": "^1.7.9"
|
||||
}
|
||||
|
@ -11280,9 +11339,9 @@
|
|||
"integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA=="
|
||||
},
|
||||
"epg-grabber": {
|
||||
"version": "0.37.4",
|
||||
"resolved": "https://registry.npmjs.org/epg-grabber/-/epg-grabber-0.37.4.tgz",
|
||||
"integrity": "sha512-PS104bH9tHRa9kivSwx47AKMkfHwKy51XQTx+GO6sIXvIp2Z4LBpwMEXGcfPoAsdIGxgs2Wrl0dZ/QGL+7x6YQ==",
|
||||
"version": "0.38.0",
|
||||
"resolved": "https://registry.npmjs.org/epg-grabber/-/epg-grabber-0.38.0.tgz",
|
||||
"integrity": "sha512-jbwTgi6G7e+zrb2oNC0C7mcQYoRkFnvhXCurexeICaEy4avRB6WS5rD/yfqYoiqaXOM3x1BNBpCKFYoS7Ob5YA==",
|
||||
"requires": {
|
||||
"axios": "^1.6.1",
|
||||
"axios-cache-interceptor": "^0.10.3",
|
||||
|
@ -11297,6 +11356,7 @@
|
|||
"http-cookie-agent": "^6.0.8",
|
||||
"lodash": "^4.17.21",
|
||||
"node-gzip": "^1.1.2",
|
||||
"socks-proxy-agent": "^8.0.5",
|
||||
"tough-cookie": "^5.0.0",
|
||||
"winston": "^3.3.3",
|
||||
"xml-js": "^1.6.11"
|
||||
|
@ -12006,6 +12066,22 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"ip-address": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
|
||||
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
|
||||
"requires": {
|
||||
"jsbn": "1.1.0",
|
||||
"sprintf-js": "^1.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"sprintf-js": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"is-arrayish": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
||||
|
@ -12650,6 +12726,11 @@
|
|||
"esprima": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"jsbn": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
||||
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
|
||||
},
|
||||
"jsesc": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
|
||||
|
@ -13776,6 +13857,30 @@
|
|||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="
|
||||
},
|
||||
"smart-buffer": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="
|
||||
},
|
||||
"socks": {
|
||||
"version": "2.8.3",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
|
||||
"integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==",
|
||||
"requires": {
|
||||
"ip-address": "^9.0.5",
|
||||
"smart-buffer": "^4.2.0"
|
||||
}
|
||||
},
|
||||
"socks-proxy-agent": {
|
||||
"version": "8.0.5",
|
||||
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
|
||||
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
|
||||
"requires": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "^4.3.4",
|
||||
"socks": "^2.8.3"
|
||||
}
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
|
|
|
@ -40,8 +40,8 @@
|
|||
"@alex_neo/jest-expect-message": "^1.0.5",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@freearhey/core": "^0.5.0",
|
||||
"@ntlab/sfetch": "^1.0.0",
|
||||
"@freearhey/core": "^0.5.1",
|
||||
"@ntlab/sfetch": "^1.2.0",
|
||||
"@octokit/core": "^6.1.3",
|
||||
"@octokit/plugin-paginate-rest": "^11.3.6",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^13.2.6",
|
||||
|
@ -67,7 +67,7 @@
|
|||
"csv-parser": "^3.0.0",
|
||||
"cwait": "^1.1.2",
|
||||
"dayjs": "^1.11.10",
|
||||
"epg-grabber": "^0.37.4",
|
||||
"epg-grabber": "^0.38.0",
|
||||
"epg-parser": "^0.3.1",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
|
@ -98,6 +98,7 @@
|
|||
"serve": "^14.2.4",
|
||||
"signale": "^1.4.0",
|
||||
"skip-postinstall": "^1.0.0",
|
||||
"socks-proxy-agent": "^8.0.5",
|
||||
"srcset": "^4.0.0",
|
||||
"table2array": "^0.0.2",
|
||||
"tabletojson": "^2.0.7",
|
||||
|
|
|
@ -26,7 +26,7 @@ async function main() {
|
|||
const logger = new Logger()
|
||||
const file = new File(options.config)
|
||||
const dir = file.dirname()
|
||||
const config = (await import(pathToFileURL(options.config))).default
|
||||
const config = (await import(pathToFileURL(options.config).toString())).default
|
||||
const outputFilepath = options.output || `${dir}/${config.site}.channels.xml`
|
||||
|
||||
let channels = new Collection()
|
||||
|
|
|
@ -16,6 +16,7 @@ program
|
|||
.option('-l, --lang <code>', 'Filter channels by language (ISO 639-2 code)')
|
||||
.option('-t, --timeout <milliseconds>', 'Override the default timeout for each request')
|
||||
.option('-d, --delay <milliseconds>', 'Override the default delay between request')
|
||||
.option('-x, --proxy <url>', 'Use the specified proxy')
|
||||
.option(
|
||||
'--days <days>',
|
||||
'Override the number of days for which the program will be loaded (defaults to the value from the site config)',
|
||||
|
@ -42,6 +43,7 @@ export type GrabOptions = {
|
|||
lang?: string
|
||||
days?: number
|
||||
cron?: string
|
||||
proxy?: string
|
||||
}
|
||||
|
||||
const options: GrabOptions = program.opts()
|
||||
|
|
|
@ -40,19 +40,20 @@ async function main() {
|
|||
const data = new Collection()
|
||||
sites.forEach((site: Site) => {
|
||||
data.add([
|
||||
`<a href="sites/${site.domain}">${site.domain}</a>`,
|
||||
`${site.totalChannels} / ${site.markedChannels}`,
|
||||
site.getStatus().emoji,
|
||||
site.getIssues().all().join(', ')
|
||||
{ value: `<a href="sites/${site.domain}">${site.domain}</a>` },
|
||||
{ value: site.totalChannels, align: 'right' },
|
||||
{ value: site.markedChannels, align: 'right' },
|
||||
{ value: site.getStatus().emoji, align: 'center' },
|
||||
{ value: site.getIssues().all().join(', ') }
|
||||
])
|
||||
})
|
||||
|
||||
logger.info('updating sites.md...')
|
||||
const table = new HTMLTable(data.all(), [
|
||||
{ name: 'Site' },
|
||||
{ name: 'Channels *', align: 'center' },
|
||||
{ name: 'Status' },
|
||||
{ name: 'Notes' }
|
||||
{ name: 'Site', align: 'left' },
|
||||
{ name: 'Channels<br>(total / with xmltv-id)', colspan: 2, align: 'left' },
|
||||
{ name: 'Status', align: 'left' },
|
||||
{ name: 'Notes', align: 'left' }
|
||||
])
|
||||
const rootStorage = new Storage(ROOT_DIR)
|
||||
const sitesTemplate = await new Storage().load('scripts/templates/_sites.md')
|
||||
|
|
|
@ -7,7 +7,7 @@ export class ConfigLoader {
|
|||
const fileUrl = pathToFileURL(filepath).toString()
|
||||
const config = (await import(fileUrl)).default
|
||||
const defaultConfig = {
|
||||
days: 2,
|
||||
days: 1,
|
||||
delay: 0,
|
||||
output: 'guide.xml',
|
||||
request: {
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { EPGGrabber, GrabCallbackData, EPGGrabberMock, SiteConfig, Channel } from 'epg-grabber'
|
||||
import { Logger, Collection } from '@freearhey/core'
|
||||
import { Queue } from './'
|
||||
import { Queue, ProxyParser } from './'
|
||||
import { GrabOptions } from '../commands/epg/grab'
|
||||
import { TaskQueue, PromisyClass } from 'cwait'
|
||||
import { SocksProxyAgent } from 'socks-proxy-agent'
|
||||
|
||||
type GrabberProps = {
|
||||
logger: Logger
|
||||
|
@ -14,6 +15,7 @@ export class Grabber {
|
|||
logger: Logger
|
||||
queue: Queue
|
||||
options: GrabOptions
|
||||
grabber: EPGGrabber | EPGGrabberMock
|
||||
|
||||
constructor({ logger, queue, options }: GrabberProps) {
|
||||
this.logger = logger
|
||||
|
@ -23,6 +25,7 @@ export class Grabber {
|
|||
}
|
||||
|
||||
async grab(): Promise<{ channels: Collection; programs: Collection }> {
|
||||
const proxyParser = new ProxyParser()
|
||||
const taskQueue = new TaskQueue(Promise as PromisyClass, this.options.maxConnections)
|
||||
|
||||
const total = this.queue.size()
|
||||
|
@ -49,6 +52,24 @@ export class Grabber {
|
|||
config.delay = delay
|
||||
}
|
||||
|
||||
if (this.options.proxy !== undefined) {
|
||||
const proxy = proxyParser.parse(this.options.proxy)
|
||||
|
||||
if (
|
||||
proxy.protocol &&
|
||||
['socks', 'socks5', 'socks5h', 'socks4', 'socks4a'].includes(String(proxy.protocol))
|
||||
) {
|
||||
const socksProxyAgent = new SocksProxyAgent(this.options.proxy)
|
||||
|
||||
config.request = {
|
||||
...config.request,
|
||||
...{ httpAgent: socksProxyAgent, httpsAgent: socksProxyAgent }
|
||||
}
|
||||
} else {
|
||||
config.request = { ...config.request, ...{ proxy } }
|
||||
}
|
||||
}
|
||||
|
||||
const _programs = await this.grabber.grab(
|
||||
channel,
|
||||
date,
|
||||
|
|
|
@ -2,9 +2,15 @@ type Column = {
|
|||
name: string
|
||||
nowrap?: boolean
|
||||
align?: string
|
||||
colspan?: number
|
||||
}
|
||||
|
||||
type DataItem = string[]
|
||||
type DataItem = {
|
||||
value: string
|
||||
nowrap?: boolean
|
||||
align?: string
|
||||
colspan?: number
|
||||
}[]
|
||||
|
||||
export class HTMLTable {
|
||||
data: DataItem[]
|
||||
|
@ -20,20 +26,23 @@ export class HTMLTable {
|
|||
|
||||
output += ' <thead>\r\n <tr>'
|
||||
for (const column of this.columns) {
|
||||
output += `<th align="left">${column.name}</th>`
|
||||
const nowrap = column.nowrap ? ' nowrap' : ''
|
||||
const align = column.align ? ` align="${column.align}"` : ''
|
||||
const colspan = column.colspan ? ` colspan="${column.colspan}"` : ''
|
||||
|
||||
output += `<th${align}${nowrap}${colspan}>${column.name}</th>`
|
||||
}
|
||||
output += '</tr>\r\n </thead>\r\n'
|
||||
|
||||
output += ' <tbody>\r\n'
|
||||
for (const item of this.data) {
|
||||
for (const row of this.data) {
|
||||
output += ' <tr>'
|
||||
let i = 0
|
||||
for (const prop in item) {
|
||||
const column = this.columns[i]
|
||||
const nowrap = column.nowrap ? ' nowrap' : ''
|
||||
const align = column.align ? ` align="${column.align}"` : ''
|
||||
output += `<td${align}${nowrap}>${item[prop]}</td>`
|
||||
i++
|
||||
for (const item of row) {
|
||||
const nowrap = item.nowrap ? ' nowrap' : ''
|
||||
const align = item.align ? ` align="${item.align}"` : ''
|
||||
const colspan = item.colspan ? ` colspan="${item.colspan}"` : ''
|
||||
|
||||
output += `<td${align}${nowrap}${colspan}>${item.value}</td>`
|
||||
}
|
||||
output += '</tr>\r\n'
|
||||
}
|
||||
|
|
|
@ -13,3 +13,4 @@ export * from './queueCreator'
|
|||
export * from './issueLoader'
|
||||
export * from './issueParser'
|
||||
export * from './htmlTable'
|
||||
export * from './proxyParser'
|
||||
|
|
27
scripts/core/proxyParser.ts
Normal file
27
scripts/core/proxyParser.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { URL } from 'node:url'
|
||||
|
||||
type ProxyParserResult = {
|
||||
protocol: string | null
|
||||
auth: {
|
||||
username: string | null
|
||||
password: string | null
|
||||
}
|
||||
host: string
|
||||
port: number | null
|
||||
}
|
||||
|
||||
export class ProxyParser {
|
||||
parse(_url: string): ProxyParserResult {
|
||||
const parsed = new URL(_url)
|
||||
|
||||
return {
|
||||
protocol: parsed.protocol.replace(':', '') || null,
|
||||
auth: {
|
||||
username: parsed.username || null,
|
||||
password: parsed.password || null
|
||||
},
|
||||
host: parsed.hostname,
|
||||
port: parsed.port ? parseInt(parsed.port) : null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
# Sites
|
||||
|
||||
\* Total number of channels / with a valid `xmltv-id`
|
||||
|
||||
_TABLE_
|
||||
|
|
|
@ -6,7 +6,7 @@ dayjs.extend(customParseFormat)
|
|||
dayjs.extend(utc)
|
||||
|
||||
const date = dayjs.utc('2025-01-12', 'YYYY-MM-DD').startOf('d')
|
||||
const channel = { site_id: 'bbc1', xmltv_id: 'BBCOne.uk' }
|
||||
const channel = { site_id: 'bbc1' }
|
||||
|
||||
it('can generate valid url', () => {
|
||||
expect(url({ channel, date })).toBe('https://example.com/api/bbc1/2025-01-12')
|
||||
|
@ -32,11 +32,7 @@ it('can parse response', () => {
|
|||
})
|
||||
|
||||
it('can handle empty guide', () => {
|
||||
const result = parser({
|
||||
date,
|
||||
channel,
|
||||
content: ''
|
||||
})
|
||||
const results = parser({ content: '' })
|
||||
|
||||
expect(result).toMatchObject([])
|
||||
expect(results).toMatchObject([])
|
||||
})
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
# Sites
|
||||
|
||||
\* Total number of channels / with a valid `xmltv-id`
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th align="left">Site</th><th align="left">Channels *</th><th align="left">Status</th><th align="left">Notes</th></tr>
|
||||
<tr><th align="left">Site</th><th align="left" colspan="2">Channels<br>(total / with xmltv-id)</th><th align="left">Status</th><th align="left">Notes</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><a href="sites/iltalehti.fi">iltalehti.fi</a></td><td align="center">142 / 44</td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2396</td></tr>
|
||||
<tr><td><a href="sites/indihometv.com">indihometv.com</a></td><td align="center">130 / 124</td><td>🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/kan.org.il">kan.org.il</a></td><td align="center">6 / 6</td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2273</td></tr>
|
||||
<tr><td><a href="sites/iltalehti.fi">iltalehti.fi</a></td><td align="right">142</td><td align="right">44</td><td align="center">🟡</td><td>https://github.com/iptv-org/epg/issues/2396</td></tr>
|
||||
<tr><td><a href="sites/indihometv.com">indihometv.com</a></td><td align="right">130</td><td align="right">124</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/kan.org.il">kan.org.il</a></td><td align="right">6</td><td align="right">6</td><td align="center">🔴</td><td>https://github.com/iptv-org/epg/issues/2273</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -9,9 +9,6 @@
|
|||
<programme start="20221019044000 +0000" stop="20221019071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example2.com)</title></programme>
|
||||
<programme start="20221020043000 +0000" stop="20221020071000 +0000" channel="Channel1.us"><title lang="en">Program1 (example.com)</title></programme>
|
||||
<programme start="20221020043000 +0000" stop="20221020071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example.com)</title></programme>
|
||||
<programme start="20221020044000 +0000" stop="20221020071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example2.com)</title></programme>
|
||||
<programme start="20221019043000 +0000" stop="20221019071000 +0000" channel="Channel3.us"><title lang="en">Program1 (example2.com)</title></programme>
|
||||
<programme start="20221020043000 +0000" stop="20221020071000 +0000" channel="Channel3.us"><title lang="en">Program1 (example2.com)</title></programme>
|
||||
<programme start="20221019043000 +0000" stop="20221019071000 +0000" channel="Channel4.us"><title lang="en">Program1 (example2.com)</title></programme>
|
||||
<programme start="20221020043000 +0000" stop="20221020071000 +0000" channel="Channel4.us"><title lang="en">Program1 (example2.com)</title></programme>
|
||||
</tv>
|
|
@ -9,9 +9,6 @@
|
|||
<programme start="20221019044000 +0000" stop="20221019071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example2.com)</title></programme>
|
||||
<programme start="20221020043000 +0000" stop="20221020071000 +0000" channel="Channel1.us"><title lang="en">Program1 (example.com)</title></programme>
|
||||
<programme start="20221020043000 +0000" stop="20221020071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example.com)</title></programme>
|
||||
<programme start="20221020044000 +0000" stop="20221020071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example2.com)</title></programme>
|
||||
<programme start="20221019043000 +0000" stop="20221019071000 +0000" channel="Channel3.us"><title lang="en">Program1 (example2.com)</title></programme>
|
||||
<programme start="20221020043000 +0000" stop="20221020071000 +0000" channel="Channel3.us"><title lang="en">Program1 (example2.com)</title></programme>
|
||||
<programme start="20221019043000 +0000" stop="20221019071000 +0000" channel="Channel4.us"><title lang="en">Program1 (example2.com)</title></programme>
|
||||
<programme start="20221020043000 +0000" stop="20221020071000 +0000" channel="Channel4.us"><title lang="en">Program1 (example2.com)</title></programme>
|
||||
</tv>
|
Binary file not shown.
|
@ -6,7 +6,7 @@ dayjs.extend(customParseFormat)
|
|||
dayjs.extend(utc)
|
||||
|
||||
const date = dayjs.utc('2025-01-12', 'YYYY-MM-DD').startOf('d')
|
||||
const channel = { site_id: 'bbc1', xmltv_id: 'BBCOne.uk' }
|
||||
const channel = { site_id: 'bbc1' }
|
||||
|
||||
it('can generate valid url', () => {
|
||||
expect(url({ channel, date })).toBe('https://example.com/api/bbc1/2025-01-12')
|
||||
|
@ -32,11 +32,7 @@ it('can parse response', () => {
|
|||
})
|
||||
|
||||
it('can handle empty guide', () => {
|
||||
const result = parser({
|
||||
date,
|
||||
channel,
|
||||
content: ''
|
||||
})
|
||||
const results = parser({ content: '' })
|
||||
|
||||
expect(result).toMatchObject([])
|
||||
expect(results).toMatchObject([])
|
||||
})
|
||||
|
|
|
@ -97,6 +97,30 @@ describe('epg:grab', () => {
|
|||
|
||||
expect(stdout).toContain('ERR: Connection timeout')
|
||||
})
|
||||
|
||||
it('can grab epg via https proxy', () => {
|
||||
const cmd = `${ENV_VAR} npm run grab --- --site=example.com --proxy=https://bob:123456@proxy.com:1234 --output="${path.resolve(
|
||||
'tests/__data__/output/guide.xml'
|
||||
)}"`
|
||||
const stdout = execSync(cmd, { encoding: 'utf8' })
|
||||
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
|
||||
|
||||
expect(content('tests/__data__/output/guide.xml')).toEqual(
|
||||
content('tests/__data__/expected/guide2.xml')
|
||||
)
|
||||
})
|
||||
|
||||
it('can grab epg via socks5 proxy', () => {
|
||||
const cmd = `${ENV_VAR} npm run grab --- --site=example.com --proxy=socks5://bob:123456@proxy.com:1234 --output="${path.resolve(
|
||||
'tests/__data__/output/guide.xml'
|
||||
)}"`
|
||||
const stdout = execSync(cmd, { encoding: 'utf8' })
|
||||
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
|
||||
|
||||
expect(content('tests/__data__/output/guide.xml')).toEqual(
|
||||
content('tests/__data__/expected/guide2.xml')
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
function content(filepath: string) {
|
||||
|
|
67
yarn.lock
67
yarn.lock
|
@ -396,10 +396,10 @@
|
|||
dependencies:
|
||||
levn "^0.4.1"
|
||||
|
||||
"@freearhey/core@^0.5.0":
|
||||
version "0.5.0"
|
||||
resolved "https://registry.npmjs.org/@freearhey/core/-/core-0.5.0.tgz"
|
||||
integrity sha512-FcA5Pv9RvFvLYAwNmD/2vlSR49Rx+kihJ+xbIUgIACHY6lBUptfbNznm00DQoUyWRJG/cfT3dkYCwIxSUsdP+w==
|
||||
"@freearhey/core@^0.5.1":
|
||||
version "0.5.1"
|
||||
resolved "https://registry.npmjs.org/@freearhey/core/-/core-0.5.1.tgz"
|
||||
integrity sha512-UDKIOyrtcUXaiAeIvjNFTI6DlempiOQaRB83CqHNF1VPRHNBiNhGhERWyInHE2cjLp/cc0CA/IykOYS39kBK7Q==
|
||||
dependencies:
|
||||
"@types/fs-extra" "^11.0.2"
|
||||
"@types/lodash" "^4.14.198"
|
||||
|
@ -741,10 +741,10 @@
|
|||
"@nodelib/fs.scandir" "2.1.5"
|
||||
fastq "^1.6.0"
|
||||
|
||||
"@ntlab/sfetch@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/@ntlab/sfetch/-/sfetch-1.0.0.tgz"
|
||||
integrity sha512-AWrC43z1TncvB7S7dl9Wn8xZpCqdKFBfXqaN3BXPfJeS3gxV9Fm86eAsW95YdXTOgPWbCC/GAgVuXi6Aot6DkQ==
|
||||
"@ntlab/sfetch@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/@ntlab/sfetch/-/sfetch-1.2.0.tgz"
|
||||
integrity sha512-9SE4NnqWo8l6mG0rnAkgng6ozSamIpF3EC+GOTQGGa6eAC0tNJvzrylMz6YRjjEGH6mOfn7ZBAuKj5WIZUul6A==
|
||||
dependencies:
|
||||
axios "^1.7.9"
|
||||
|
||||
|
@ -1271,7 +1271,7 @@ acorn-walk@^8.1.1:
|
|||
resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz"
|
||||
integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==
|
||||
|
||||
agent-base@^7.1.3:
|
||||
agent-base@^7.1.2, agent-base@^7.1.3:
|
||||
version "7.1.3"
|
||||
resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz"
|
||||
integrity sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==
|
||||
|
@ -2158,10 +2158,10 @@ entities@^4.2.0, entities@^4.3.0, entities@^4.4.0:
|
|||
resolved "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz"
|
||||
integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==
|
||||
|
||||
epg-grabber@^0.37.4:
|
||||
version "0.37.4"
|
||||
resolved "https://registry.npmjs.org/epg-grabber/-/epg-grabber-0.37.4.tgz"
|
||||
integrity sha512-PS104bH9tHRa9kivSwx47AKMkfHwKy51XQTx+GO6sIXvIp2Z4LBpwMEXGcfPoAsdIGxgs2Wrl0dZ/QGL+7x6YQ==
|
||||
epg-grabber@^0.38.0:
|
||||
version "0.38.0"
|
||||
resolved "https://registry.npmjs.org/epg-grabber/-/epg-grabber-0.38.0.tgz"
|
||||
integrity sha512-jbwTgi6G7e+zrb2oNC0C7mcQYoRkFnvhXCurexeICaEy4avRB6WS5rD/yfqYoiqaXOM3x1BNBpCKFYoS7Ob5YA==
|
||||
dependencies:
|
||||
axios "^1.6.1"
|
||||
axios-cache-interceptor "^0.10.3"
|
||||
|
@ -2176,6 +2176,7 @@ epg-grabber@^0.37.4:
|
|||
http-cookie-agent "^6.0.8"
|
||||
lodash "^4.17.21"
|
||||
node-gzip "^1.1.2"
|
||||
socks-proxy-agent "^8.0.5"
|
||||
tough-cookie "^5.0.0"
|
||||
winston "^3.3.3"
|
||||
xml-js "^1.6.11"
|
||||
|
@ -2835,6 +2836,14 @@ inquirer@^8.2.6:
|
|||
through "^2.3.6"
|
||||
wrap-ansi "^6.0.1"
|
||||
|
||||
ip-address@^9.0.5:
|
||||
version "9.0.5"
|
||||
resolved "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz"
|
||||
integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==
|
||||
dependencies:
|
||||
jsbn "1.1.0"
|
||||
sprintf-js "^1.1.3"
|
||||
|
||||
is-arrayish@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz"
|
||||
|
@ -3373,6 +3382,11 @@ js-yaml@^4.1.0:
|
|||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
|
||||
jsbn@1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz"
|
||||
integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==
|
||||
|
||||
jsesc@^2.5.1:
|
||||
version "2.5.2"
|
||||
resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz"
|
||||
|
@ -4393,6 +4407,28 @@ slash@^3.0.0:
|
|||
resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz"
|
||||
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
|
||||
|
||||
smart-buffer@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz"
|
||||
integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==
|
||||
|
||||
socks-proxy-agent@^8.0.5:
|
||||
version "8.0.5"
|
||||
resolved "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz"
|
||||
integrity sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==
|
||||
dependencies:
|
||||
agent-base "^7.1.2"
|
||||
debug "^4.3.4"
|
||||
socks "^2.8.3"
|
||||
|
||||
socks@^2.8.3:
|
||||
version "2.8.3"
|
||||
resolved "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz"
|
||||
integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==
|
||||
dependencies:
|
||||
ip-address "^9.0.5"
|
||||
smart-buffer "^4.2.0"
|
||||
|
||||
source-map-support@0.5.13:
|
||||
version "0.5.13"
|
||||
resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz"
|
||||
|
@ -4406,6 +4442,11 @@ source-map@^0.6.0, source-map@^0.6.1:
|
|||
resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz"
|
||||
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
|
||||
|
||||
sprintf-js@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz"
|
||||
integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==
|
||||
|
||||
sprintf-js@~1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue