import fs from "node:fs"; import axios from 'axios' import child_process from 'node:child_process' import util from "util"; 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 { spawn } from 'child_process'; import qrcode from "qrcode" const biliHeaders = { '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 isAria2 是否使用aria2 * @param videoDownloadConcurrency 视频下载并发 * @returns {Promise} */ export async function downloadBFile(url, fullFileName, progressCallback, isAria2 = false, videoDownloadConcurrency = 1) { if (isAria2) { return aria2DownloadBFile(url, fullFileName, progressCallback, videoDownloadConcurrency); } else { return normalDownloadBFile(url, fullFileName, progressCallback); } } /** * 正常下载 * @param url * @param fullFileName * @param progressCallback * @returns {Promise} */ async function normalDownloadBFile(url, fullFileName, progressCallback) { return axios .get(url, { responseType: 'stream', headers: { ...biliHeaders }, }) .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} */ 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}`)); } }); }); } /** * 获取下载链接 * @param url * @returns {Promise} */ export async function getDownloadUrl(url) { return axios .get(url, { headers: { ...biliHeaders }, }) .then(({ data }) => { const info = JSON.parse( data.match(/