基于youtube-dl-exec的YouTube播放列表下载器故障排查与功能实现求助
YouTube播放列表下载器修复方案
原代码核心问题分析
- 播放列表未适配:原代码仅支持单视频下载,未添加播放列表专属参数,无法批量获取列表内视频
- 视频合并逻辑错误:直接二进制拼接MP4文件会导致容器格式损坏,必须通过专业工具(如ffmpeg)完成合法合并
- 目录未初始化:未检查并创建下载/输出目录,可能触发文件写入失败
- 异步流程漏洞:合并操作未做异步处理,会提前给客户端返回响应,实际合并任务可能未完成
- 前端脚本错误:直接引用后端
server.js,浏览器无法解析Node.js代码
修复后的 index.html
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="stylesheet" href="styles.css" /> <title>YouTube播放列表下载器</title> </head> <body> <h1>YouTube播放列表下载器</h1> <form id="downloadForm"> <label for="playlistUrl">YouTube播放列表URL:</label> <input type="text" id="playlistUrl" name="playlistUrl" required /> <button type="submit">下载并合并</button> </form> <div id="progress"></div> <script> document.getElementById('downloadForm').addEventListener('submit', async (e) => { e.preventDefault(); const playlistUrl = document.getElementById('playlistUrl').value; const progressDiv = document.getElementById('progress'); progressDiv.textContent = '开始下载播放列表...'; try { const response = await fetch('/download', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: `playlistUrl=${encodeURIComponent(playlistUrl)}` }); const result = await response.text(); progressDiv.textContent = result; } catch (err) { progressDiv.textContent = '请求失败: ' + err.message; } }); </script> </body> </html>
修复后的 server.js
const express = require("express"); const fs = require("fs").promises; const path = require("path"); const { exec } = require("child_process"); const youtubedl = require("youtube-dl-exec"); const bodyParser = require("body-parser"); const app = express(); const port = process.env.PORT || 3000; app.use(express.static("public")); app.use(bodyParser.urlencoded({ extended: true })); // 确保目标目录存在,不存在则创建 const ensureDir = async (dirPath) => { try { await fs.access(dirPath); } catch { await fs.mkdir(dirPath, { recursive: true }); } }; // 批量下载播放列表所有视频 const downloadPlaylist = async (playlistUrl, videosDir) => { await ensureDir(videosDir); const options = { cwd: videosDir, format: "best[ext=mp4]", yesPlaylist: true, output: "%(title)s.%(ext)s", noOverwrites: true }; try { await youtubedl(playlistUrl, options); // 过滤出下载完成的MP4文件 const files = await fs.readdir(videosDir); return files.filter(file => file.endsWith(".mp4")).map(file => path.join(videosDir, file)); } catch (error) { console.error("播放列表下载失败:", error); throw error; } }; // 按批次合并视频(依赖ffmpeg) const mergeVideosInBatches = async (videoPaths, outputDir, batchSize) => { await ensureDir(outputDir); const totalBatches = Math.ceil(videoPaths.length / batchSize); for (let batch = 0; batch < totalBatches; batch++) { const start = batch * batchSize; const end = Math.min(start + batchSize, videoPaths.length); const batchVideos = videoPaths.slice(start, end); const outputFile = path.join(outputDir, `batch_${batch + 1}.mp4`); // 生成ffmpeg所需的文件列表 const fileListPath = path.join(outputDir, `batch_${batch + 1}_list.txt`); const listContent = batchVideos.map(vid => `file '${vid.replace(/'/g, "\\'")}'`).join("\n"); await fs.writeFile(fileListPath, listContent); // 执行ffmpeg合并命令 await new Promise((resolve, reject) => { const ffmpegCmd = `ffmpeg -f concat -safe 0 -i ${fileListPath} -c copy ${outputFile}`; exec(ffmpegCmd, (err, stdout, stderr) => { if (err) { console.error("合并失败:", err); reject(err); } else { console.log(`批次 ${batch + 1} 合并完成: ${outputFile}`); resolve(); } // 删除临时列表文件 fs.unlink(fileListPath).catch(console.error); }); }); } }; app.post("/download", async (req, res) => { const playlistUrl = req.body.playlistUrl; const videosDir = "./videos"; const outputDir = "./merged_batches"; const numVideosPerBatch = 100; try { res.setHeader('Content-Type', 'text/plain'); res.write("正在下载播放列表,请稍候...\n"); const videoPaths = await downloadPlaylist(playlistUrl, videosDir); res.write(`成功下载 ${videoPaths.length} 个视频\n`); res.write("开始合并视频批次...\n"); await mergeVideosInBatches(videoPaths, outputDir, numVideosPerBatch); res.end(`所有批次合并完成!合并后的文件位于 ${outputDir}`); } catch (error) { res.end(`处理失败: ${error.message}`); } }); app.listen(port, () => { console.log(`服务器运行在端口 ${port}`); });
使用说明
- 依赖安装:执行
npm install express youtube-dl-exec body-parser,同时需安装ffmpeg并添加到系统环境变量 - 目录结构:将
index.html放入public目录,server.js置于项目根目录 - 启动服务:运行
node server.js,访问http://localhost:3000,输入播放列表URL即可开始处理
内容的提问来源于stack exchange,提问作者yeppp 21
相关产品推荐
相关产品推荐

