You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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}`);
});

使用说明

  1. 依赖安装:执行npm install express youtube-dl-exec body-parser,同时需安装ffmpeg并添加到系统环境变量
  2. 目录结构:将index.html放入public目录,server.js置于项目根目录
  3. 启动服务:运行node server.js,访问http://localhost:3000,输入播放列表URL即可开始处理

内容的提问来源于stack exchange,提问作者yeppp 21

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 15:15:54