Add --proxy option

Carlinhos027-patch-1
freearhey 2 months ago
parent 5d7a23cffd
commit e3dc655a27

72
package-lock.json generated

@ -20,6 +20,7 @@
"@octokit/types": "^14.1.0",
"@stylistic/eslint-plugin": "^5.2.2",
"@swc/jest": "^0.2.39",
"@types/async": "^3.2.25",
"@types/cli-progress": "^3.11.6",
"@types/fs-extra": "^11.0.4",
"@types/jest": "^30.0.0",
@ -41,12 +42,10 @@
"jest-expect-message": "^1.1.3",
"lodash.uniqueid": "^4.0.1",
"m3u-linter": "^0.4.2",
"mediainfo.js": "^0.3.6",
"node-cleanup": "^2.1.2",
"socks-proxy-agent": "^8.0.5",
"tsx": "^4.20.3"
},
"devDependencies": {
"@types/async": "^3.2.25",
"mediainfo.js": "^0.3.6"
}
},
"node_modules/@alex_neo/jest-expect-message": {
@ -2746,8 +2745,7 @@
"node_modules/@types/async": {
"version": "3.2.25",
"resolved": "https://registry.npmjs.org/@types/async/-/async-3.2.25.tgz",
"integrity": "sha512-O6Th/DI18XjrL9TX8LO9F/g26qAz5vynmQqlXt/qLGrskvzCKXKc5/tATz3G2N6lM8eOf3M8/StB14FncAmocg==",
"dev": true
"integrity": "sha512-O6Th/DI18XjrL9TX8LO9F/g26qAz5vynmQqlXt/qLGrskvzCKXKc5/tATz3G2N6lM8eOf3M8/StB14FncAmocg=="
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@ -3435,6 +3433,14 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
"engines": {
"node": ">= 14"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@ -4694,7 +4700,6 @@
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.1.tgz",
"integrity": "sha512-R1QfovbPsKmosqTnPoRFiJ7CF9MLRgb53ChvMZm+r4p76/+8yKDy17qLL2PKInORy2RkZZekuK0efYgmzTkXyQ==",
"dev": true,
"engines": {
"node": ">=18"
},
@ -4989,6 +4994,14 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ip-address": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
"engines": {
"node": ">= 12"
}
},
"node_modules/iptv-playlist-parser": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/iptv-playlist-parser/-/iptv-playlist-parser-0.15.0.tgz",
@ -6373,7 +6386,6 @@
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/mediainfo.js/-/mediainfo.js-0.3.6.tgz",
"integrity": "sha512-3xVRlxwlVWIZV3z1q7pb8LzFOO7iKi/DXoRiFRZdOlrUEhPyJDaaRt0uK32yQJabArQicRBeq7cRxmdZlIBTyA==",
"dev": true,
"dependencies": {
"yargs": "^18.0.0"
},
@ -6388,7 +6400,6 @@
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"engines": {
"node": ">=12"
},
@ -6400,7 +6411,6 @@
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
"integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
"dev": true,
"dependencies": {
"string-width": "^7.2.0",
"strip-ansi": "^7.1.0",
@ -6413,14 +6423,12 @@
"node_modules/mediainfo.js/node_modules/emoji-regex": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz",
"integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==",
"dev": true
"integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg=="
},
"node_modules/mediainfo.js/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
@ -6437,7 +6445,6 @@
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
"integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
"dev": true,
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
@ -6454,7 +6461,6 @@
"version": "18.0.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
"integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
"dev": true,
"dependencies": {
"cliui": "^9.0.1",
"escalade": "^3.1.1",
@ -6471,7 +6477,6 @@
"version": "22.0.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
"integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
"dev": true,
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23"
}
@ -7132,6 +7137,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.7",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
"dependencies": {
"ip-address": "^10.0.1",
"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",

@ -74,6 +74,7 @@
"m3u-linter": "^0.4.2",
"mediainfo.js": "^0.3.6",
"node-cleanup": "^2.1.2",
"socks-proxy-agent": "^8.0.5",
"tsx": "^4.20.3"
}
}

@ -2,7 +2,7 @@ import { Logger, Storage, Collection } from '@freearhey/core'
import { ROOT_DIR, STREAMS_DIR, DATA_DIR } from '../../constants'
import { PlaylistParser, StreamTester, CliTable, DataProcessor, DataLoader } from '../../core'
import { Stream } from '../../models'
import { program } from 'commander'
import { program, OptionValues } from 'commander'
import { eachLimit } from 'async-es'
import chalk from 'chalk'
import os from 'node:os'
@ -31,10 +31,10 @@ program
.option('-x, --proxy <url>', 'Use the specified proxy')
.parse(process.argv)
const options = program.opts()
const options: OptionValues = program.opts()
const logger = new Logger()
const tester = new StreamTester()
const tester = new StreamTester({ options })
async function main() {
if (await isOffline()) {
@ -95,7 +95,7 @@ async function runTest(stream: Stream) {
const result = await tester.test(stream)
let status = ''
const errorStatusCodes = ['ENOTFOUND']
const errorStatusCodes = ['HTTP_404_NOT_FOUND']
if (result.status.ok) status = chalk.green('OK')
else if (errorStatusCodes.includes(result.status.code)) {
status = chalk.red(result.status.code)

@ -10,4 +10,5 @@ export * from './logParser'
export * from './markdown'
export * from './numberParser'
export * from './playlistParser'
export * from './proxyParser'
export * from './streamTester'

@ -0,0 +1,31 @@
import { URL } from 'node:url'
interface ProxyParserResult {
protocol: string | null
auth?: {
username?: string
password?: string
}
host: string
port: number | null
}
export class ProxyParser {
parse(_url: string): ProxyParserResult {
const parsed = new URL(_url)
const result: ProxyParserResult = {
protocol: parsed.protocol.replace(':', '') || null,
host: parsed.hostname,
port: parsed.port ? parseInt(parsed.port) : null
}
if (parsed.username || parsed.password) {
result.auth = {}
if (parsed.username) result.auth.username = parsed.username
if (parsed.password) result.auth.password = parsed.password
}
return result
}
}

@ -1,9 +1,41 @@
import { Stream } from '../models'
import { TESTING } from '../constants'
import mediaInfoFactory from 'mediainfo.js'
import axios, { AxiosInstance, AxiosProxyConfig, AxiosRequestConfig } from 'axios'
import { ProxyParser } from './proxyParser.js'
import { OptionValues } from 'commander'
import { SocksProxyAgent } from 'socks-proxy-agent'
type StreamTesterProps = {
options: OptionValues
}
export class StreamTester {
constructor() {}
client: AxiosInstance
constructor({ options }: StreamTesterProps) {
const proxyParser = new ProxyParser()
let request: AxiosRequestConfig = {
responseType: 'arraybuffer'
}
if (options.proxy !== undefined) {
const proxy = proxyParser.parse(options.proxy) as AxiosProxyConfig
if (
proxy.protocol &&
['socks', 'socks5', 'socks5h', 'socks4', 'socks4a'].includes(String(proxy.protocol))
) {
const socksProxyAgent = new SocksProxyAgent(options.proxy)
request = { ...request, ...{ httpAgent: socksProxyAgent, httpsAgent: socksProxyAgent } }
} else {
request = { ...request, ...{ proxy } }
}
}
this.client = axios.create(request)
}
async test(stream: Stream) {
if (TESTING) {
@ -12,31 +44,18 @@ export class StreamTester {
return results[stream.url as keyof typeof results]
} else {
try {
const controller = new AbortController()
const timeout = 10000
const timeoutId = setTimeout(() => controller.abort(), timeout)
const res = await fetch(stream.url, {
signal: controller.signal,
const res = await this.client(stream.url, {
signal: AbortSignal.timeout(timeout),
headers: {
'User-Agent': stream.getUserAgent() || 'Mozilla/5.0',
Referer: stream.getReferrer()
}
})
clearTimeout(timeoutId)
if (!res.ok) {
return {
status: {
ok: false,
code: `HTTP_${res.status}`
}
}
}
const mediainfo = await mediaInfoFactory({ format: 'object' })
const buffer = await res.arrayBuffer()
const buffer = await res.data
const result = await mediainfo.analyzeData(
() => buffer.byteLength,
(size: any, offset: number | undefined) =>
@ -60,8 +79,16 @@ export class StreamTester {
}
} catch (error: any) {
let code = 'UNKNOWN_ERROR'
if (error.name === 'AbortError') {
if (error.name === 'CanceledError') {
code = 'TIMEOUT'
} else if (error.name === 'AxiosError') {
if (error.response) {
const status = error.response?.status
const statusText = error.response?.statusText.toUpperCase().replace(/\s+/, '_')
code = `HTTP_${status}_${statusText}`
} else {
code = `AXIOS_${error.code}`
}
} else if (error.cause) {
const cause = error.cause as Error & { code?: string }
if (cause.code) {

Loading…
Cancel
Save