先说个背景。我平时维护着一个小型内容分享频道,经常要把 YouTube 上的公开视频拉下来,做点裁剪或压缩,再发到 Discord 社群里。最开始直接丢在线转换网站,折腾了两个月忍无可忍:加载慢、偶尔带水印、甚至遇到过下载链接失效导致频道开天窗的情况。后来我搭了一套本地命令行工具链,给这套脚本集起了个代号 zapret,全称是 Zero-API Processing and Reliable Export Toolkit,核心就干三件事——把 YouTube 视频下载到本地、按需转码压缩、推送到 Discord 指定频道。这套方案帮我解决了很多实际问题,今天就把完整的搭建思路、命令细节和踩过的坑一次性分享出来。适合想批量下载视频、有转码需求、或者想把下载器接入 Discord bot 的读者参考。
1. 为什么我淘汰了在线转换站,转而用本地脚本处理视频
在线转换站的模式看起来很省事,粘贴链接点下载就行。但在我这个使用频率下,它的几个问题被放大得很明显。
1.1 在线站点的核心痛点:速度不可控、文件大小受限、生命周期不稳定
在线站的基本逻辑是:你把 YouTube 链接发给它的服务器,它帮你下载并转码,再把结果文件流式回传给你。听起来没什么问题,但实际操作会有三个反复出现的状况。
第一是速度完全不可控。冷门视频可能几秒就完成,而热门的长视频经常排队五到十分钟,而且一旦服务器负载高,下载就容易被中断。有一次我赶着把一个给活动预告的视频发到频道,在线站硬是转了八分钟,最后报错,等于整个流程白等。
第二是文件大小限制。当时我用的免费档位单文件限制在几百 MB 左右,一个 1080p 的完整视频动辄 1-2 GB,根本传不下来,只能选低清晰度。可低清晰度拉下来,画质在手机上还能看,投到电视屏幕上就完全不行了。
第三是站点的生命周期不稳定。这类在线转换服务的政策波动很大,随手搜一下就能发现不少站点今天还能用、明天就关停,或者频繁改域名。你不可能把内容生产线压在一个随时会消失的外部服务上。
1.2 自建工具链的实际收益与选型思路
换成本地方案之后,上面这些问题基本都不存在了。自建工具链的组成很朴素:yt-dlp负责拉流,ffmpeg负责转码和裁剪,再加一个脚本把这两者串起来,最后用 Discord 的接口推送。我管这套脚本集合叫 zapret,因为它完全基于本地 API 调用,不依赖任何第三方网页服务,所以整个流程非常稳定。
选型的逻辑也很简单。yt-dlp是目前持续维护、更新频率很高的 YouTube 下载命令行工具,支持绝大多数公开视频的流地址提取;ffmpeg则是转码领域的标准工具,处理剪切、压缩、格式转换都非常成熟。这两个都是开源、免费、跨平台的,整个下载处理链路跑起来后,除了耗电和带宽,几乎没有边际成本。
对比在线站,自建方案至少有三个明显优势:
| 对比维度 | 在线转换站 | 自建命令行脚本 |
|---|---|---|
| 下载速度 | 受制于服务器排队和带宽 | 直接走本机带宽,速度更稳定 |
| 文件大小上限 | 免费档通常限制单文件大小 | 只受本机磁盘空间限制 |
| 可重复性 | 无缓存,每次都要重新提交 | 脚本和参数完全可复现,支持批处理 |
| 隐私性 | 需要把链接提交给第三方服务器 | 全程本地处理,不经过陌生服务 |
1.3 本地环境的准备:Python、ffmpeg 与 yt-dlp 的安装细节
搭建这套工具链的第一步是准备环境,这一步其实大多数人都会踩坑,尤其是 ffmpeg 的安装。我用的是 Ubuntu 服务器,安装命令如下:
# 更新系统并安装 Python 和 ffmpeg sudo apt update sudo apt install -y python3 python3-pip ffmpeg # 用 pip 安装 yt-dlp pip3 install -U yt-dlp如果你是 Windows 或 macOS 本地环境,建议优先考虑用包管理器安装:macOS 用brew install ffmpeg yt-dlp,Windows 可以用winget install yt-dlp,或者直接去 ffmpeg 官网下载预编译二进制。这里有个容易忽略的地方:Windows 下 ffmpeg 也需要手动配置 PATH,如果安装完在命令行输入ffmpeg -version没反应,大概率就是 PATH 没配好。
装完之后建议先验证一下版本:
yt-dlp --version ffmpeg -version都正常输出之后,环境就准备完成了。接下来就进入真正好玩的阶段——下载命令和批处理脚本。
2. yt-dlp 命令行实战:从一条基础命令到完整批处理脚本
yt-dlp 的功能非常丰富,但常用的核心参数其实就那几个。把这一节看懂,你就能应付绝大多数下载场景。
2.1 最基础的下载命令是怎么工作的
先看一条最朴素的下载命令:
yt-dlp "https://www.youtube.com/watch?v=xxxxx"执行之后,yt-dlp 会拉取视频页面里的元数据,解析出可用流,然后自动选择一个当前环境能处理的最佳画质组合进行下载。所谓“最佳画质组合”,意思是当视频存在独立的视频流和音频流时,yt-dlp 会自动分别下载,再调用 ffmpeg 合并成单个 MKV 或 MP4 文件。
这其实解释了一个很多新手会遇到的疑问:为什么明明下载的是 4K 视频,却听到有些人说“yt-dlp 默认不下载最佳画质”。默认参数确实只能拿到分辨率较高的一个流,但如果该视频把高清视频流和高质量音频流分开了,yt-dlp 默认参数下会自动选择合并,所以一般不会出问题。真正常见的问题反而是:某些视频存在 60fps 版本,默认选择未必是最高的,需要显式指定格式参数。
2.2 常用参数拆解:格式选择、输出路径与元数据
我最常用的参数组合是这一条:
yt-dlp \ -f "bestvideo[height<=1080][ext=mp4]+bestaudio[ext=m4a]/best[height<=1080][ext=mp4]" \ --merge-output-format mp4 \ -o "/data/videos/%(title)s.%(ext)s" \ --write-thumbnail \ --write-description \ "https://www.youtube.com/watch?v=xxxxx"逐个解释一下:
-f "bestvideo[height<=1080][ext=mp4]+bestaudio[ext=m4a]/best[height<=1080][ext=mp4]":指定优先选择 1080p 及以下、MP4 封装的视频流,再加上 M4A 音频流,合并成 MP4。如果前者不可用,就回退到完整的 1080p MP4 单文件。--merge-output-format mp4:合并时强制输出 MP4 容器,保证在 Discord 和大部分播放器里直接能播。-o "/data/videos/%(title)s.%(ext)s":指定保存路径和文件名模板。%(title)s是视频标题,%(ext)s是扩展名。--write-thumbnail:把封面图一起下载。--write-description:把视频简介保存为文本文件。
实际跑起来后,输出日志里会看到类似[download] 100% of 512.3MiB的进度,最后提示[Merger] Merging formats into "xxx.mp4",说明合并完成。
2.3 把单条命令扩展成批处理脚本:队列、日志与断点续传
单条命令解决不了批量需求。我需要下载某个频道最近 20 个视频时,最直接的方法是写一个循环脚本。我的做法是:
#!/bin/bash # zapret-download.sh # 用法:./zapret-download.sh url_list.txt INPUT_FILE="$1" OUTPUT_DIR="/data/videos" while IFS= read -r url; do [[ -z "$url" ]] && continue echo "===== 开始处理: $url =====" yt-dlp \ -f "bestvideo[height<=1080][ext=mp4]+bestaudio[ext=m4a]/best[height<=1080][ext=mp4]" \ --merge-output-format mp4 \ --write-thumbnail \ -o "${OUTPUT_DIR}/%(title)s.%(ext)s" \ "$url" >> /var/log/zapret/download.log 2>&1 if [ $? -eq 0 ]; then echo "$url [成功]" >> /var/log/zapret/success.log else echo "$url [失败]" >> /var/log/zapret/failed.log fi done < "$INPUT_FILE"这里有几个小细节值得说。第一,日志一定要拆成成功和失败两个文件,后面排查问题时能省很多时间。第二,yt-dlp 本身支持断点续传,如果某个视频下载到一半断网了,重新执行同一条命令,它会检测到本地已有临时文件并继续下载,而不是从头再来,这个特性在处理大文件时尤其有用。
批处理脚本跑起来以后,可以直接把链接逐行放进url_list.txt,然后执行:
./zapret-download.sh url_list.txt实测下来,20 个视频大概的耗时取决于网速和视频源服务器,本地跑一晚上基本能全部完成。
2.4 进阶技巧:按时间过滤、下载字幕和仅提取音频
如果你的需求不只是完整视频,还有几个常见场景可以使用针对性参数。
只下载某个频道最近一个月的视频:
yt-dlp --dateafter 20250101 --max-downloads 20 -o "/data/videos/%(title)s.%(ext)s" "https://www.youtube.com/@channel/videos"这个命令会拉取频道页面的视频列表,筛选日期在 2025 年 1 月 1 日之后的视频,最多抓前 20 个。
下载字幕文件:
yt-dlp --write-subs --sub-langs "zh-Hans,en" --skip-download -o "/data/subs/%(title)s.%(ext)s" "https://www.youtube.com/watch?v=xxxxx"只提取音频做播客或素材库:
yt-dlp -x --audio-format mp3 --audio-quality 0 -o "/data/audio/%(title)s.%(ext)s" "https://www.youtube.com/watch?v=xxxxx"-x是提取音频的快捷键,--audio-quality 0表示使用最好的音频质量。
3. 把下载好的视频送进 Discord:三种可行的分发路径
视频下载不是终点,把文件推到 Discord 频道里才是。但 Discord 对文件上传有一系列限制,必须先搞清楚边界,再决定用哪条分发路径。
3.1 Discord 的文件上传限制:10MB、25MB 与 50MB 的不同层级
Discord 的文件上传大小上限取决于频道服务器的特权等级:
| 服务器等级 | 单文件上传上限 | 说明 |
|---|---|---|
| 普通免费用户 / 普通服务器 | 10MB | 早期默认限制 |
| 开通 Nitro 的免费服务器 | 25MB | 大多数社区频道的基础档 |
| 验证过的服务器 | 50MB | 通常需要做服务器验证 |
| Nitro 用户上传基础 | 25MB 起步,视订阅档位而定 | 个人用户可以提升 |
关键问题在于,1080p 的十分钟视频压到很低的码率,体积也通常在 50-150MB 左右,直接传基本超限。所以实际方案几乎必须走“转码压缩到适合的上传体积”这条路线,除非你的频道本来就是发几 MB 的短视频。
我的目标一直是把视频控制在 25MB 以内,这样才能稳妥地发到大多数普通 Discord 频道。压缩一个 1080p 的长视频到这个体积,意味着它可能已经不清晰了。所以在实际处理中,我会根据视频内容和用途选择不同的转码策略:如果只是给社群快速预览,320p 到 480p 就够用;如果是正式内容存档,那就不走 Discord,直接丢到网盘再发链接。
3.2 用一条 ffmpeg 命令完成压缩、裁剪和格式对齐
把视频压到 25MB 以内的核心方法,无非是降分辨率、降码率、裁剪时长这三件事。我在 zapret 脚本里集成了一个函数,输入原始视频路径和目标体积,自动计算合适的码率,再执行转码。
#!/bin/bash # zapret-compress.sh # 用法:./zapret-compress.sh input.mp4 output.mp4 target_size_mb INPUT="$1" OUTPUT="$2" TARGET_MB="$3" # 计算目标码率(kbps),预留一点音频码率空间 # 公式:(目标体积 MB * 8192) / 视频时长秒数 - 音频码率 DURATION=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$INPUT") AUDIO_BITRATE=64 TOTAL_BITRATE=$(echo "scale=0; ${TARGET_MB} * 8192 / ${DURATION}" | bc) VIDEO_BITRATE=$(echo "${TOTAL_BITRATE} - ${AUDIO_BITRATE}" | bc) ffmpeg -i "$INPUT" \ -c:v libx264 \ -b:v "${VIDEO_BITRATE}k" \ -preset medium \ -c:a aac \ -b:a "${AUDIO_BITRATE}k" \ -movflags +faststart \ "$OUTPUT"这个脚本用的ffprobe是 ffmpeg 自带的探针工具,先读取视频时长,再根据目标体积反推码率。核心公式是:
- 总码率(kbps)= 目标体积(MB)× 8192(MB 转 kbit)÷ 时长(秒)
- 视频码率 = 总码率 − 音频码率
假设一个 10 分钟(600 秒)的视频要压到 20MB,总码率就是20 × 8192 / 600 = 273 kbps,减去 64kbps 音频,视频码率只有约 209kbps。这个码率下分辨率如果还是 1080p,画面会非常糊。所以我会在压缩前判断:如果计算出的视频码率低于 500kbps,就降低分辨率到 720p 或 480p,再重新计算一遍。
一个更稳的替代方案是直接用-vf scale=1280:720强制降到 720p:
ffmpeg -i "$INPUT" \ -vf "scale=1280:720:force_original_aspect_ratio=decrease" \ -c:v libx264 -crf 28 \ -preset medium \ -c:a aac -b:a 64k \ -movflags +faststart \ "$OUTPUT"-crf 28是恒定质量参数,值越高质量越低、体积越小。对于用于线上预览的内容,28 是一个体积和画质的平衡点。你可以先用这条命令压一版,看体积是否合适再微调。
3.3 用 Webhook 实现零代码推送
文件压缩好了,怎么推送到 Discord?最简单的方案是 Webhook。在 Discord 频道设置里创建 Webhook 后会得到一个类似下面的 URL:
https://discord.com/api/webhooks/1234567890/abcdefg然后就可以用一条命令直接上传文件:
curl -X POST \ -H "Content-Type: multipart/form-data" \ -F "content=这里是视频说明文字" \ -F "file=@/data/videos/output.mp4" \ "https://discord.com/api/webhooks/1234567890/abcdefg"Webhook 的好处是零代码、即插即用,适合手动操作或简单地写进 shell 脚本。缺点是没有互动能力,不能查权限、不能做复杂的命令响应。
3.4 用 discord.py 搭建一个简单的下载机器人
如果我要把整套流程做成一个可供多人使用的服务,直接写一个 Discord bot 会更好。Bot 可以在频道里通过斜杠命令触发下载任务,下载完成后把文件发出来,还能做权限控制。
下面是一个最小可用的discord.py机器人示例,核心逻辑是监听/download命令,下载指定视频,压缩到 25MB 以内,再回复文件。
import discord from discord.ext import commands import subprocess import os TOKEN = "你的BOT_TOKEN" CHANNEL_ID = 1234567890 # 目标频道 ID bot = commands.Bot(command_prefix="/", intents=discord.Intents.default()) @bot.event async def on_ready(): print(f"登录成功:{bot.user}") @bot.command() async def download(ctx, url: str): # 限制只有特定角色可以使用 role = discord.utils.get(ctx.author.roles, name="管理员") if not role: await ctx.send("没有权限使用此命令。") return await ctx.send("正在下载视频,请稍候...") title = "temp_video" subprocess.run([ "yt-dlp", "-f", "best[height<=720]", "--merge-output-format", "mp4", "-o", f"{title}.%(ext)s", url, ], check=True) filename = f"{title}.mp4" # 简单判断体积,超过 20MB 就压缩 if os.path.getsize(filename) > 20 * 1024 * 1024: compressed = f"{title}_compressed.mp4" subprocess.run([ "ffmpeg", "-i", filename, "-c:v", "libx264", "-crf", "28", "-c:a", "aac", "-b:a", "64k", "-movflags", "+faststart", compressed, ], check=True) os.remove(filename) filename = compressed await ctx.send(file=discord.File(filename)) os.remove(filename) bot.run(TOKEN)这个代码虽然简单,但足以跑通完整链路。我自己的生产脚本比这个复杂,增加了任务队列、失败重试和更精细的权限管理,整体思路完全一致。
4. 自动化之后的一系列坑:限流、队列、失败重试和告警
自动化流程上线后,真正需要调试的地方才开始出现。下面这几个坑,几乎每个跑过下载机器人的人都会遇到。
4.1 第一个坑:Discord 上传一直失败,原来是被服务器限流
第一次跑通机器人后,我一次性往频道里推了十多个视频文件,结果从第三个开始全部报错。一开始以为是文件超限,检查体积发现都是 20MB 左右,没有超过服务器 25MB 的上限。后来读文档才发现,Discord 对 Webhook 上传有频率限制,详情是:同一 Webhook 每分钟最多 30 次请求,对单个频道上传还有额外的速率限制。连续快速上传一定会触发 429 限流响应,返回头里带了Retry-After字段。
解决方案很简单:在上传前做固定间隔。我在脚本里加了sleep 10,每上传一个文件就等 10 秒,之后再没有触发过限流。
4.2 第二个坑:视频时长太长导致 ffmpeg 预测文件体积不准确
按公式计算码率后,最终实际文件体积有时会偏离预期,偏差大到 20% 以上。原因有两个:一是 libx264 在低码率下会对画面复杂度高的场景提高实际输出体积,二是 audio 码率是固定的,公式计算时如果没预留足够空间,总码率就会超。我在脚本里加了兜底校验,转码完成后用stat检查文件体积,如果超过目标值的 90% 就适当降分辨率重压一次。这个“校验-重压”逻辑写起来很简单,但能解决 80% 的压不准问题。
另外-movflags +faststart这个参数一定要加。不加的话,MP4 的元数据在文件末尾,播放器必须下载完整文件才能从头播放;加了之后元数据移到文件头,上传到 Discord 后在线预览会顺畅很多。
4.3 第三个坑:yt-dlp 下载失败的原因往往是网站改版,而不是你的命令有问题
Youtube 的网站结构隔几个月就会调整一次,yt-dlp 的解析逻辑也会随之失效,最明显的表现是下载报错信息里有Unsupported URL、Unable to extract这类关键词。处理方式很直接:升级 yt-dlp 到最新版。
pip3 install -U yt-dlp建议在批处理脚本开头加上一段自动升级逻辑:
yt-dlp --update >> /var/log/zapret/update.log 2>&1我的 cron 任务每天凌晨跑一次批处理前,会先执行升级,这样基本上能让下载器保持可用状态。但注意,--update需要能连到官方源,如果你的运行环境网络受限,可以考虑定时从打包源重新安装。
4.4 任务队列和失败重试:保证无人值守时的稳定性
批量下载最怕的就是跑到一半某个视频下载失败,后续任务全部停止。我的方案是在外层套一个简单的重试循环:每个链接最多尝试 3 次,每次失败后等待 20 秒再重试,仍然失败就把链接写入failed.log并继续下一个。
attempt=1 max_attempts=3 while [ $attempt -le $max_attempts ]; do yt-dlp "$url" \ -o "${OUTPUT_DIR}/%(title)s.%(ext)s" \ >> /var/log/zapret/download.log 2>&1 if [ $? -eq 0 ]; then echo "$url [成功]" >> /var/log/zapret/success.log break else echo "第 ${attempt} 次尝试失败: $url" >> /var/log/zapret/retry.log attempt=$((attempt + 1)) sleep 20 fi done这个循环能覆盖绝大多数临时网络抖动问题。如果三次都失败,就把链接留到下次人工排查就是了。
4.5 把下载、转码、上传串成一条流水线:完整 bash 脚本示例
上面这些模块组合到一起,就是一个完整的 zapret 主脚本。这里贴一个精简版,方便你直接改路径后使用:
#!/bin/bash # zapret-pipeline.sh # 功能:下载 -> 压缩 -> 上传 Discord INPUT_URL="$1" WORK_DIR="/data/tmp" OUTPUT_DIR="/data/videos" WEBHOOK_URL="https://discord.com/api/webhooks/xxx/yyy" cd "$WORK_DIR" title=$(yt-dlp --get-title "$INPUT_URL") echo "开始下载: $title" yt-dlp -f "best[height<=1080]" --merge-output-format mp4 \ -o "${WORK_DIR}/${title}.%(ext)s" "$INPUT_URL" >> /var/log/zapret/download.log 2>&1 src="${WORK_DIR}/${title}.mp4" echo "开始检查体积" size=$(stat -c%s "$src") if [ "$size" -gt 20971520 ]; then dst="${WORK_DIR}/${title}_compressed.mp4" ffmpeg -i "$src" -vf "scale=1280:720:force_original_aspect_ratio=decrease" \ -c:v libx264 -crf 28 -c:a aac -b:a 64k \ -movflags +faststart "$dst" >> /var/log/zapret/ffmpeg.log 2>&1 rm "$src" src="$dst" fi echo "上传到 Discord" curl -s -X POST \ -H "Content-Type: multipart/form-data" \ -F "content=【自动发布】$title" \ -F "file=@$src" \ "$WEBHOOK_URL" rm -f "$src" echo "完成: $title"脚本里用stat -c%s直接读字节数,大小判断为 20MB,给上传上限留足缓冲。整个流程跑下来的耗时,视频下载占大头,转码速度和时长、码率相关,上传取决于你的上行带宽。
5. 关于版权、隐私和容易被忽略的使用边界
自动下载和分发视频的能力听起来非常好用,但它同时带来了三个需要认真对待的问题。我建议每一个想复刻这套流程的人都先想清楚这几点,再动手搭建。
5.1 版权归属:个人合理使用与公开分发的边界
YouTube 上的视频分两种:创作者明确声明允许转载的,和默认保留版权的。下载到本地自用,比如离线观看、做剪辑素材、研究学习,在很多地区属于合理使用的范畴,风险相对较低。但把下载的视频再自动转发到自己的 Discord 频道里,尤其是当频道是公开的、订阅人数多的时候,这就涉及公开传播了。
我做这套工具的初衷是用于自己社群的内部资料存档和预告片分发,所以在我自己的频道里,只推送自己制作的视频、渠道明确允许二创的官方素材,以及购买了授权的内容。这个逻辑,不管你套不套自动化脚本,都应该保持一致。对来源不明的第三方视频,最好先读一下视频简介或频道页面的版权声明,不要默认“能下载就能发”。
5.2 隐私安全:处理数据时不要让中间服务介入
自建工具链的一个显著好处是,不需要把链接提交给任何第三方转换站,减少了链接和视频文件被陌生人服务获取的风险。但要注意的是,yt-dlp请求的是 YouTube 本身,它会看到你的 IP。如果你担心这个问题,可以通过配置--proxy参数指向可信的代理服务。不过这条涉及具体网络环境方案,不在本文展开,实际使用时按你自己的合规要求处理就行。
另外,下载的视频如果包含他人个人信息、未公开的联系方式,或者涉及未成年人的内容,无论如何都不要在公开频道传播,这不是能不能下载的问题,而是基本的数据伦理问题。
5.3 合理使用建议:控制并发、遵守平台协议、及时清理临时文件
最后一个建议是关于自己服务的稳定性。自动化下载不应该以极高的并发去请求平台,否则不仅可能触发平台的风控,还可能让你的 IP 被暂时限制访问。我在实际使用中会把并发控制在 1 到 2 个任务,批量任务之间加入合理的随机延迟,避免看起来像恶意抓取。
同时,/data/tmp这类临时目录隔几天就会积累大量中间文件,记得写一个清理脚本,超过 2 天的中间文件直接删掉,防止磁盘爆掉。
find /data/tmp -type f -mtime +2 -delete这套 zapret 工具链从 2024 年初跑到现在,前前后后迭代了很多版本,从最初只有一两条命令,慢慢长成了带任务队列、失败重试、压缩判断和 Webhook 推送的完整流水线。回过头来看,它最大的价值不是在某个命令上省了多少时间,而是让我彻底摆脱了对那些随时可能关停的在线服务的依赖,整个内容处理流程真正握在了自己手里。
如果你也准备搭一套同样的流程,我的建议是从最基础的yt-dlp命令开始,确认下载没问题再往上加压缩和推送逻辑,一步步来。每一步单独跑通了,最后拼起来才不会抓瞎。我在调试过程中踩过最久的一个坑就是视频体积老是超限,后来加了“校验-重压”逻辑才彻底根治,这个经验你也可以直接抄走。