|
1 | 1 | import path from 'path';
|
2 | 2 | import {existsSync} from 'fs';
|
3 | 3 | import {base64Decode} from '../libs_drpy/crypto-util.js';
|
| 4 | +import '../utils/random-http-ua.js' |
4 | 5 | import * as drpy from '../libs/drpyS.js';
|
5 | 6 | // 创建 Agent 实例以复用 TCP 连接
|
6 | 7 | import http from 'http';
|
7 | 8 | import https from 'https';
|
| 9 | +import axios from 'axios'; |
| 10 | +import {ENV} from "../utils/env.js"; |
8 | 11 |
|
9 | 12 | // const AgentOption = { keepAlive: true, maxSockets: 100,timeout: 60000 }; // 最大连接数100,60秒定期清理空闲连接
|
10 | 13 | const AgentOption = {keepAlive: true};
|
@@ -34,9 +37,10 @@ export default (fastify, options, done) => {
|
34 | 37 | const proxyUrl = `${protocol}://${hostname}${request.url}`.split('?')[0].replace('/api/', '/proxy/') + '/?do=js';
|
35 | 38 | const publicUrl = `${protocol}://${hostname}/public/`;
|
36 | 39 | const httpUrl = `${protocol}://${hostname}/http`;
|
| 40 | + const mediaProxyUrl = `${protocol}://${hostname}/mediaProxy`; |
37 | 41 | // console.log(`proxyUrl:${proxyUrl}`);
|
38 | 42 | const env = {
|
39 |
| - proxyUrl, publicUrl, httpUrl, getProxyUrl: function () { |
| 43 | + proxyUrl, publicUrl, httpUrl, mediaProxyUrl, getProxyUrl: function () { |
40 | 44 | return proxyUrl
|
41 | 45 | }
|
42 | 46 | };
|
@@ -161,7 +165,11 @@ export default (fastify, options, done) => {
|
161 | 165 | ...(headers ? headers : {}),
|
162 | 166 | ...(rangeHeader ? {Range: rangeHeader} : {}), // 添加 Range 请求头
|
163 | 167 | }
|
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); |
165 | 173 | }
|
166 | 174 |
|
167 | 175 | // 根据媒体类型来决定如何设置字符编码
|
@@ -250,6 +258,32 @@ export default (fastify, options, done) => {
|
250 | 258 | reply.status(500).send({error: `Failed to proxy jx ${jxName}: ${error.message}`});
|
251 | 259 | }
|
252 | 260 | });
|
| 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 | + |
253 | 287 | done();
|
254 | 288 | };
|
255 | 289 |
|
@@ -314,3 +348,168 @@ function proxyStreamMedia(videoUrl, headers, reply) {
|
314 | 348 | // 必须调用 .end() 才能发送请求
|
315 | 349 | proxyRequest.end();
|
316 | 350 | }
|
| 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 | +} |
0 commit comments