import fs from "node:fs"; import axios from 'axios' import child_process from 'node:child_process' import util from "util"; import path from "path"; import { BILI_BVID_TO_CID, BILI_DYNAMIC, BILI_PLAY_STREAM, BILI_SCAN_CODE_DETECT, BILI_SCAN_CODE_GENERATE, BILI_VIDEO_INFO } from "../constants/tools.js"; import { mkdirIfNotExists } from "./file.js"; import { exec, spawn } from 'child_process'; import qrcode from "qrcode" export const BILI_HEADER = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36', referer: 'https://www.bilibili.com', } /** * 下载单个bili文件 * @param url 下载链接 * @param fullFileName 文件名 * @param progressCallback 下载进度 * @param biliDownloadMethod 下载方式 {BILI_DOWNLOAD_METHOD} * @param videoDownloadConcurrency 视频下载并发 * @returns {Promise} */ export async function downloadBFile(url, fullFileName, progressCallback, biliDownloadMethod = 0, videoDownloadConcurrency = 1) { if (biliDownloadMethod === 0) { // 原生 return normalDownloadBFile(url, fullFileName, progressCallback); } if (biliDownloadMethod === 1) { // 性能 Aria2 return aria2DownloadBFile(url, fullFileName, progressCallback, videoDownloadConcurrency); } else { // 轻量 return axelDownloadBFile(url, fullFileName, progressCallback, videoDownloadConcurrency); } } /** * 正常下载 * @param url * @param fullFileName * @param progressCallback * @returns {Promise<{fullFileName: string, totalLen: number}>} */ async function normalDownloadBFile(url, fullFileName, progressCallback) { return axios .get(url, { responseType: 'stream', headers: { ...BILI_HEADER }, }) .then(({ data, headers }) => { let currentLen = 0; const totalLen = headers['content-length']; return new Promise((resolve, reject) => { data.on('data', ({ length }) => { currentLen += length; progressCallback?.(currentLen / totalLen); }); data.pipe( fs.createWriteStream(fullFileName).on('finish', () => { resolve({ fullFileName, totalLen, }); }), ); }); }); } /** * 使用Aria2下载 * @param url * @param fullFileName * @param progressCallback * @param videoDownloadConcurrency * @returns {Promise<{fullFileName: string, totalLen: number}>} */ async function aria2DownloadBFile(url, fullFileName, progressCallback, videoDownloadConcurrency) { return new Promise((resolve, reject) => { logger.info(`[R插件][Aria2下载] 正在使用Aria2进行下载!`); // 构建aria2c命令 const aria2cArgs = [ '--file-allocation=none', // 避免预分配文件空间 '--continue', // 启用暂停支持 '-o', fullFileName, // 指定输出文件名 '--console-log-level=warn', // 减少日志 verbosity '--download-result=hide', // 隐藏下载结果概要 '--header', 'referer: https://www.bilibili.com', // 添加自定义标头 `--max-connection-per-server=${ videoDownloadConcurrency }`, // 每个服务器的最大连接数 `--split=${ videoDownloadConcurrency }`, // 分成 6 个部分进行下载 url ]; // Spawn aria2c 进程 const aria2c = spawn('aria2c', aria2cArgs); let totalLen = 0; let currentLen = 0; // 处理aria2c标准输出数据以捕获进度(可选) aria2c.stdout.on('data', (data) => { const output = data.toString(); const match = output.match(/\((\d+)\s*\/\s*(\d+)\)/); if (match) { currentLen = parseInt(match[1], 10); totalLen = parseInt(match[2], 10); progressCallback?.(currentLen / totalLen); } }); // 处理aria2c的stderr以捕获错误 aria2c.stderr.on('data', (data) => { console.error(`aria2c error: ${ data }`); }); // 处理进程退出 aria2c.on('close', (code) => { if (code === 0) { resolve({ fullFileName, totalLen }); } else { reject(new Error(`aria2c exited with code ${ code }`)); } }); }); } /** * 使用 C 语言写的轻量级下载工具 Axel 进行下载 * @param url * @param fullFileName * @param progressCallback * @param videoDownloadConcurrency * @returns {Promise<{fullFileName: string, totalLen: number}>} */ async function axelDownloadBFile(url, fullFileName, progressCallback, videoDownloadConcurrency) { return new Promise((resolve, reject) => { // 构建路径 fullFileName = path.resolve(fullFileName); // 构建 -H 参数 const headerParams = Object.entries(BILI_HEADER).map( ([key, value]) => `--header="${ key }: ${ value }"` ).join(' '); let command = ''; let downloadTool = 'wget'; if (videoDownloadConcurrency === 1) { // wget 命令 command = `${ downloadTool } -O ${ fullFileName } ${ headerParams } '${ url }'`; } else { // AXEL 命令行 downloadTool = 'axel'; command = `${ downloadTool } -n ${ videoDownloadConcurrency } -o ${ fullFileName } ${ headerParams } '${ url }'`; } // 执行命令 const axel = exec(command); logger.info(`[R插件][axel/wget] 执行命令:${ downloadTool } 下载方式为:${ downloadTool === 'wget' ? '单线程' : '多线程' }`); axel.stdout.on('data', (data) => { const match = data.match(/(\d+)%/); if (match) { const progress = parseInt(match[1], 10) / 100; progressCallback?.(progress); } }); axel.stderr.on('data', (data) => { logger.info(`[R插件][${ downloadTool }]: ${ data }`); }); axel.on('close', (code) => { if (code === 0) { resolve({ fullFileName, totalLen: fs.statSync(fullFileName).size, }); } else { reject(new Error(`[R插件][${ downloadTool }] 错误:${ code }`)); } }); }); } /** * 获取下载链接 * @param url * @returns {Promise} */ export async function getDownloadUrl(url) { return axios .get(url, { headers: { ...BILI_HEADER }, }) .then(({ data }) => { const info = JSON.parse( data.match(/