Skip to content

Commit b66bbb8

Browse files
committed
update:本地代理支持流代理
1 parent 4879b3f commit b66bbb8

18 files changed

+810
-199
lines changed

Diff for: README.md

+4-5
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,12 @@ nodejs作为服务端的drpy实现。全面升级异步写法
1313

1414
## 更新记录
1515

16-
### 20241227
16+
### 20241228
1717

18-
更新至V1.0.23
18+
更新至V1.0.24
1919

20-
1. 支持推送
21-
2. 优化几个盘的源细节问题
22-
3. 设置中心夸克扫码支持
20+
1. 本地代理支持多线程流代理
21+
2. 设置中心功能优化
2322

2423
[点此查看完整更新记录](docs/updateRecord.md)
2524

Diff for: controllers/api.js

+201-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import path from 'path';
22
import {existsSync} from 'fs';
33
import {base64Decode} from '../libs_drpy/crypto-util.js';
4+
import '../utils/random-http-ua.js'
45
import * as drpy from '../libs/drpyS.js';
56
// 创建 Agent 实例以复用 TCP 连接
67
import http from 'http';
78
import https from 'https';
9+
import axios from 'axios';
10+
import {ENV} from "../utils/env.js";
811

912
// const AgentOption = { keepAlive: true, maxSockets: 100,timeout: 60000 }; // 最大连接数100,60秒定期清理空闲连接
1013
const AgentOption = {keepAlive: true};
@@ -34,9 +37,10 @@ export default (fastify, options, done) => {
3437
const proxyUrl = `${protocol}://${hostname}${request.url}`.split('?')[0].replace('/api/', '/proxy/') + '/?do=js';
3538
const publicUrl = `${protocol}://${hostname}/public/`;
3639
const httpUrl = `${protocol}://${hostname}/http`;
40+
const mediaProxyUrl = `${protocol}://${hostname}/mediaProxy`;
3741
// console.log(`proxyUrl:${proxyUrl}`);
3842
const env = {
39-
proxyUrl, publicUrl, httpUrl, getProxyUrl: function () {
43+
proxyUrl, publicUrl, httpUrl, mediaProxyUrl, getProxyUrl: function () {
4044
return proxyUrl
4145
}
4246
};
@@ -161,7 +165,11 @@ export default (fastify, options, done) => {
161165
...(headers ? headers : {}),
162166
...(rangeHeader ? {Range: rangeHeader} : {}), // 添加 Range 请求头
163167
}
164-
return proxyStreamMedia(content, new_headers, reply); // 走 流式代理
168+
// return proxyStreamMediaMulti(content, new_headers, request, reply); // 走 流式代理
169+
// 将查询参数构建为目标 URL
170+
const redirectUrl = `/mediaProxy?url=${encodeURIComponent(content)}&headers=${encodeURIComponent(new_headers)}&thread=${ENV.get('thread') || 1}`;
171+
// 执行重定向
172+
return reply.redirect(redirectUrl);
165173
}
166174

167175
// 根据媒体类型来决定如何设置字符编码
@@ -250,6 +258,32 @@ export default (fastify, options, done) => {
250258
reply.status(500).send({error: `Failed to proxy jx ${jxName}: ${error.message}`});
251259
}
252260
});
261+
262+
263+
// 用法同 https://github.com/Zhu-zi-a/mediaProxy
264+
fastify.all('/mediaProxy', async (request, reply) => {
265+
const {thread = 1, form = 'urlcode', url, header, size = '128K', randUa = 0} = request.query;
266+
267+
// Check if the URL parameter is missing
268+
if (!url) {
269+
return reply.code(400).send({error: 'Missing required parameter: url'});
270+
}
271+
272+
try {
273+
// Decode URL and headers based on the form type
274+
const decodedUrl = form === 'base64' ? base64Decode(url) : decodeURIComponent(url);
275+
const decodedHeader = header
276+
? JSON.parse(form === 'base64' ? base64Decode(header) : decodeURIComponent(header))
277+
: {};
278+
279+
// Call the proxy function, passing the decoded URL and headers
280+
return await proxyStreamMediaMulti(decodedUrl, decodedHeader, request, reply, thread, size, randUa);
281+
} catch (error) {
282+
fastify.log.error(error);
283+
reply.code(500).send({error: error.message});
284+
}
285+
});
286+
253287
done();
254288
};
255289

@@ -314,3 +348,168 @@ function proxyStreamMedia(videoUrl, headers, reply) {
314348
// 必须调用 .end() 才能发送请求
315349
proxyRequest.end();
316350
}
351+
352+
353+
// Helper function for range-based chunk downloading
354+
async function fetchStream(url, headers, start, end, randUa) {
355+
try {
356+
const response = await axios.get(url, {
357+
headers: {
358+
...headers,
359+
...randUa ? {'User-Agent': randomUa.generateUa(1, {device: ['pc']})} : {},
360+
Range: `bytes=${start}-${end}`,
361+
},
362+
responseType: 'stream',
363+
});
364+
return {stream: response.data, headers: response.headers};
365+
} catch (error) {
366+
throw new Error(`Failed to fetch range ${start}-${end}: ${error.message}`);
367+
}
368+
}
369+
370+
async function proxyStreamMediaMulti(mediaUrl, reqHeaders, request, reply, thread, size, randUa = 0) {
371+
try {
372+
// console.log('mediaUrl:', mediaUrl);
373+
// console.log('reqHeaders:', reqHeaders);
374+
// console.log('randUa:', randUa);
375+
let initialHeaders;
376+
let contentLength;
377+
378+
// First attempt with HEAD request to get the full content length
379+
/*
380+
try {
381+
const response = await axios.head(mediaUrl, {
382+
headers: Object.assign({}, reqHeaders, {'User-Agent': randomUa.generateUa()})
383+
});
384+
initialHeaders = response.headers;
385+
contentLength = parseInt(initialHeaders['content-length'], 10);
386+
387+
} catch (error) {
388+
// If HEAD fails, fallback to GET request without Range to get the full content length
389+
const response = await axios.get(mediaUrl, {
390+
headers: Object.assign({}, reqHeaders, {'User-Agent': randomUa.generateUa()}),
391+
responseType: 'stream'
392+
});
393+
initialHeaders = response.headers;
394+
contentLength = parseInt(initialHeaders['content-length'], 10);
395+
// 立即销毁流,防止下载文件内容
396+
response.data.destroy();
397+
}
398+
*/
399+
const randHeaders = randUa ? Object.assign({}, reqHeaders, {'User-Agent': randomUa.generateUa(1, {device: ['pc']})}) : reqHeaders;
400+
// console.log('randHeaders:', randHeaders);
401+
const response = await axios.get(mediaUrl, {
402+
headers: randHeaders,
403+
// headers: reqHeaders,
404+
responseType: 'stream'
405+
});
406+
initialHeaders = response.headers;
407+
contentLength = parseInt(initialHeaders['content-length'], 10);
408+
// 立即销毁流,防止下载文件内容
409+
response.data.destroy();
410+
// console.log('contentLength:', contentLength);
411+
412+
// Ensure that we got a valid content length
413+
if (!contentLength) {
414+
throw new Error('Failed to get the total content length.');
415+
}
416+
417+
// Set response headers based on the target URL headers, excluding certain ones
418+
Object.entries(initialHeaders).forEach(([key, value]) => {
419+
if (!['transfer-encoding', 'content-length'].includes(key.toLowerCase())) {
420+
reply.raw.setHeader(key, value);
421+
}
422+
});
423+
424+
reply.raw.setHeader('Accept-Ranges', 'bytes');
425+
426+
// Parse the range from the request or default to 'bytes=0-'
427+
const range = request.headers.range || 'bytes=0-';
428+
const [startStr, endStr] = range.replace(/bytes=/, '').split('-');
429+
let start = parseInt(startStr, 10);
430+
let end = endStr ? parseInt(endStr, 10) : contentLength - 1;
431+
432+
// Ensure that the range is within the file's length
433+
if (start < 0) start = 0;
434+
if (end >= contentLength) end = contentLength - 1;
435+
436+
if (start >= end) {
437+
reply.code(416).header('Content-Range', `bytes */${contentLength}`).send();
438+
return;
439+
}
440+
441+
// Set Content-Range and Content-Length headers before any data is sent
442+
reply.raw.setHeader('Content-Range', `bytes ${start}-${end}/${contentLength}`);
443+
reply.raw.setHeader('Content-Length', end - start + 1);
444+
reply.raw.writeHead(206); // Ensure headers are sent before streaming
445+
446+
// Calculate the chunk size based on the provided size parameter (e.g., '128K', '1M')
447+
const chunkSize = sizeToBytes(size);
448+
449+
// Calculate the total number of chunks
450+
const totalChunks = Math.ceil((end - start + 1) / chunkSize);
451+
452+
// Limit the number of concurrent threads to the provided 'thread' value
453+
const threadCount = Math.min(thread, totalChunks);
454+
455+
// Split the range into multiple sub-ranges based on the number of threads
456+
const ranges = Array.from({length: threadCount}, (_, i) => {
457+
const subStart = start + i * (end - start + 1) / threadCount;
458+
const subEnd = Math.min(subStart + (end - start + 1) / threadCount - 1, end);
459+
return {start: Math.floor(subStart), end: Math.floor(subEnd)};
460+
});
461+
462+
// Fetch the streams concurrently for the calculated ranges
463+
const fetchChunks = ranges.map(range => fetchStream(mediaUrl, randHeaders, range.start, range.end, randUa));
464+
const streams = await Promise.all(fetchChunks);
465+
466+
// Send the chunks to the client in order
467+
let cnt = 0;
468+
for (const {stream} of streams) {
469+
cnt += 1;
470+
// Handle streaming and stop when client disconnects
471+
const onAbort = () => {
472+
console.log('Client aborted the connection');
473+
stream.destroy(); // Destroy the stream if client disconnects
474+
};
475+
476+
// Listen to the 'aborted' event on the request object
477+
request.raw.on('aborted', onAbort);
478+
479+
try {
480+
// console.log(`第${cnt}段流代理开始运行...`);
481+
for await (const chunk of stream) {
482+
if (request.raw.aborted) {
483+
// console.log(`第${cnt}段流代理结束运行`);
484+
break; // Stop streaming if the client aborted
485+
}
486+
reply.raw.write(chunk);
487+
}
488+
} catch (error) {
489+
console.error('[proxyStreamMediaMulti] error during streaming:', error.message);
490+
} finally {
491+
request.raw.removeListener('aborted', onAbort); // Clean up event listener
492+
}
493+
}
494+
495+
reply.raw.end(); // End the response once the streaming is done
496+
497+
} catch (error) {
498+
console.error('[proxyStreamMediaMulti] error:', error.message);
499+
if (!reply.sent) {
500+
reply.code(500).send({error: error.message});
501+
}
502+
}
503+
}
504+
505+
// Helper function to convert size string (e.g., '128K', '1M') to bytes
506+
function sizeToBytes(size) {
507+
const sizeMap = {
508+
K: 1024,
509+
M: 1024 * 1024,
510+
G: 1024 * 1024 * 1024
511+
};
512+
const unit = size[size.length - 1].toUpperCase();
513+
const number = parseInt(size, 10);
514+
return number * (sizeMap[unit] || 1);
515+
}

Diff for: controllers/config.js

+6
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,14 @@ async function generateSiteJSON(jsDir, requestHost, sub, subFilePath) {
1616
valid_files = valid_files.filter(it => !(new RegExp(sub.reg || '.*')).test(it));
1717
}
1818
let sort_file = path.join(path.dirname(subFilePath), `./order_common.html`);
19+
if (!existsSync(sort_file)) {
20+
sort_file = path.join(path.dirname(subFilePath), `./order_common.example.html`);
21+
}
1922
if (sub.sort) {
2023
sort_file = path.join(path.dirname(subFilePath), `./${sub.sort}.html`);
24+
if (!existsSync(sort_file)) {
25+
sort_file = path.join(path.dirname(subFilePath), `./${sub.sort}.example.html`);
26+
}
2127
}
2228
if (existsSync(sort_file)) {
2329
console.log('sort_file:', sort_file);

0 commit comments

Comments
 (0)