B站缓存视频一键合并MP4:手把手教你用FFmpeg+Node.js自动化处理
B站缓存视频自动化合并实战FFmpeg与Node.js高效协作指南每次在B站缓存了喜欢的视频合集却苦于无法直接在播放器里连续观看那些分散的M4S文件确实让人头疼。今天我们就来彻底解决这个问题——用FFmpeg和Node.js打造一个智能化的视频合并工具不仅能自动识别文件路径还能完美处理多集视频命名整个过程完全无需手动干预。1. 环境准备与工具配置工欲善其事必先利其器。在开始自动化处理之前我们需要准备好两个核心工具FFmpeg和Node.js运行环境。FFmpeg堪称多媒体处理的瑞士军刀它能够解码、编码、转码、混流、分离几乎所有主流的多媒体格式。对于我们的需求来说它的合并功能尤为关键——能够无损地将音频和视频M4S流合并为标准的MP4容器。安装步骤访问FFmpeg官网(https://ffmpeg.org)下载最新稳定版解压到本地目录例如C:\ffmpeg将bin目录C:\ffmpeg\bin添加到系统PATH环境变量验证安装在命令行运行ffmpeg -versionNode.js则为我们的自动化脚本提供了强大的运行时支持。建议安装LTS版本以确保稳定性# 验证Node.js安装 node -v npm -v提示如果遇到权限问题在Linux/macOS上可能需要使用sudoWindows上则以管理员身份运行命令提示符。2. 理解B站缓存文件结构B站的Android客户端缓存视频采用了特定的目录结构理解这个结构对我们的自动化脚本至关重要。典型的缓存路径如下Android/data/tv.danmaku.bilibilihd/download/ ├── BV1xxxxxx1 (视频ID) │ ├── 1 (分集目录) │ │ ├── entry.json (元数据文件) │ │ ├── 64 (视频质量目录) │ │ │ ├── audio.m4s │ │ │ ├── video.m4s │ ├── 2 (第二集目录) │ │ ├── entry.json │ │ ├── 64 │ │ │ ├── audio.m4s │ │ │ ├── video.m4s关键文件说明entry.json包含视频标题、分集信息等元数据video.m4s纯视频流文件audio.m4s纯音频流文件数字目录(如64)代表视频质量不同数值对应不同分辨率多集视频识别逻辑// 伪代码示例 if (目录下有多个数字子目录) { // 这是多集视频 从entry.json读取page_data.part作为分集标题 } else { // 这是单集视频 直接使用entry.json中的title作为文件名 }3. 构建自动化处理脚本现在我们来开发核心的Node.js脚本它将自动遍历缓存目录、解析元数据并生成正确的FFmpeg命令。我们使用Node.js的文件系统模块(fs)和路径模块(path)来处理文件操作。项目初始化mkdir bilibili-m4s-merger cd bilibili-m4s-merger npm init -y npm install commander chalk --save创建主脚本文件merger.js我们先构建基本框架#!/usr/bin/env node const fs require(fs); const path require(path); const { execSync } require(child_process); const chalk require(chalk); const program require(commander); program .version(1.0.0) .description(B站缓存视频自动化合并工具) .requiredOption(-i, --input path, B站缓存目录路径) .option(-o, --output path, 输出目录路径, ./output) .parse(process.argv); console.log(chalk.blue(开始处理B站缓存视频...));核心处理逻辑分步实现遍历输入目录识别所有视频缓存项对每个缓存项读取entry.json获取元数据根据是否多集决定输出文件名构建FFmpeg命令并执行处理过程中加入错误检查和日志输出function processCacheItem(cachePath) { const entries fs.readdirSync(cachePath); const videoItems entries.filter(entry fs.statSync(path.join(cachePath, entry)).isDirectory() !isNaN(parseInt(entry)) ); const isMultiEpisode videoItems.length 1; const entryData JSON.parse(fs.readFileSync( path.join(cachePath, videoItems[0], entry.json), utf8 )); // 创建输出目录 const outputDir path.join(program.output, entryData.title); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } // 处理每集视频 videoItems.forEach(item { const itemPath path.join(cachePath, item); const qualityDirs fs.readdirSync(itemPath) .filter(dir fs.statSync(path.join(itemPath, dir)).isDirectory() !isNaN(parseInt(dir))); if (qualityDirs.length 0) { console.log(chalk.yellow(警告: ${itemPath} 下未找到视频质量目录)); return; } const bestQuality Math.max(...qualityDirs.map(Number)); const mediaPath path.join(itemPath, bestQuality.toString()); const videoFile path.join(mediaPath, video.m4s); const audioFile path.join(mediaPath, audio.m4s); if (!fs.existsSync(videoFile) || !fs.existsSync(audioFile)) { console.log(chalk.yellow(警告: ${mediaPath} 下缺少音视频文件)); return; } let outputName; if (isMultiEpisode) { const itemData JSON.parse(fs.readFileSync( path.join(itemPath, entry.json), utf8 )); outputName ${itemData.page_data.part}.mp4; } else { outputName ${entryData.title}.mp4; } const outputPath path.join(outputDir, outputName); const ffmpegCmd ffmpeg -i ${videoFile} -i ${audioFile} -c copy ${outputPath}; try { console.log(chalk.gray(处理中: ${outputName})); execSync(ffmpegCmd, { stdio: inherit }); console.log(chalk.green(成功: ${outputName})); } catch (error) { console.log(chalk.red(失败: ${outputName})); console.error(error); } }); }4. 高级功能与错误处理基础功能完成后我们可以进一步优化脚本的健壮性和用户体验。以下是几个值得添加的高级功能4.1 进度显示与日志记录添加进度条可以显著提升用户体验特别是在处理大量视频时const progress require(progress-bar); function showProgress(current, total) { const bar new progress(:bar :percent, { total: total, width: 40 }); bar.update(current / total); }4.2 并发处理控制使用Promise和async/await控制并发数量避免系统资源耗尽const { promisify } require(util); const exec promisify(require(child_process).exec); const pool require(worker-pool)(4); // 限制4个并发 async function runFfmpegConcurrently(command) { return pool(async () { try { await exec(command); return { success: true }; } catch (error) { return { success: false, error }; } }); }4.3 配置文件支持通过配置文件自定义行为比如设置默认输出目录、选择视频质量优先级等// config.json { defaultOutput: ./converted, qualityPriority: [64, 32, 16], skipExisting: true, logLevel: verbose }4.4 完整的错误处理机制完善的错误处理应该包括输入目录验证FFmpeg可用性检查文件权限检查磁盘空间检查网络请求超时处理function validateEnvironment() { // 检查FFmpeg是否可用 try { execSync(ffmpeg -version, { stdio: ignore }); } catch { throw new Error(FFmpeg未安装或未配置到PATH环境变量); } // 检查输入目录 if (!fs.existsSync(program.input)) { throw new Error(输入目录不存在); } // 检查输出目录可写 try { fs.accessSync(program.output, fs.constants.W_OK); } catch { throw new Error(输出目录不可写); } // 检查磁盘空间 const freeSpace getFreeDiskSpace(program.output); if (freeSpace 1024 * 1024 * 1024) { // 小于1GB console.log(chalk.yellow(警告: 磁盘剩余空间不足1GB)); } }5. 打包与分发为了让脚本更易于使用我们可以将其打包为可执行文件并添加桌面快捷方式。5.1 使用pkg打包npm install pkg -g pkg -t node14-win-x64 --output bilibili-merger.exe merger.js5.2 创建GUI包装器对于不熟悉命令行的用户可以开发简单的Electron界面const { app, BrowserWindow, dialog } require(electron); let mainWindow; app.on(ready, () { mainWindow new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: true } }); mainWindow.loadFile(index.html); mainWindow.webContents.on(did-finish-load, () { mainWindow.webContents.send(version, app.getVersion()); }); });5.3 添加系统右键菜单集成在Windows上可以通过修改注册表添加右键菜单项Windows Registry Editor Version 5.00 [HKEY_CLASSES_ROOT\Directory\shell\B站视频合并] 合并B站缓存视频 [HKEY_CLASSES_ROOT\Directory\shell\B站视频合并\command] \C:\\path\\to\\bilibili-merger.exe\ -i \%1\6. 性能优化技巧当处理大量视频时性能优化变得尤为重要。以下是几个经过验证的优化方法6.1 文件操作优化使用fs.promises替代回调式API批量读取目录内容减少IO操作缓存已经读取的元数据async function batchReadFiles(dir) { const files await fs.promises.readdir(dir); return Promise.all( files.map(file fs.promises.readFile(path.join(dir, file), utf8) ) ); }6.2 FFmpeg参数调优使用硬件加速解码如果可用合理设置线程数禁用不需要的流和元数据ffmpeg -hwaccel auto -threads 4 -i video.m4s -i audio.m4s -map_metadata -1 -c copy output.mp46.3 内存管理限制并发FFmpeg进程数监控内存使用情况处理完成后主动清理临时文件const memoryMonitor setInterval(() { const memoryUsage process.memoryUsage(); if (memoryUsage.rss 1024 * 1024 * 512) { // 512MB console.log(chalk.yellow(内存使用过高暂停新任务)); pool.pause(); } else { pool.resume(); } }, 5000);7. 实际应用案例让我们看几个典型的应用场景了解如何调整脚本来满足特定需求。7.1 批量处理整个缓存目录bilibili-merger -i C:\Users\Username\Android\tv.danmaku.bilibilihd\download -o D:\B站视频7.2 仅处理特定视频通过添加视频ID过滤参数program.option(-f, --filter ids, 仅处理指定视频ID(逗号分隔));7.3 自动上传到云存储集成云存储SDK处理完成后自动上传const { S3Client, PutObjectCommand } require(aws-sdk/client-s3); async function uploadToS3(filePath, bucketName) { const client new S3Client({ region: us-east-1 }); const fileStream fs.createReadStream(filePath); const command new PutObjectCommand({ Bucket: bucketName, Key: path.basename(filePath), Body: fileStream }); await client.send(command); }7.4 与媒体服务器集成将处理好的视频直接导入Plex/Jellyfin等媒体服务器const plexAPI require(plex-api); const client new plexAPI({ hostname: localhost, port: 32400, token: YOUR_PLEX_TOKEN }); function refreshPlexLibrary(sectionId) { return client.query(/library/sections/${sectionId}/refresh); }