
先说个背景。我平时维护着一个小型内容分享频道经常要把 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-dlpWindows 可以用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?vxxxxx执行之后yt-dlp 会拉取视频页面里的元数据解析出可用流然后自动选择一个当前环境能处理的最佳画质组合进行下载。所谓“最佳画质组合”意思是当视频存在独立的视频流和音频流时yt-dlp 会自动分别下载再调用 ffmpeg 合并成单个 MKV 或 MP4 文件。这其实解释了一个很多新手会遇到的疑问为什么明明下载的是 4K 视频却听到有些人说“yt-dlp 默认不下载最佳画质”。默认参数确实只能拿到分辨率较高的一个流但如果该视频把高清视频流和高质量音频流分开了yt-dlp 默认参数下会自动选择合并所以一般不会出问题。真正常见的问题反而是某些视频存在 60fps 版本默认选择未必是最高的需要显式指定格式参数。2.2 常用参数拆解格式选择、输出路径与元数据我最常用的参数组合是这一条yt-dlp \ -f bestvideo[height1080][extmp4]bestaudio[extm4a]/best[height1080][extmp4] \ --merge-output-format mp4 \ -o /data/videos/%(title)s.%(ext)s \ --write-thumbnail \ --write-description \ https://www.youtube.com/watch?vxxxxx逐个解释一下-f bestvideo[height1080][extmp4]bestaudio[extm4a]/best[height1080][extmp4]指定优先选择 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[height1080][extmp4]bestaudio[extm4a]/best[height1080][extmp4] \ --merge-output-format mp4 \ --write-thumbnail \ -o ${OUTPUT_DIR}/%(title)s.%(ext)s \ $url /var/log/zapret/download.log 21 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?vxxxxx只提取音频做播客或素材库yt-dlp -x --audio-format mp3 --audio-quality 0 -o /data/audio/%(title)s.%(ext)s https://www.youtube.com/watch?vxxxxx-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 formatduration -of defaultnoprint_wrappers1:nokey1 $INPUT) AUDIO_BITRATE64 TOTAL_BITRATE$(echo scale0; ${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× 8192MB 转 kbit÷ 时长秒视频码率 总码率 − 音频码率假设一个 10 分钟600 秒的视频要压到 20MB总码率就是20 × 8192 / 600 273 kbps减去 64kbps 音频视频码率只有约 209kbps。这个码率下分辨率如果还是 1080p画面会非常糊。所以我会在压缩前判断如果计算出的视频码率低于 500kbps就降低分辨率到 720p 或 480p再重新计算一遍。一个更稳的替代方案是直接用-vf scale1280:720强制降到 720pffmpeg -i $INPUT \ -vf scale1280:720:force_original_aspect_ratiodecrease \ -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 后会得到一个类似下面的 URLhttps://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/abcdefgWebhook 的好处是零代码、即插即用适合手动操作或简单地写进 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/, intentsdiscord.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[height720], --merge-output-format, mp4, -o, f{title}.%(ext)s, url, ], checkTrue) 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, ], checkTrue) os.remove(filename) filename compressed await ctx.send(filediscord.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 21我的 cron 任务每天凌晨跑一次批处理前会先执行升级这样基本上能让下载器保持可用状态。但注意--update需要能连到官方源如果你的运行环境网络受限可以考虑定时从打包源重新安装。4.4 任务队列和失败重试保证无人值守时的稳定性批量下载最怕的就是跑到一半某个视频下载失败后续任务全部停止。我的方案是在外层套一个简单的重试循环每个链接最多尝试 3 次每次失败后等待 20 秒再重试仍然失败就把链接写入failed.log并继续下一个。attempt1 max_attempts3 while [ $attempt -le $max_attempts ]; do yt-dlp $url \ -o ${OUTPUT_DIR}/%(title)s.%(ext)s \ /var/log/zapret/download.log 21 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_URLhttps://discord.com/api/webhooks/xxx/yyy cd $WORK_DIR title$(yt-dlp --get-title $INPUT_URL) echo 开始下载: $title yt-dlp -f best[height1080] --merge-output-format mp4 \ -o ${WORK_DIR}/${title}.%(ext)s $INPUT_URL /var/log/zapret/download.log 21 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 scale1280:720:force_original_aspect_ratiodecrease \ -c:v libx264 -crf 28 -c:a aac -b:a 64k \ -movflags faststart $dst /var/log/zapret/ffmpeg.log 21 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命令开始确认下载没问题再往上加压缩和推送逻辑一步步来。每一步单独跑通了最后拼起来才不会抓瞎。我在调试过程中踩过最久的一个坑就是视频体积老是超限后来加了“校验-重压”逻辑才彻底根治这个经验你也可以直接抄走。