import axios from "axios"; import fs from "node:fs"; import path from "path"; import child_process from "node:child_process"; import util from "util"; /** * 去除JSON的一些转义 \\" -> \" ->" * @param str */ function escapeSpecialChars(str) { return str.replace(/\\\\"/g, '\\"').replace(/\\"/g, '"'); } const parseVideoName = videoInfo => { const strAc号 = "ac" + (videoInfo?.dougaId || ""); const str标题 = videoInfo?.title; const str作者 = videoInfo?.user.name; const str上传时间 = videoInfo?.createTime; const str描述 = videoInfo?.description; const raw = [strAc号, str标题, str作者, str上传时间, str描述] .map(d => d || "") .join("_") .slice(0, 100); return raw; }; const parseVideoNameFixed = videoInfo => { const f = parseVideoName(videoInfo); const t = f.replaceAll(" ", "-"); return t; }; async function parseUrl(videoUrlAddress) { // eg https://www.acfun.cn/v/ac4621380?quickViewId=videoInfo_new&ajaxpipe=1 const urlSuffix = "?quickViewId=videoInfo_new&ajaxpipe=1"; const url = videoUrlAddress + urlSuffix; const raw = await axios.get(url).then(resp => { return resp.data; }); // Split const strsRemoveHeader = raw.split("window.pageInfo = window.videoInfo ="); const strsRemoveTail = strsRemoveHeader[1].split(""); const strJson = strsRemoveTail[0]; const strJsonEscaped = escapeSpecialChars(strJson); /** Object videoInfo */ const videoInfo = JSON.parse(strJsonEscaped); const videoName = parseVideoNameFixed(videoInfo); const ksPlayJson = videoInfo.currentVideoInfo.ksPlayJson; /** Object ksPlay */ const ksPlay = JSON.parse(ksPlayJson); const representations = ksPlay.adaptationSet[0].representation; const urlM3u8s = representations.map(d => d.url); return { urlM3u8s, videoName }; } async function parseM3u8(m3u8Url) { const m3u8File = await axios.get(m3u8Url).then(resp => resp.data); /** 分离ts文件链接 */ const rawPieces = m3u8File.split(/\n#EXTINF:.{8},\n/); /** 过滤头部 */ const m3u8RelativeLinks = rawPieces.slice(1); /** 修改尾部 去掉尾部多余的结束符 */ const patchedTail = m3u8RelativeLinks[m3u8RelativeLinks.length - 1].split("\n")[0]; m3u8RelativeLinks[m3u8RelativeLinks.length - 1] = patchedTail; /** 完整链接,直接加m3u8Url的通用前缀 */ const m3u8Prefix = m3u8Url.split("/").slice(0, -1).join("/"); const m3u8FullUrls = m3u8RelativeLinks.map(d => m3u8Prefix + "/" + d); /** aria2c下载的文件名,就是取url最后一段,去掉末尾url参数(?之后是url参数) */ const tsNames = m3u8RelativeLinks.map(d => d.split("?")[0]); /** 文件夹名,去掉文件名末尾分片号 */ let outputFolderName = tsNames[0].slice(0, -9); /** 输出最后合并的文件名,加个通用mp4后缀 */ const outputFileName = outputFolderName + ".mp4"; return { m3u8FullUrls, tsNames, outputFolderName, outputFileName, }; } // 下载m3u8 async function downloadM3u8Videos(m3u8FullUrls, outputFolderName) { /** 新建下载文件夹 在当前运行目录下 */ const outPath = outputFolderName; /** 批下载 */ const strDownloadParamFiles = m3u8FullUrls.map(async (d, i) => { return new Promise((resolve, reject) => { const writer = fs.createWriteStream(outPath + `${i}.ts`); axios .get(d, { headers: { "User-Agent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Mobile Safari/537.36", }, responseType: "stream", }) .then(dres => { dres.data.pipe(writer); writer.on("finish", () => resolve(true)); writer.on("error", () => reject); }); }); }); /** 写入下载链接列表文件 */ // fs.writeFileSync(path.resolve(outPath, "urls.txt"), str下载参数文件); return Promise.all(strDownloadParamFiles); } async function mergeAcFileToMp4(tsNames, FullFileName, outputFileName, shouldDelete = true) { /** 合并参数列表 格式file path */ const concatStrs = tsNames.map( (d, i) => `file ${path.resolve(FullFileName, i + ".ts").replace(/\\/g, "/")}`, ); const ffmpegList = path.resolve(FullFileName, "file.txt"); fs.writeFileSync(ffmpegList, concatStrs.join("\n")); const outPath = path.resolve(outputFileName); // 判断当前环境 let env; if (process.platform === "win32") { env = process.env; } else if (process.platform === "linux") { env = { ...process.env, PATH: "/usr/local/bin:" + child_process.execSync("echo $PATH").toString(), }; } else { console.log("暂时不支持当前操作系统!"); } const execFile = util.promisify(child_process.execFile); try { const cmd = "ffmpeg"; const args = ["-y", "-f", "concat", "-safe", "0", "-i", ffmpegList, "-c", "copy", outPath]; await execFile(cmd, args, { env }); if (shouldDelete) { fs.unlink(FullFileName, f => f); } return { outputFileName }; } catch (err) { logger.error(err); } } export { parseUrl, parseM3u8, downloadM3u8Videos, mergeAcFileToMp4 };