├── .env ├── 2.0.1 ├── Dockerfile ├── config.json ├── docker-entrypoint.sh ├── index.js ├── package.json └── requirements.txt ├── README.md ├── docker-compose.yml └── start.sh /.env: -------------------------------------------------------------------------------- 1 | # StreamShield Proxy 配置 2 | PROXY_PORT=4994 # StreamShield 代理服务端口 3 | PROXY_HOST=http://your_domain_or_ip:4994 # StreamShield 代理访问地址(域名或IP,如果用了反向代理则改成https的链接) 4 | PROXY_TOKEN=your_token # StreamShield 安全令牌 5 | PROXY_CONFIG_PATH=/path/to/yourconfig/ # StreamShield 配置目录 6 | 7 | # OFIII 服务配置 8 | OFIII_PORT=50002 # OFIII 服务端口 9 | OFIII_CONFIG_PATH=/path/to/your/ofiiiconfig/ # OFIII 配置目录 10 | HOST_IP=your_actual_host_ip # 本机IP地址或域名(只填写IP或者host,没有http) 11 | OFIII_USER=Double001 # OFIII 用户名 12 | OFIII_EXPIRE=20350220235900 # OFIII 过期时间 13 | -------------------------------------------------------------------------------- /2.0.1/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | 3 | # 设置工作目录 4 | WORKDIR /app 5 | 6 | # 复制 AKTV_NODE-linux 可执行文件到镜像中 7 | COPY AKTV_NODE-linux /app/AKTV_NODE-linux 8 | RUN chmod +x /app/AKTV_NODE-linux 9 | 10 | # 创建 AKTV 配置文件模板 11 | RUN echo '{"ip":"${AKTV_HOST}","port":${AKTV_PORT}}' > /app/aktv_config_template.json 12 | 13 | # 复制 package.json 和 package-lock.json(如果存在) 14 | COPY package*.json ./ 15 | 16 | # 安装 Node.js 依赖 17 | RUN npm install 18 | 19 | # 复制所有源代码到容器中 20 | COPY . . 21 | 22 | # 创建启动脚本 23 | RUN echo '#!/bin/sh' > /app/start.sh && \ 24 | echo 'set -e' >> /app/start.sh && \ 25 | echo 'echo "Starting script..."' >> /app/start.sh && \ 26 | echo 'if [ "$(uname -m)" = "x86_64" ] && [ -f "/app/AKTV_NODE-linux" ] && [ "$AKTV_HOST" ] && [ "$AKTV_PORT" ]; then' >> /app/start.sh && \ 27 | echo ' echo "Running on x86_64 with AKTV config. Configuring AKTV..."' >> /app/start.sh && \ 28 | echo ' sed "s/\${AKTV_HOST}/$AKTV_HOST/g; s/\${AKTV_PORT}/$AKTV_PORT/g" /app/aktv_config_template.json > /app/config.json' >> /app/start.sh && \ 29 | echo ' echo "AKTV config:"' >> /app/start.sh && \ 30 | echo ' cat /app/config.json' >> /app/start.sh && \ 31 | echo ' echo "Starting AKTV_NODE-linux..."' >> /app/start.sh && \ 32 | echo ' /app/AKTV_NODE-linux > /app/aktv.log 2>&1 &' >> /app/start.sh && \ 33 | echo ' AKTV_PID=$!' >> /app/start.sh && \ 34 | echo ' echo "AKTV_NODE-linux started with PID $AKTV_PID"' >> /app/start.sh && \ 35 | echo ' sleep 5' >> /app/start.sh && \ 36 | echo ' if ! kill -0 $AKTV_PID 2>/dev/null; then' >> /app/start.sh && \ 37 | echo ' echo "AKTV_NODE-linux failed to start. Log:"' >> /app/start.sh && \ 38 | echo ' cat /app/aktv.log' >> /app/start.sh && \ 39 | echo ' exit 1' >> /app/start.sh && \ 40 | echo ' fi' >> /app/start.sh && \ 41 | echo 'else' >> /app/start.sh && \ 42 | echo ' echo "Not running on x86_64 or AKTV config not set. Using default AKTV source."' >> /app/start.sh && \ 43 | echo ' export AKTV_DEFAULT_SOURCE="http://aktv.top/live.m3u"' >> /app/start.sh && \ 44 | echo 'fi' >> /app/start.sh && \ 45 | echo 'echo "Starting Node.js application..."' >> /app/start.sh && \ 46 | echo 'exec node index.js' >> /app/start.sh 47 | 48 | # 确保 start.sh 有执行权限 49 | RUN chmod +x /app/start.sh 50 | 51 | # 暴露端口(使用环境变量) 52 | EXPOSE 4994 ${AKTV_PORT} 53 | 54 | # 启动命令 55 | CMD ["/bin/sh", "-c", "/app/start.sh"] 56 | -------------------------------------------------------------------------------- /2.0.1/config.json: -------------------------------------------------------------------------------- 1 | {"ip":"127.0.0.1","port":"3000"} -------------------------------------------------------------------------------- /2.0.1/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | # 创建必要的目录结构 5 | mkdir -p /app/config/proxy_hosts \ 6 | /app/config/remote_m3u/no_proxy \ 7 | /app/config/remote_m3u/proxy_needed \ 8 | /app/config/local_m3u/no_proxy \ 9 | /app/config/local_m3u/proxy_needed \ 10 | /app/config/generated 11 | 12 | # Nginx 配置 13 | mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled 14 | ln -sf /dev/stdout /var/log/nginx/access.log 15 | ln -sf /dev/stderr /var/log/nginx/error.log 16 | 17 | # 添加默认的代理 host 列表(每次都会覆盖) 18 | echo "hinet.net 19 | googlevideo.com 20 | tvb.com 21 | livednow.com 22 | orz-7.com 23 | 4gtv.tv 24 | ofiii.com 25 | youtube.com 26 | mytvsuper.com 27 | beesport.livednow.com 28 | thetvapp.to 29 | pki.goog 30 | thetv-ts.wx.sb 31 | digicert.com 32 | aktv.top 33 | v2h-cdn.com 34 | 88.212.7.11 35 | 5f22d76e220e1.streamlock.net 36 | 38.64.72.148 37 | ntdfreevcpc-tgc.cdn.hinet.net 38 | bk.msbot.us.kg 39 | live-gbnews.simplestreamcdn.com 40 | moveonjoy.com 41 | freetv.fun 42 | cloudfront.net 43 | live-hls-web-aje.getaj.net 44 | fox-foxnewsnow-samsungus.amagi.tv 45 | nmxlive.akamaized.net 46 | mediatailor.us-east-1.amazonaws.com 47 | tsv2.amagi.tv 48 | appletree-mytimeau-samsung.amagi.tv 49 | bcovlive-a.akamaihd.net 50 | rt-rtd.rttv.com 51 | amagi.tv 52 | 143.244.60.30:80 53 | 143.244.60.30" > /app/config/proxy_hosts/default.txt 54 | 55 | # 添加默认的远程 M3U 源(每次都会覆盖) 56 | echo "https://raw.githubusercontent.com/btjson/TVB/refs/heads/main/Aktv.m3u" > /app/config/remote_m3u/proxy_needed/default_sources.txt 57 | 58 | # 创建用户自定义文件(如果不存在) 59 | touch_if_not_exists() { 60 | if [ ! -f "$1" ]; then 61 | touch "$1" 62 | echo "Created empty file: $1" 63 | fi 64 | } 65 | 66 | touch_if_not_exists /app/config/proxy_hosts/user_defined.txt 67 | touch_if_not_exists /app/config/remote_m3u/no_proxy/sources.txt 68 | touch_if_not_exists /app/config/remote_m3u/proxy_needed/sources.txt 69 | 70 | # 检查本地 M3U 目录是否为空,如果为空则创建示例文件 71 | check_and_create_example() { 72 | if [ -z "$(ls -A $1)" ]; then 73 | echo "#EXTM3U 74 | #EXTINF:-1,Example Channel 75 | http://example.com/stream.m3u8" > "$1/example.m3u" 76 | echo "Created example M3U file in: $1" 77 | fi 78 | } 79 | 80 | check_and_create_example /app/config/local_m3u/no_proxy 81 | check_and_create_example /app/config/local_m3u/proxy_needed 82 | 83 | # 确保 generated 目录存在但为空 84 | rm -rf /app/config/generated/* 85 | mkdir -p /app/config/generated 86 | 87 | # 设置正确的权限 88 | chown -R node:node /app/config 89 | 90 | # 执行传递给脚本的命令(通常是启动 Node.js 应用) 91 | exec "$@" 92 | -------------------------------------------------------------------------------- /2.0.1/index.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const https = require('https'); 3 | const url = require('url'); 4 | const fetch = require('node-fetch'); 5 | const { Headers } = require('node-fetch'); 6 | const LRU = require('lru-cache'); 7 | const fs = require('fs').promises; 8 | const path = require('path'); 9 | const xml2js = require('xml2js'); 10 | const { Readable } = require('stream'); 11 | 12 | // 添加全局配置对象 13 | const globalConfig = { 14 | proxyHandler: null, 15 | tvgManager: null 16 | }; 17 | 18 | // 添加编码处理 19 | const decoder = new TextDecoder('utf-8'); 20 | const encoder = new TextEncoder(); 21 | 22 | // 定义基础配置目录常量 23 | const BASE_CONFIG_DIR = '/app/config'; 24 | 25 | // 环境变量 26 | const PORT = process.env.PORT || 4994; 27 | const CUSTOM_DOMAIN = process.env.CUSTOM_DOMAIN || 'default-domain.com'; 28 | const SECURITY_TOKEN = process.env.SECURITY_TOKEN || 'test123'; 29 | const VPS_HOST = process.env.VPS_HOST; 30 | const DEBUG = process.env.DEBUG === 'true'; 31 | const CACHE_UPDATE_INTERVAL = parseInt(process.env.CACHE_UPDATE_INTERVAL) || 600000; 32 | const USE_DEFAULT_SOURCES = process.env.USE_DEFAULT_SOURCES !== 'false'; 33 | const EPG_URL = process.env.EPG_URL || 'https://assets.livednow.com/epg.xml'; 34 | 35 | // TVG URL管理类优化 36 | class TvgUrlManager { 37 | constructor() { 38 | this.urls = new Set(); 39 | } 40 | 41 | addUrl(url) { 42 | if (url && typeof url === 'string') { 43 | url.split(',').forEach(u => this.urls.add(u.trim())); 44 | } 45 | } 46 | 47 | getUrlString() { 48 | return Array.from(this.urls).join(','); 49 | } 50 | 51 | clear() { 52 | this.urls.clear(); 53 | } 54 | } 55 | 56 | // 代理处理类优化 57 | class ProxyHandler { 58 | constructor(proxyHosts, vpsHost, securityToken) { 59 | this.proxyHosts = proxyHosts; 60 | this.vpsHost = vpsHost; 61 | this.securityToken = securityToken; 62 | } 63 | 64 | shouldProxy(url) { 65 | try { 66 | const parsedUrl = new URL(url); 67 | const hostname = parsedUrl.hostname; 68 | const fullHost = parsedUrl.host; 69 | const shouldProxy = this.proxyHosts.some(host => 70 | hostname.includes(host) || fullHost.includes(host) 71 | ); 72 | 73 | log(`检查URL是否需要代理: ${url}, 结果: ${shouldProxy}`); 74 | return shouldProxy; 75 | } catch (error) { 76 | logError(`检查代理时出错: ${error.message}`); 77 | return false; 78 | } 79 | } 80 | 81 | addProxyHeader(url, encodeUrl = false) { 82 | if (!url || !this.vpsHost || !this.securityToken) return url; 83 | if (url.includes(`${this.vpsHost}/${this.securityToken}/proxy/`)) return url; 84 | const proxyUrl = `${this.vpsHost}/${this.securityToken}/proxy/${encodeUrl ? encodeURIComponent(url) : url}`; 85 | log(`添加代理头: ${url} -> ${proxyUrl}`); 86 | return proxyUrl; 87 | } 88 | } 89 | 90 | // 内容验证器优化 91 | class ContentValidator { 92 | static validateM3UContent(content) { 93 | if (!content || typeof content !== 'string') { 94 | throw new Error('无效的 M3U 内容'); 95 | } 96 | const validatedContent = decoder.decode(encoder.encode(content.trim())); 97 | if (!validatedContent) { 98 | throw new Error('内容验证后为空'); 99 | } 100 | return validatedContent; 101 | } 102 | 103 | static validateLine(line) { 104 | if (!line || typeof line !== 'string') { 105 | return ''; 106 | } 107 | return decoder.decode(encoder.encode(line.trim())); 108 | } 109 | 110 | static validateUrl(url) { 111 | try { 112 | new URL(url); 113 | return true; 114 | } catch (error) { 115 | return false; 116 | } 117 | } 118 | } 119 | 120 | // 日志函数优化 121 | function log(...args) { 122 | if (DEBUG) { 123 | console.log('[DEBUG]', new Date().toISOString(), ...args); 124 | } 125 | } 126 | 127 | function logInfo(...args) { 128 | console.log('[INFO]', new Date().toISOString(), ...args); 129 | } 130 | 131 | function logError(...args) { 132 | console.error('[ERROR]', new Date().toISOString(), ...args); 133 | } 134 | 135 | // 配置加载优化 136 | async function loadConfig() { 137 | log('开始加载配置'); 138 | const config = { 139 | PORT, 140 | CUSTOM_DOMAIN, 141 | SECURITY_TOKEN, 142 | VPS_HOST, 143 | DEBUG, 144 | CACHE_UPDATE_INTERVAL, 145 | USE_DEFAULT_SOURCES, 146 | EPG_URL, 147 | }; 148 | 149 | // 加载代理 hosts 150 | config.PROXY_HOSTS = await loadProxyHosts(); 151 | 152 | // 初始化全局代理处理器 153 | globalConfig.proxyHandler = new ProxyHandler(config.PROXY_HOSTS, config.VPS_HOST, config.SECURITY_TOKEN); 154 | 155 | // 初始化全局 TVG 管理器 156 | globalConfig.tvgManager = new TvgUrlManager(); 157 | globalConfig.tvgManager.addUrl(config.EPG_URL); 158 | 159 | // 加载M3U源 160 | await loadM3USources(config); 161 | 162 | return config; 163 | } 164 | 165 | async function loadProxyHosts() { 166 | const defaultHosts = await readFileContent(path.join(BASE_CONFIG_DIR, 'proxy_hosts/default.txt')); 167 | try { 168 | const userHosts = await readFileContent(path.join(BASE_CONFIG_DIR, 'proxy_hosts/user_defined.txt')); 169 | return [...new Set([...defaultHosts, ...userHosts])]; // 去重 170 | } catch (error) { 171 | handleConfigError(error, 'proxy hosts'); 172 | return defaultHosts; 173 | } 174 | } 175 | 176 | async function loadM3USources(config) { 177 | config.REMOTE_M3U_NO_PROXY = []; 178 | if (config.USE_DEFAULT_SOURCES) { 179 | config.REMOTE_M3U_NO_PROXY = await loadRemoteSources(path.join(BASE_CONFIG_DIR, 'remote_m3u/no_proxy/default_sources.txt')); 180 | } 181 | 182 | try { 183 | const userSources = await loadRemoteSources(path.join(BASE_CONFIG_DIR, 'remote_m3u/no_proxy/sources.txt')); 184 | config.REMOTE_M3U_NO_PROXY = config.REMOTE_M3U_NO_PROXY.concat(userSources); 185 | } catch (error) { 186 | handleConfigError(error, 'no-proxy sources'); 187 | } 188 | 189 | try { 190 | config.REMOTE_M3U_PROXY = await loadRemoteSources(path.join(BASE_CONFIG_DIR, 'remote_m3u/proxy_needed/sources.txt')); 191 | } catch (error) { 192 | handleConfigError(error, 'proxy-needed sources');config.REMOTE_M3U_PROXY = []; 193 | } 194 | 195 | config.LOCAL_M3U_PROXY = await listFilesInDirectory(path.join(BASE_CONFIG_DIR, 'local_m3u/proxy_needed')); 196 | config.LOCAL_M3U_NO_PROXY = await listFilesInDirectory(path.join(BASE_CONFIG_DIR, 'local_m3u/no_proxy')); 197 | } 198 | function handleConfigError(error, sourceType) { 199 | if (error.code === 'ENOENT') { 200 | logInfo(`未找到${sourceType}文件`);} else { 201 | logError(`读取${sourceType}时出错: ${error.message}`); 202 | } 203 | } 204 | 205 | // 远程源加载优化 206 | async function loadRemoteSources(filePath) { 207 | log(`从以下位置加载远程源: ${filePath}`); 208 | try { 209 | const content = await readFileContent(filePath); 210 | return content 211 | .map(line => parseLine(line)) 212 | .filter(item => item !== null); 213 | } catch (error) { 214 | logError(`加载远程源时出错: ${error.message}`); 215 | return []; 216 | } 217 | } 218 | 219 | function parseLine(line) { 220 | try { 221 | if (line.includes(',http')) { 222 | return parseDirectChannel(line); 223 | } 224 | return parseSourceUrl(line); 225 | } catch (error) { 226 | logError(`解析行时出错: ${line},错误: ${error.message}`); 227 | return null; 228 | } 229 | } 230 | 231 | function parseDirectChannel(line) { 232 | const [name, url] = line.split(','); 233 | if (!ContentValidator.validateUrl(url.trim())) { 234 | throw new Error('无效的URL'); 235 | } 236 | return { 237 | url: url.trim(), 238 | name: name.trim(),isDirectChannel: true 239 | }; 240 | } 241 | 242 | function parseSourceUrl(line) { 243 | const urlObj = new URL(line); 244 | validateUrl(urlObj); 245 | return { 246 | url: urlObj.toString(), 247 | groupTitle: urlObj.searchParams.get('group-title'), 248 | removeTvLogo: urlObj.searchParams.get('remove-tv-logo') === 'true', 249 | originalUrl: line 250 | }; 251 | } 252 | 253 | function validateUrl(urlObj) { 254 | if (urlObj.hostname === '127.0.0.1' || urlObj.hostname === 'localhost') { 255 | log(`警告: 检测到本地URL: ${urlObj.toString()}`); 256 | } 257 | } 258 | 259 | // 文件处理优化 260 | async function readFileContent(filePath) { 261 | log(`读取文件内容: ${filePath}`); 262 | try { 263 | const content = await fs.readFile(filePath, 'utf8'); 264 | return content 265 | .replace(/\r\n/g, '\n') 266 | .split('\n') 267 | .filter(line => line.trim()) 268 | .map(line => ContentValidator.validateLine(line)); 269 | } catch (error) { 270 | logError(`读取文件出错 ${filePath}: ${error.message}`); 271 | return []; 272 | } 273 | } 274 | 275 | async function writeFileContent(filePath, content) { 276 | log(`写入文件内容: ${filePath}`); 277 | try { 278 | const validatedContent = ContentValidator.validateM3UContent(content); 279 | await fs.writeFile(filePath, validatedContent, 'utf8'); 280 | } catch (error) { 281 | logError(`写入文件出错 ${filePath}: ${error.message}`);throw error; 282 | } 283 | } 284 | // 文件处理相关函数需要放在配置加载之前 285 | async function listFilesInDirectory(dirPath) { 286 | log(`列出目录中的文件: ${dirPath}`); 287 | try { 288 | const files = await fs.readdir(dirPath); 289 | return files.map(file => path.join(dirPath, file)); 290 | } catch (error) { 291 | if (error.code === 'ENOENT') { 292 | logInfo(`目录不存在: ${dirPath}`);return []; 293 | } 294 | logError(`列出目录文件时出错 ${dirPath}: ${error.message}`); 295 | return []; 296 | } 297 | } 298 | 299 | // 文件读取函数 300 | async function readFileContent(filePath) { 301 | log(`读取文件内容: ${filePath}`); 302 | try { 303 | const content = await fs.readFile(filePath, 'utf8'); 304 | return content.replace(/\r\n/g, '\n') 305 | .split('\n') 306 | .filter(line => line.trim()) 307 | .map(line => ContentValidator.validateLine(line)); 308 | } catch (error) { 309 | if (error.code === 'ENOENT') { 310 | logInfo(`文件不存在: ${filePath}`); 311 | return []; 312 | } 313 | logError(`读取文件出错 ${filePath}: ${error.message}`); 314 | return []; 315 | } 316 | } 317 | 318 | // 文件写入函数 319 | async function writeFileContent(filePath, content) { 320 | log(`写入文件内容: ${filePath}`); 321 | try { 322 | // 确保目录存在 323 | const dir = path.dirname(filePath); 324 | await fs.mkdir(dir, { recursive: true }); 325 | const validatedContent = ContentValidator.validateM3UContent(content); 326 | await fs.writeFile(filePath, validatedContent, 'utf8');} catch (error) { 327 | logError(`写入文件出错 ${filePath}: ${error.message}`);throw error; 328 | } 329 | } 330 | 331 | // 内容解析和组织函数 332 | function parseAndGroupContent(content) { 333 | const lines = content.split('\n'); 334 | let result = new Map(); 335 | let currentGroup = ''; 336 | let currentInfo = ''; 337 | 338 | for (let i = 0; i < lines.length; i++) { 339 | const line = lines[i].trim(); 340 | if (!line) continue; 341 | 342 | // 跳过EXTM3U头 343 | if (line.startsWith('#EXTM3U')) continue; 344 | 345 | // 处理分组标记 346 | if (line.includes('#genre#')) { 347 | currentGroup = line.split(',')[0].trim(); 348 | if (!result.has(currentGroup)) { 349 | result.set(currentGroup, new Set()); 350 | } 351 | continue; 352 | } 353 | 354 | // 处理EXTINF行 355 | if (line.startsWith('#EXTINF:')) { 356 | currentInfo = line; 357 | // 从EXTINF行提取分组信息 358 | const groupMatch = line.match(/group-title="([^"]+)"/); 359 | if (groupMatch) { 360 | currentGroup = groupMatch[1]; 361 | if (!result.has(currentGroup)) { 362 | result.set(currentGroup, new Set()); 363 | } 364 | } 365 | } else if (line.startsWith('http') && currentInfo) { 366 | // 添加频道信息和URL到分组 367 | const channelInfo = `${currentInfo}\n${line}`; 368 | if (!result.has(currentGroup)) { 369 | result.set(currentGroup, new Set()); 370 | } 371 | result.get(currentGroup).add(channelInfo); 372 | currentInfo = ''; 373 | } 374 | } 375 | 376 | return result; 377 | } 378 | 379 | // 生成最终内容函数 380 | function generateFinalContent(groupedContent, tvgUrl) { 381 | let content = [`#EXTM3U x-tvg-url="${tvgUrl}"`]; 382 | 383 | // 按分组组织内容 384 | for (const [group, channels] of groupedContent) { 385 | // 添加分组标记 386 | if (group) { 387 | content.push(`\n#EXTINF:-1 group-title="${group}",=== ${group} ===`); 388 | } 389 | 390 | // 添加该分组的所有频道 391 | channels.forEach(channel => { 392 | content.push(channel); 393 | }); 394 | } 395 | 396 | return content.join('\n'); 397 | } 398 | 399 | // 文件处理相关函数需要放在配置加载之前 400 | async function listFilesInDirectory(dirPath) { 401 | log(`列出目录中的文件: ${dirPath}`); 402 | try { 403 | const files = await fs.readdir(dirPath); 404 | return files.map(file => path.join(dirPath, file)); 405 | } catch (error) { 406 | if (error.code === 'ENOENT') { 407 | logInfo(`目录不存在: ${dirPath}`);return []; 408 | } 409 | logError(`列出目录文件时出错 ${dirPath}: ${error.message}`); 410 | return []; 411 | } 412 | } 413 | 414 | // 文件读取函数 415 | async function readFileContent(filePath) { 416 | log(`读取文件内容: ${filePath}`); 417 | try { 418 | const content = await fs.readFile(filePath, 'utf8'); 419 | return content.replace(/\r\n/g, '\n') 420 | .split('\n') 421 | .filter(line => line.trim()) 422 | .map(line => ContentValidator.validateLine(line)); 423 | } catch (error) { 424 | if (error.code === 'ENOENT') { 425 | logInfo(`文件不存在: ${filePath}`); 426 | return []; 427 | } 428 | logError(`读取文件出错 ${filePath}: ${error.message}`); 429 | return []; 430 | } 431 | } 432 | 433 | // 文件写入函数 434 | async function writeFileContent(filePath, content) { 435 | log(`写入文件内容: ${filePath}`); 436 | try { 437 | // 确保目录存在 438 | const dir = path.dirname(filePath); 439 | await fs.mkdir(dir, { recursive: true }); 440 | const validatedContent = ContentValidator.validateM3UContent(content); 441 | await fs.writeFile(filePath, validatedContent, 'utf8');} catch (error) { 442 | logError(`写入文件出错 ${filePath}: ${error.message}`);throw error; 443 | } 444 | } 445 | 446 | 447 | // M3U 处理核心功能优化 448 | async function parseAndModifyM3U(content, sourceUrl, options = {}, proxyHosts) { 449 | log(`开始解析和修改M3U内容: ${sourceUrl}`); 450 | try { 451 | if (!content || !content.trim()) { 452 | log('内容为空,返回原始内容'); 453 | return content; 454 | } 455 | 456 | const lines = content.split('\n'); 457 | let modifiedContent = []; 458 | let currentGenre = ''; 459 | let channelNumber = 1000; 460 | let headerProcessed = false; 461 | 462 | // 处理每一行 463 | for (let i = 0; i < lines.length; i++) { 464 | const line = ContentValidator.validateLine(lines[i]); 465 | 466 | // 跳过空行 467 | if (!line) continue; 468 | 469 | // 处理EXTM3U头 470 | if (line.startsWith('#EXTM3U')) { 471 | if (!headerProcessed) { 472 | const tvgMatch = line.match(/x-tvg-url="([^"]+)"/); 473 | if (tvgMatch) { 474 | globalConfig.tvgManager.addUrl(tvgMatch[1]); 475 | } 476 | headerProcessed = true; 477 | } 478 | continue; 479 | } 480 | 481 | // 处理分类标记 482 | if (line.includes('#genre#')) { 483 | currentGenre = line.split(',')[0].trim(); 484 | log(`找到分类: ${currentGenre}`); 485 | continue; 486 | } 487 | 488 | // 处理EXTINF行 489 | if (line.startsWith('#EXTINF:')) { 490 | const modifiedLine = processExtInfLine(line, currentGenre, channelNumber++); 491 | modifiedContent.push(modifiedLine); 492 | 493 | // 处理下一行URL 494 | if (i + 1 < lines.length) { 495 | const nextLine = ContentValidator.validateLine(lines[++i]); 496 | if (ContentValidator.validateUrl(nextLine)) { 497 | modifiedContent.push(processUrl(nextLine)); 498 | } else { 499 | i--; // URL无效,回退索引 500 | } 501 | }}// 处理URL行 502 | else if (ContentValidator.validateUrl(line)) { 503 | const prevLine = lines[i - 1]?.trim(); 504 | if (!prevLine?.startsWith('#EXTINF:')) { 505 | // 为没有EXTINF的URL创建一个 506 | const channelName = prevLine || `Channel ${channelNumber}`; 507 | modifiedContent.push( 508 | `#EXTINF:-1 group-title="${currentGenre}" tvg-chno="${channelNumber++}",${channelName}` 509 | ); 510 | } 511 | modifiedContent.push(processUrl(line)); 512 | }} 513 | 514 | // 构建最终内容 515 | const finalContent = [ 516 | `#EXTM3U x-tvg-url="${globalConfig.tvgManager.getUrlString()}"`, 517 | ...modifiedContent].join('\n'); 518 | 519 | return finalContent.trim(); 520 | } catch (error) { 521 | logError(`处理M3U内容时出错: ${error.message}`); 522 | return content; // 出错时返回原始内容 523 | } 524 | } 525 | 526 | // URL处理优化 527 | function processUrl(url) { 528 | try { 529 | return globalConfig.proxyHandler.shouldProxy(url) ? 530 | globalConfig.proxyHandler.addProxyHeader(url) : url; 531 | } catch (error) { 532 | logError(`处理URL时出错: ${error.message}`); 533 | return url; 534 | } 535 | } 536 | 537 | // EXTINF行处理优化 538 | function processExtInfLine(line, genre, channelNumber) { 539 | let modifiedLine = line; 540 | 541 | // 更新或添加group-title 542 | if (genre) { 543 | modifiedLine = modifiedLine.includes('group-title=') ? 544 | modifiedLine.replace(/group-title="[^"]*"/, `group-title="${genre}"`) : 545 | modifiedLine.replace('#EXTINF:', `#EXTINF:-1 group-title="${genre}"`); 546 | } 547 | 548 | // 更新或添加tvg-chno 549 | if (!modifiedLine.includes('tvg-chno=')) { 550 | modifiedLine = modifiedLine.replace('#EXTINF:', `#EXTINF:-1 tvg-chno="${channelNumber}"`); 551 | } 552 | 553 | return modifiedLine; 554 | } 555 | // 源处理优化 556 | const processSource = async (source) => { 557 | log(`开始处理源: ${typeof source === 'string' ? source : JSON.stringify(source)}`); 558 | try { 559 | let sourceContent; 560 | let sourceUrl; 561 | let options = {}; 562 | 563 | // 处理直接频道 564 | if (source.isDirectChannel) { 565 | return `#EXTINF:-1 tvg-name="${source.name}",${source.name}\n${source.url}\n`; 566 | } 567 | 568 | // 获取源内容 569 | sourceContent = await getSourceContent(source); 570 | if (!sourceContent) { 571 | throw new Error('获取源内容失败'); 572 | } 573 | 574 | // 如果是特殊格式(#genre#),进行相应处理 575 | if (sourceContent.includes('#genre#')) { 576 | const lines = sourceContent.split('\n'); 577 | let result = []; 578 | let currentGenre = ''; 579 | 580 | for (let i = 0; i < lines.length; i++) { 581 | const line = lines[i].trim(); 582 | if (!line) continue; 583 | 584 | if (line.includes('#genre#')) { 585 | currentGenre = line.split(',')[0].trim(); 586 | continue; 587 | } 588 | 589 | if (line.includes(',http')) { 590 | const [name, url] = line.split(',').map(item => item.trim()); 591 | result.push(`#EXTINF:-1 group-title="${currentGenre}",${name}`); 592 | result.push(processUrl(url));// 使用现有的processUrl处理URL 593 | } 594 | } 595 | 596 | return result.length ? `#EXTM3U\n${result.join('\n')}` : ''; 597 | } 598 | 599 | // 标准M3U格式,使用现有的parseAndModifyM3U处理 600 | return await parseAndModifyM3U( 601 | sourceContent, 602 | sourceUrl, 603 | options, 604 | config.PROXY_HOSTS 605 | ); 606 | 607 | } catch (error) { 608 | logError(`处理源时出错: ${error.message}`); 609 | logError(`错误堆栈: ${error.stack}`); 610 | return ''; 611 | } 612 | }; 613 | 614 | // 源内容获取优化 615 | async function getSourceContent(source) { 616 | if (typeof source === 'string') { 617 | // 本地文件 618 | const content = await fs.readFile(source, 'utf8'); 619 | log(`读取本地文件: ${source}, 内容长度: ${content.length}`); 620 | return content; 621 | } else if (typeof source === 'object' && source.url) { 622 | // 远程源 623 | const response = await fetchWithTimeout(source.url, {},30000); 624 | if (!response.ok) { 625 | throw new Error(`HTTP错误! 状态: ${response.status}`); 626 | } 627 | const content = await response.text(); 628 | log(`获取远程内容完成: ${source.url}, 内容长度: ${content.length}`); 629 | return content; 630 | } 631 | return null; 632 | } 633 | 634 | // 网络请求处理优化 635 | async function fetchWithTimeout(url, options = {}, timeout = 30000) { 636 | log(`开始获取URL内容(带超时): ${url}`); 637 | const controller = new AbortController(); 638 | const id = setTimeout(() => controller.abort(), timeout); 639 | 640 | const httpsAgent = new https.Agent({ 641 | rejectUnauthorized: false 642 | }); 643 | 644 | try { 645 | const response = await fetch(url, { 646 | ...options, 647 | signal: controller.signal, 648 | agent: url.startsWith('https') ? httpsAgent : null, 649 | redirect: 'follow' 650 | }); 651 | clearTimeout(id); 652 | log(`URL内容获取完成: ${url}, 状态: ${response.status}`); 653 | return response; 654 | } catch (error) { 655 | clearTimeout(id); 656 | if (error.name === 'AbortError') { 657 | throw new Error(`请求超时: ${url}`); 658 | } 659 | throw error; 660 | } 661 | } 662 | 663 | // 请求处理器优化 664 | class RequestHandler { 665 | constructor(config) { 666 | this.config = config; 667 | } 668 | 669 | async handleRequest(req, res) { 670 | const parsedUrl = url.parse(req.url, true); 671 | const pathParts = parsedUrl.pathname.split('/').filter(part => part); 672 | 673 | log(`收到请求: ${req.method} ${req.url}`); 674 | try { 675 | if (!this.validateToken(pathParts[0])) { 676 | throw new Error('无效的访问令牌'); 677 | } 678 | 679 | switch(pathParts[1]) { 680 | case 'health': 681 | return this.handleHealthCheck(res); 682 | case 'proxy': 683 | return await this.handleProxy(req, res); 684 | case 'all.m3u':default: 685 | return await this.handleM3UList(req, res); 686 | } 687 | } catch (error) { 688 | this.handleError(error, res); 689 | } 690 | } 691 | 692 | validateToken(token) { 693 | return token === this.config.SECURITY_TOKEN; 694 | } 695 | 696 | handleHealthCheck(res) { 697 | res.writeHead(200, { 'Content-Type': 'application/json' }); 698 | res.end(JSON.stringify({ 699 | status: 'OK', 700 | timestamp: new Date().toISOString() 701 | })); 702 | } 703 | 704 | async handleM3UList(req, res) { 705 | try { 706 | let content = m3uCache.get('all'); 707 | if (!content) { 708 | content = await generateAggregatedM3U(this.config); 709 | m3uCache.set('all', content); 710 | } 711 | res.writeHead(200, { 'Content-Type': 'application/x-mpegURL' }); 712 | res.end(content); 713 | } catch (error) { 714 | this.handleError(error, res); 715 | } 716 | } 717 | 718 | async handleProxy(req, res) { 719 | try { 720 | const urlParts = req.url.split(`/${this.config.SECURITY_TOKEN}/proxy/`); 721 | if (urlParts.length < 2) { 722 | throw new Error('无效的代理请求'); 723 | } 724 | 725 | const targetUrl = decodeURIComponent(urlParts[1]); 726 | const response = await fetchWithTimeout(targetUrl, { 727 | method: req.method, 728 | headers: req.headers, 729 | body: req.method !== 'GET' && req.method !== 'HEAD' ? req : undefined 730 | }); 731 | 732 | res.writeHead(response.status, response.headers.raw()); 733 | response.body.pipe(res); 734 | } catch (error) { 735 | this.handleError(error, res); 736 | } 737 | } 738 | 739 | handleError(error, res) { 740 | logError(`请求处理错误: ${error.message}`); 741 | const status = error.message.includes('无效的访问令牌') ? 403 : 500; 742 | res.writeHead(status, { 'Content-Type': 'text/plain' }); 743 | res.end(error.message); 744 | } 745 | } 746 | 747 | // 优化后的 generateAggregatedM3U 函数 748 | async function generateAggregatedM3U(config) { 749 | logInfo('开始生成聚合M3U内容...'); 750 | const tvgManager = new TvgUrlManager(); 751 | tvgManager.addUrl(config.EPG_URL); 752 | const groupedContent = new Map(); 753 | 754 | // 处理所有来源 755 | const sources = [ 756 | ...config.LOCAL_M3U_PROXY, 757 | ...config.LOCAL_M3U_NO_PROXY, 758 | ...config.REMOTE_M3U_PROXY, 759 | ...config.REMOTE_M3U_NO_PROXY 760 | ]; 761 | 762 | log(`开始处理 ${sources.length} 个源`); 763 | for (const source of sources) { 764 | try { 765 | log(`处理源: ${typeof source === 'string' ? source : JSON.stringify(source)}`); 766 | const content = await processSource(source); 767 | if (content && content.trim()) { 768 | // 提取 TVG URLs 769 | const tvgMatch = content.match(/x-tvg-url="([^"]+)"/); 770 | if (tvgMatch) { 771 | tvgManager.addUrl(tvgMatch[1]); 772 | } 773 | 774 | // 解析并按分组存储内容 775 | const parsedContent = parseAndGroupContent(content); 776 | for (const [group, channels] of parsedContent) { 777 | if (!groupedContent.has(group)) { 778 | groupedContent.set(group, new Set()); 779 | } 780 | channels.forEach(channel => { 781 | groupedContent.get(group).add(channel); 782 | }); 783 | } 784 | 785 | log(`成功添加源内容到分组`); 786 | } else { 787 | log(`源${JSON.stringify(source)} 返回空内容`); 788 | } 789 | } catch (error) { 790 | logError(`处理源时出错${JSON.stringify(source)}: ${error.message}`); 791 | } 792 | } 793 | 794 | // 生成最终内容 795 | const finalContent = generateFinalContent(groupedContent, tvgManager.getUrlString()); 796 | 797 | // 确保目录存在并写入文件 798 | try { 799 | const outputDir = path.join(BASE_CONFIG_DIR, 'generated'); 800 | await fs.mkdir(outputDir, { recursive: true }); 801 | await writeFileContent( 802 | path.join(outputDir,'all.m3u'), 803 | finalContent 804 | ); 805 | log(`内容已写入文件,总长度: ${finalContent.length}`); 806 | } catch (error) { 807 | logError(`写入聚合内容时出错: ${error.message}`); 808 | } 809 | 810 | return finalContent; 811 | } 812 | 813 | 814 | // 缓存优化 815 | const m3uCache = new LRU({ 816 | max: 100, 817 | maxAge: 1000 * 60 * 5 818 | }); 819 | 820 | // 主程序入口优化 821 | async function main() { 822 | logInfo('启动 StreamShield Proxy...'); 823 | try { 824 | // 加载配置 825 | config = await loadConfig(); 826 | 827 | // 初始化缓存 828 | const initialContent = await generateAggregatedM3U(config); 829 | m3uCache.set('all', initialContent); 830 | 831 | // 创建请求处理器 832 | const requestHandler = new RequestHandler(config); 833 | 834 | // 启动服务器 835 | const server = http.createServer((req, res) => requestHandler.handleRequest(req, res)); 836 | server.listen(config.PORT, () => { 837 | logInfo(`StreamShield Proxy v2.0.0 运行在端口 ${config.PORT}`); 838 | }); 839 | 840 | // 设置定期更新 841 | setInterval(async () => { 842 | try { 843 | const updatedContent = await generateAggregatedM3U(config); 844 | m3uCache.set('all', updatedContent); 845 | logInfo(`定期更新完成, 新内容长度: ${updatedContent.length}`); 846 | } catch (error) { 847 | logError(`定期更新错误: ${error.message}`); 848 | } 849 | }, config.CACHE_UPDATE_INTERVAL);} catch (error) { 850 | logError(`初始化错误: ${error.message}`);process.exit(1); 851 | } 852 | } 853 | 854 | // 启动应用 855 | main().catch(error => { 856 | logError(`启动错误: ${error.message}`); 857 | process.exit(1); 858 | });/ 859 | -------------------------------------------------------------------------------- /2.0.1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "streamshield-proxy", 3 | "version": "2.0.0", 4 | "description": "Optimized StreamShield Proxy for fast TV channel switching with enhanced features", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "start:dev": "NODE_ENV=development node --no-buffering index.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "dependencies": { 12 | "xml2js": "^0.4.23", 13 | "node-fetch": "^2.6.9", 14 | "lru-cache": "^6.0.0", 15 | "xmldom": "^0.6.0", 16 | "dotenv": "^10.0.0" 17 | }, 18 | "devDependencies": { 19 | "nodemon": "^2.0.22" 20 | }, 21 | "engines": { 22 | "node": ">=14.0.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /2.0.1/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StreamShield Proxy: 多源流媒体聚合代理工具 2 | 3 | ## 简介 4 | StreamShield Proxy 2.0.0 是一个强大的多源流媒体聚合代理工具,旨在解决因 IP 限制而无法直接播放各种流媒体内容的问题。它支持多个VPS部署,形成CDN网络,为用户提供流畅的流媒体播放体验。 5 | 6 | ## 核心功能 7 | - **多源聚合**:支持本地和远端url等多个流媒体源聚合 8 | - **智能代理**:自动处理需要代理的内容 9 | - **内容定制**:支持移除tv-logo、修改group-title等自定义操作 10 | - **安全加固**:集成安全token机制 11 | - **灵活配置**:通过环境变量实现灵活配置 12 | - **高效缓存**:使用LRU缓存优化性能 13 | - **跨平台支持**:支持arm64和amd64架构 14 | 15 | ## 新版本特性 (2.0.0) 16 | - 全新的多源聚合机制 17 | - 优化的缓存系统,提高响应速度 18 | - 增强的错误处理和日志记录 19 | - 支持对特定源的内容进行自定义修改 20 | 21 | ## 快速开始 22 | 23 | ### 使用Docker Compose部署 24 | 前置调教先安装完成docker和docker compose 25 | 26 | 1. 克隆仓库: 27 | ```bash 28 | git clone https://github.com/pppyyyccc/streamshield-proxy.git 29 | cd streamshield-proxy 30 | ``` 31 | 2. 编辑 `.env` 文件,设置您的个人配置。 32 | ```bash 33 | nano .env 34 | ``` 35 | 3. 启动服务: 36 | ```bash 37 | chmod +x start.sh 38 | ./start.sh 39 | ``` 40 | 41 | 4. 更新服务: 42 | ```bash 43 | docker compose pull 44 | docker compose down 45 | docker compose up -d 46 | ``` 47 | 48 | ### 使用Docker部署 49 | ```bash 50 | docker run -d -p 4994:4994 --name streamshield-proxy \ 51 | -e VPS_HOST="http://your-vps-ip/host:4994" \ 52 | -e SECURITY_TOKEN="your_security_token" \ 53 | -e DEBUG="true" \ 54 | --restart always \ 55 | ppyycc/streamshield-proxy:latest 56 | ``` 57 | ## 环境变量配置 58 | 59 | | 变量 | 描述 | 60 | |-----------------------|-------------------------------------| 61 | | `VPS_HOST` | 您的VPS主机地址 | 62 | | `SECURITY_TOKEN` | 安全访问令牌 | 63 | | `DEBUG` | 是否开启调试模式 | 64 | | `USE_DEFAULT_SOURCES=false`|是否启用内部的默认AKTV源 | 65 | 66 | ## 本地持久化文件夹结构 67 | 68 | 69 | ```bash 70 | config/ 71 | ├── proxy_hosts/ 72 | │ ├── default.txt # 默认代理 hosts 列表 (默认创建,每次升级会覆盖) 73 | │ └── user_defined.txt # 用户自定义代理 hosts 列表 (预创建,用户编辑,升级不会覆盖) 74 | ├── remote_m3u/ 75 | │ ├── no_proxy/ 76 | │ │ ├── default_sources.txt # 默认非代理 M3U 源列表 (默认创建,每次升级会覆盖) 77 | │ │ └── sources.txt # 用户自定义非代理 M3U 源列表 (预创建,用户编辑,升级不会覆盖) 78 | │ └── proxy_needed/ 79 | │ └── sources.txt # 用户自定义代理 M3U 源列表 (预创建,用户编辑,升级不会覆盖) 80 | ├── local_m3u/ 81 | │ ├── no_proxy/ 82 | │ │ └── user_m3u.m3u # 用户本地非代理 M3U 文件 (用户手动添加,文件名随便取,后缀支持m3u和txt) 83 | │ └── proxy_needed/ 84 | │ └── user_m3u.m3u # 用户本地代理 M3U 文件 (用户手动添加,文件名随便取,后缀支持m3u和txt) 85 | └── generated/ 86 | └── all.m3u # 聚合的 M3U 文件 (程序自动生成) 87 | ``` 88 | 89 | ## 使用说明 90 | 91 | 1. 用户可以在 \`user_defined.txt\` 中添加自定义的需要代理的hosts,一行一条。 92 | 2. 在 \`remote_m3u/no_proxy/sources.txt\` 和 \`remote_m3u/proxy_needed/sources.txt\` 中添加远程 M3U 源。 93 | 3. 用户可以在 \`local_m3u\` 文件夹中添加本地 M3U 文件。 94 | 4. \`generated/all.m3u\` 是程序自动生成的聚合 M3U 文件,请勿手动编辑。 95 | 5. 别的vps上生成的播放链接放在主vps的\`remote_m3u/no_proxy/sources.txt内便能成为cdn链接节点。 96 | 97 | 98 | 注意:除了 \`default.txt\` 和 \`default_sources.txt\`,其他文件在升级时不会被覆盖。" > 99 | 100 | ## 访问 101 | 在您的流媒体播放器中使用以下格式的URL: 102 | http://[您的服务器IP或域名]:[端口]/[SECURITY_TOKEN],例如http://100.100.100.100:4994/your_security_token 103 | 104 | ## 注意事项 105 | - 强烈建议使用HTTPS反向代理以增强安全性 106 | - 请定期更新您的安全令牌 107 | - 确保所有需要的端口都已开放 108 | 109 | ## 反馈与支持 110 | 如果您在使用过程中遇到任何问题或有任何建议,请在GitHub上提交issue。 111 | 112 | ## 贡献 113 | 欢迎提交Pull Request或提出功能建议。 114 | 115 | ## 许可证 116 | 本项目遵循MIT开源协议。 117 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | streamshield-proxy: 5 | image: ppyycc/streamshield-proxy:latest 6 | container_name: streamshield-proxy 7 | restart: always 8 | ports: 9 | - "${PROXY_PORT}:4994" 10 | volumes: 11 | - ${PROXY_CONFIG_PATH}:/app/config 12 | environment: 13 | - DEBUG=true 14 | - VPS_HOST=${PROXY_HOST} 15 | - SECURITY_TOKEN=${PROXY_TOKEN} 16 | logging: 17 | driver: json-file 18 | options: 19 | max-size: "10m" 20 | max-file: "5" 21 | 22 | doube-ofiii: 23 | image: doubebly/doube-ofiii:1.0.5 24 | container_name: doube-ofiii 25 | restart: always 26 | ports: 27 | - "${OFIII_PORT}:5000" 28 | volumes: 29 | - ${OFIII_CONFIG_PATH}:/app/config 30 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 检查是否已经有 .env 文件 3 | if [ ! -f .env ]; then 4 | echo "未找到 .env 文件,请先复制 .env.example 并修改配置" 5 | exit 1 6 | fi 7 | # 加载环境变量 8 | source .env 9 | # 创建 OFIII 配置 10 | mkdir -p ${OFIII_CONFIG_PATH} 11 | cat > ${OFIII_CONFIG_PATH}/users.json << EOL 12 | { 13 | "${OFIII_USER}": "${OFIII_EXPIRE}" 14 | } 15 | EOL 16 | # 设置 OFIII 配置权限 17 | chmod -R 755 ${OFIII_CONFIG_PATH} 18 | # 启动服务 19 | docker-compose up -d 20 | # 等待服务启动 21 | sleep 5 22 | 23 | GLOBAL_M3U_URL="https://raw.githubusercontent.com/YueChan/Live/refs/heads/main/Global.m3u" 24 | OFIII_SOURCE_URL="http://${HOST_IP}:${OFIII_PORT}/Sub?type=txt&token=${OFIII_USER}" 25 | 26 | # 添加 Global.m3u 源到 proxy_needed/sources.txt 27 | if ! grep -q "${GLOBAL_M3U_URL}" "${PROXY_CONFIG_PATH}/remote_m3u/proxy_needed/sources.txt"; then 28 | echo "${GLOBAL_M3U_URL}" >> "${PROXY_CONFIG_PATH}/remote_m3u/proxy_needed/sources.txt" 29 | echo -e "\033[36m已添加 Global.m3u 到代理源列表\033[0m" 30 | else 31 | echo -e "\033[33mGlobal.m3u 已存在于代理源列表,跳过添加\033[0m" 32 | fi 33 | 34 | # 添加 OFIII 源到 no_proxy sources 35 | if ! grep -q "${OFIII_SOURCE_URL}" "${PROXY_CONFIG_PATH}/remote_m3u/no_proxy/sources.txt"; then 36 | echo "${OFIII_SOURCE_URL}" >> "${PROXY_CONFIG_PATH}/remote_m3u/no_proxy/sources.txt" 37 | echo -e "\033[36m已添加 OFIII 源到非代理源列表\033[0m" 38 | else 39 | echo -e "\033[33mOFIII 源已存在于非代理源列表,跳过添加\033[0m" 40 | fi 41 | 42 | 43 | # 以优雅的颜色显示信息 44 | echo -e "\033[36m服务已启动!\033[0m" 45 | echo -e "\033[36mStreamShield 代理访问地址: ${PROXY_HOST}/${PROXY_TOKEN}\033[0m" 46 | 47 | --------------------------------------------------------------------------------