
最近我在整理自己的视频素材库时一直觉得手头的工作流又碎又慢。一边是YouTube上要长期留存的视频、课程、音乐另一边是Discord里大家共享资源、讨论内容的频道两边来回搬运全靠手动操作下载一次、转码一次、再上传一次重复劳动特别多。后来我干脆把整套流程做成了一个小工具代号就叫zapret。这个名字没啥特殊含义纯粹是“压榨”的意思——把视频资源的下载、压缩、归档、分发全部榨干到一个流程里。整套方案我已经在本地和小组频道里跑了大半年稳定、省心今天把完整思路和可复现的步骤写出来。这套东西不是给你看热闹的。如果你和我一样平时有大量YouTube视频需要批量保存或者管着一个Discord频道需要定期往里面发资源又或者只是不想每次手动“下载-改名-转码-上传”四连击那这个项目可以直接帮你把流程自动化。我用的全是开源工具yt-dlp负责下载FFmpeg负责转码压缩Discord Webhook负责推送整个流程可以在自己的电脑或小服务器上跑不需要额外花钱买服务。1. 项目整体设计与思路拆解1.1 为什么会做这个工具手动流程的四个痛点先说说我原来是怎么干活的你大概能理解为什么非做这个工具不可。以前我从YouTube保存视频基本是打开网页找到视频复制链接打开某个下载站点粘贴链接等网页解析选格式下载再文件名改一改存到对应文件夹。如果是要发到Discord群里共享还得再拖进对话框上传或者传到网盘再贴链接。一次两次还能忍次数多了真的烦躁。这个流程里有几个特别浪费时间的点下载环节没法批量一个链接一个链接操作遇到播放列表或者整个频道操作量直接翻倍。格式和画质的选择全凭手动同一批视频可能下载下来有的是mp4有的是webm有的带字幕有的不带归档时乱七八糟。文件命名完全没有统一规则默认文件名要么是一串乱码似的ID要么是带特殊字符的标题在服务器或本地系统里很不好处理。传到Discord还要考虑单个文件25MB的上传限制大文件得先压缩或切割这一步纯靠肉眼判断哪个文件超了、压到多少合适效率极低。所以我做的第一件事就是把这四个痛点全部抽象成可配置、可复用的模块。下载交给yt-dlp转码交给FFmpeg推送交给Discord的Webhook中间用脚本串起来。这样以后新增一个视频要入库我只需要把链接丢给脚本剩下的全自动完成。1.2 技术选型为什么是 yt-dlp FFmpeg Discord Webhook技术选型这事我踩过不少坑这里直接说我最终留下的方案和理由。下载这块yt-dlp是当前最靠谱的选择。它其实是老牌下载工具youtube-dl的活跃分支更新频率高对YouTube页面结构变化的适配速度很快支持的网站也多。更重要的是它支持直接解析播放列表、获取字幕、读取元数据、按模板命名这些正是我做批量归档需要的能力。可执行文件单一直接下载就能用没有复杂的依赖关系。FFmpeg就不用多说了视频处理领域的事实标准。yt-dlp下载下来的视频格式不一定统一比如YouTube默认有DASH流视频和音频是分开的需要合并有些格式在Discord里不支持预览需要转成H.264的MP4还有压缩、裁剪、抽音频FFmpeg一把梭全搞定。Discord侧我选了Webhook而不是完整Bot。原因很简单Webhook只需要一个URL不需要维护长连接、不需要处理Gateway事件、不需要管理权限配置单纯从脚本往频道推文件的话它是侵入性最低、最稳定的方式。如果你以后需要更复杂的交互比如用户发指令触发下载那再升级成discord.py写的Bot也不迟。1.3 整体流程图和模块分工整套工作流大概是这么跑的脚本读取配置文件拿到一个或多个YouTube视频/播放列表链接。yt-dlp根据配置下载视频自动选择最高画质或指定格式并按模板命名。下载完成后FFmpeg按需进行转码、压缩、抽音频等处理。处理完的文件通过Discord Webhook上传到指定频道附带文字说明。上传成功后脚本把任务标记为完成清理临时文件同时通过档案文件记录已处理过的视频ID避免下次重复下载。模块之间通过标准的命令行调用衔接不搞花活出问题也好排查。我用的是Python写主调度脚本但其实用纯Shell也能完成大部分工作核心逻辑不绑定语言。2. 核心模块解析与实操要点2.1 下载模块yt-dlp 的关键参数与模板命名yt-dlp的命令行参数很丰富但真正核心的其实是那几个。我经常用的配置大概是这样的yt-dlp \ --cookies cookies.txt \ -f bestvideo[height1080][extmp4]bestaudio[extm4a]/best[height1080][extmp4]/best \ --merge-output-format mp4 \ --write-info-json \ --write-sub --sub-langs zh.*,en.* --sub-format srt \ --embed-metadata \ --download-archive archive.txt \ -o %(uploader)s/%(title)s [%(id)s].%(ext)s \ https://www.youtube.com/watch?vxxxxxxxx简单拆解一下参数的含义--cookies cookies.txt登录后导出的Cookie文件。如果你的目标视频有年龄限制或会员限定这一步是必须的。用浏览器插件导出即可建议定期更新因为Cookie会过期。-f参数是格式选择器。bestvideo[height1080][extmp4]bestaudio[extm4a]的意思是分别下载最佳视频流和最佳音频流限制1080p以内优先mp4容器最后合并后面两段是逐级降级的兜底策略避免严格规则下匹配失败直接报错。如果不需要最高画质把1080改成720或480下载体积会大幅缩小。--merge-output-format mp4合并时输出mp4容器。Discord对mp4的兼容性最好。--write-info-json把视频的元数据保存为JSON文件包括标题、描述、上传日期、时长、封面等。这个对后续做索引或检索非常有用。--write-sub --sub-langs zh.*,en.*下载中英字幕支持正则匹配语言代码。--embed-metadata把元数据写入视频文件本身这样传到任何平台都带着干净的信息。--download-archive archive.txt这是一条保命参数。它会把已成功下载的视频ID记录到文件里下次运行会自动跳过这些视频。批量处理大量视频时这个机制能避免重复劳动也能在中断后断点续跑。-o是文件名模板。我用了两级目录加标题加ID的结构%(uploader)s是UP主名%(title)s是视频标题%(id)s是视频ID。保留ID很重要因为标题可能出现重名或特殊字符ID是唯一的。这里特别想说一下模板命名的重要性。很多人下载视频从来不自定义文件名导致归档里全是乱七八糟的命名。但如果你一次下载几百个视频统一命名规则就是检索的生命线。我的规则是“UP主/标题 [视频ID].mp4”既保留了可读性又保留了唯一性后续不管是手动找还是程序处理都很舒服。2.2 媒体处理模块FFmpeg 转码与压缩参数yt-dlp下载下来的文件不一定能直接扔给Discord。几个常见问题原文件是mkv或webm容器Discord不支持预览。视频编码是AV1或VP9部分客户端播放卡顿。文件体积超过Discord单文件25MB上传上限。有些场景你其实只想要音频比如音乐或播客不需要整个视频文件。针对这些情况我在流程里加了FFmpeg处理层按需触发。最常用的三个命令# 转成H.264/AAC的MP4兼容性最好 ffmpeg -i input.mkv -c:v libx264 -preset medium -crf 23 -c:a aac -b:a 128k output.mp4 # 压缩到接近25MB以内假设目标25MB视频时长600秒 # 估算码率25*1024*8/600 ≈ 341kbps减去音频128kbps视频码率约200kbps ffmpeg -i input.mp4 -c:v libx264 -b:v 200k -preset fast -c:a aac -b:a 96k output_small.mp4 # 直接提取音频为mp3 ffmpeg -i input.mp4 -vn -c:a libmp3lame -q:a 2 output.mp3注意码率的计算逻辑。Discord免费版单个附件限制是25MB你想把一个10分钟的视频传上去就需要估算合适的视频码率。公式很简单视频码率(kbps) ≈ (目标大小(MB) × 1024 × 8) ÷ 时长(秒) - 音频码率(kbps)举例时长600秒10分钟的视频目标压到24MB以内音频选96kbps那么视频码率就是24×1024×8÷600-96 ≈ 232kbps。取200kbps比较保险留一点余量。这个数学不用很精确但要心里有数不然压出来还是超25MB来回折腾很浪费时间。“preset”参数影响编码速度和压缩效率的平衡我日常用fast或medium服务器压缩大批量文件时用fast省时间本地精处理时用medium画质保留稍微好一点点。crf是恒定质量参数数值越小质量越高23是一个流传很广的“看不出来区别”档位。2.3 分发模块Discord Webhook 上传实现Discord Webhook用起来很简单本质就是一个HTTP POST请求携带multipart/form-data文件内容即可。我写了一个Python函数来封装上传逻辑import requests import os WEBHOOK_URL https://discord.com/api/webhooks/你的ID/你的token def upload_to_discord(file_path, message): with open(file_path, rb) as f: files {file: (os.path.basename(file_path), f)} data {content: message} resp requests.post(WEBHOOK_URL, filesfiles, datadata) if resp.status_code 200: print(f上传成功: {file_path}) else: print(f上传失败: {resp.status_code} - {resp.text})这段代码不复杂但有几个坑Webhook URL一定要保管好因为任何人都可以通过它往频道发消息和文件泄露了等于频道被外人随意投递。上传前检查文件大小超过25MB就主动走压缩逻辑而不是盲目POST然后等失败。多个文件要上传时建议加个time.sleep(1)。Discord对Webhook有频率限制短时间发太多次会返回429限流。实测下来每条消息之间间隔半秒到一秒基本没问题。用了Webhook之后频道里的分享方式就统一成了一个模式先看到一条文字说明下面跟着文件文件名和目录结构完全一致找资源很方便。3. 实操过程与核心环节实现3.1 环境准备装好三个基础工具整个项目依赖三个东西Python3、yt-dlp、FFmpeg。前两个用包管理器装第三个去官网下对应系统版本。以Ubuntu为例sudo apt update sudo apt install -y python3 python3-pip ffmpeg pip3 install yt-dlp requestsmacOS用户用Homebrewbrew install python ffmpeg yt-dlp装好之后建议先验证一下版本yt-dlp --version ffmpeg -version如果yt-dlp启动报错或者提示版本太老先用pip3 install -U yt-dlp升级到最新版。YouTube的页面结构经常变旧版本很可能解析失败。3.2 目录结构与配置文件我的项目目录长这样zapret/ ├── config.json # 配置文件 ├── cookies.txt # 浏览器导出的Cookie可选 ├── archive.txt # 已下载视频ID记录 ├── download.py # 主调度脚本 ├── upload.py # Discord上传脚本 ├── download/ │ └── ... # 下载完成的文件 └── temp/ └── ... # 中间处理文件config.json里放可变的参数这样改配置不用动代码{ download_dir: download, temp_dir: temp, max_height: 720, prefer_format: mp4, always_compress: false, target_size_mb: 24, webhook_url: https://discord.com/api/webhooks/你的ID/你的token, upload_enabled: true, notify_message: 新资源已入库, links: [ https://www.youtube.com/playlist?listxxxx ] }把链接写在links数组里脚本读取后逐个处理。这样以后往配置文件里加链接就行不用改代码。3.3 主脚本实现串联下载-处理-上传主脚本的逻辑不复杂核心就是按顺序调用yt-dlp和FFmpeg最后上传。我把核心部分贴出来加了必要的异常处理import json import os import subprocess import sys import time from pathlib import Path def load_config(): with open(config.json, r, encodingutf-8) as f: return json.load(f) def run_cmd(cmd): print(执行命令:, .join(cmd)) proc subprocess.run(cmd, capture_outputTrue, textTrue) if proc.returncode ! 0: print(命令执行失败:, proc.stderr) return proc.returncode def download_video(url, cfg): download_dir cfg[download_dir] os.makedirs(download_dir, exist_okTrue) cmd [ yt-dlp, --cookies, cookies.txt, -f, fbestvideo[height{cfg[max_height]}][extmp4]bestaudio[extm4a]/best[height{cfg[max_height]}][extmp4]/best, --merge-output-format, mp4, --write-info-json, --write-sub, --sub-langs, zh.*,en.*, --embed-metadata, --download-archive, archive.txt, -o, f{download_dir}/%(uploader)s/%(title)s [%(id)s].%(ext)s, url ] code run_cmd(cmd) if code ! 0: print(f下载失败: {url}) return code def get_media_info(filepath): 用ffprobe拿视频时长用于估算压缩码率 cmd [ffprobe, -v, quiet, -show_entries, formatduration, -of, csvp0, filepath] result subprocess.run(cmd, capture_outputTrue, textTrue) try: return float(result.stdout.strip()) except ValueError: return 600.0 def compress_video(filepath, target_mb24): 如果文件超过限制则压缩到接近25MB size_mb os.path.getsize(filepath) / 1024 / 1024 if size_mb target_mb: return filepath duration get_media_info(filepath) audio_bitrate 96 video_bitrate int((target_mb * 1024 * 8) / duration - audio_bitrate) if video_bitrate 100: video_bitrate 100 out_path filepath.with_name(filepath.stem _compressed.mp4) cmd [ ffmpeg, -y, -i, str(filepath), -c:v, libx264, -b:v, f{video_bitrate}k, -preset, fast, -c:a, aac, -b:a, f{audio_bitrate}k, str(out_path) ] if run_cmd(cmd) 0: return out_path return filepath这里的关键点在于先用文件大小判断是否需要压缩需要的话用ffprobe拿时长然后按目标码率公式算出合适的视频码率最后调用FFmpeg。这样不会盲目把所有视频都压一遍节省了大量时间。3.4 上传模块与完整调度上传部分的完整调度代码def process_links(): cfg load_config() upload_enabled cfg.get(upload_enabled, True) webhook_url cfg.get(webhook_url, ) for url in cfg[links]: print(f开始处理: {url}) if download_video(url, cfg) ! 0: continue # 找到本次下载生成的文件 download_dir cfg[download_dir] # 这里用一个简单的方式从archive里找最新的记录然后定位文件 # 更严谨的做法是让yt-dlp输出时打印文件路径并捕获 for root, _, files in os.walk(download_dir): for name in files: if not name.endswith(.mp4): continue filepath Path(root) / name if filepath.stat().st_size 0: continue # 跳过可能已上传过的简单看目录名就好了 processed_path compress_video(filepath, cfg.get(target_size_mb, 24)) if upload_enabled and webhook_url: upload_to_discord(processed_path, cfg.get(notify_message, )) # 清理压缩产生的中间文件 if processed_path ! filepath: os.remove(processed_path) time.sleep(1) print(f完成: {url})这段逻辑在真实场景里需要再打磨比如用yt-dlp的--print after_move:filepath输出最终文件路径来定位而不是遍历目录。但思路是通的下载完成后找到文件超了大小就压缩压缩完上传上传完清理中间文件。3.5 跑一次完整流程的实测记录拿一个实际例子来说。我从一个技术频道的播放列表里选了5个视频做测试总时长大约48分钟每个视频时长在8到12分钟不等。第一次跑配置了720p、mp4格式、自动下载中英字幕、上传到Discord测试频道。结果如下5个视频全部下载成功耗时约3分钟取决于网络状况外网环境较好的时候可以跑到每条视频20-30秒。文件总大小680MB平均每个136MB。压缩环节触发3个文件超过24MB压缩后平均每个降到18-22MB。上传环节全部成功5个文件 5条说明消息耗时约15秒。整个流程从开始到结束大概3分半钟全程没有人工干预。如果手动做同样的事光下载就要打开5个网页、复制链接、等解析、手动选格式加上命名、压缩、上传半小时打底。自动化之后时间成本几乎可以忽略。4. 常见问题与排查技巧实录4.1 yt-dlp 下载失败或报错我遇到最多的报错大概有这么几类HTTP Error 403通常是Cookie过期或目标视频有地区限制。解决方案是重新导出Cookie或者临时去掉--cookies参数试试公共视频是否能正常下载。Video unavailable视频被删除、转私密或分区限制。这个是内容侧的问题脚本层面无能为力只能跳过。格式选择失败比如-f匹配不到任何可用的流。这种时候可以把-f参数拆掉改用默认的best牺牲一点画质保证能下载。下载中断网络波动导致的半截文件。好在我用了--download-archive中断后重新跑脚本会跳过已完成的部分也能识别未完成的临时文件。排查思路很简单先不加任何自定义参数用最简单的yt-dlp URL跑一遍看是不是工具本身的问题再逐步加上-f、--cookies等参数定位是哪一步引发报错。4.2 Discord 上传失败与频率限制上传相关的坑也不少。最直接的是文件超过25MBDiscord返回413错误。我的脚本通过压缩环节已经尽量规避了但偶尔遇到特别长的视频比如一个两小时的讲座即使压得再狠体积还是超。这种时候我选择不往Discord传完整视频而是改成只上传音频MP3或者上传前先切割成多段用脚本分段处理和上传。另一个高频问题是429限流。Discord对单个Webhook的限制大概是每分钟30条消息这在批量上传几十个文件时很容易触发。解决方法就是我在脚本里加的那句time.sleep(1)每条消息间隔1秒实测几乎不会再被限流。如果你一次性要传几百个文件间隔改成2秒更保险。4.3 文件名中的特殊字符与路径问题这是个特别容易被忽略的坑。YouTube视频标题里什么字符都有/、\、:、*、?、、、、|其中斜杠在文件系统里是目录分隔符Windows下还有保留字和非法字符。yt-dlp的模板系统已经自动处理了大部分非法字符会把它们替换成下划线或去掉但特殊情况下命名还是会出错。我的建议是模板里一定要保留%(id)s因为标题可能被清理得面目全非但ID是安全且唯一的。另外脚本处理文件名时最好加一层校验把常见的非法字符替换掉避免上传时Discord文件名里出现奇怪的符号导致其他客户端打不开。4.4 字幕和元数据丢失如果你和我一样依赖字幕下载时一定要--write-sub --sub-langs zh.*,en.*。但注意YouTube的字幕是自动生成的准备状态有时候是延迟的刚上传的视频可能没有字幕可以用。这种情况yt-dlp会跳过字幕下载不报错也不影响主视频。元数据方面--embed-metadata会把标题、作者、上传日期、描述URL等写进文件。但并不是所有播放器都会显示所有信息我遇到最直观的问题是某些手机播放器不显示封面和描述只有电脑端的播放器能看全。如果你特别在意这一点可以把--write-info-json生成的JSON文件当备份里面信息最全。5. 工作流的进阶扩展思路核心流程跑通之后其实还能往很多方向扩展。我这里说几个我自己试过或计划中的方向给大家参考。第一接入RSS或频道监控实现“新视频自动入库”。用yt-dlp配合--download-archive再加上定时任务cron或系统计划任务可以每天自动扫描某个频道是否有新视频一旦发现就下载归档并推送到Discord。我目前是在自己的小服务器上挂了个cron每小时跑一次检查之前订阅的频道效果很稳。第二加上文件名规范化插件或重命名规则。比如批量把中文标题里的全角符号转成半角去掉宣传词追加标签等。这个用Python的pathlib很容易实现本质上就是字符串处理。第三对视频内容做索引。利用--write-info-json生成的JSON可以建一个简单的SQLite数据库记录视频标题、ID、上传者、时长、下载日期、频道归属。以后搜索“某个UP主某个主题的视频”直接查库不用翻硬盘。我还在JSON里提取了标题、标签、描述做全文检索配合grep或rg用起来很顺手。第四将整个流程容器化。把yt-dlp、FFmpeg、Python脚本打包进Docker镜像在NAS、路由器或远程服务器上一键跑起来。容器化之后的好处是环境永远一致不会因为系统库版本不同而导致FFmpeg行为不一致。我自己目前正在做这一步主要是为了方便把它部署到朋友的群晖NAS上。这些扩展做完之后这套工具就不再单纯是“下载器上传器”而是一个完整的个人视频资源管理系统。数据从YouTube进来经过加工落到本地和Discord形成可检索、可共享、可持续更新的资源池。6. 写在最后的实操体会整套流程做完我自己最明显的感受是存储工具的价值一半在于技术实现另一半在于命名规范和归档制度。脚本再强如果你下载完不按规则命名、不做好目录归类几个月之后依然是一堆垃圾文件。所以项目里最值得花时间的反而-o模板和archive.txt的设计。另外Discord作为归档目标有一个隐藏优势它的搜索功能很好用消息里的文件名可以直接被搜索到。这就意味着视频传输到频道后你不仅拥有了一个文件备份还拥有了一个可搜索的资源索引。我回找几个月前上传的某个讲座在Discord搜索框输入关键词十几秒就能定位到文件和消息比在本地文件夹里翻目录快得多。这个项目到现在还在持续迭代。最近我又给它加了“自动生成分享摘要”的功能下载完视频后读取描述文件提取前几行作为Discord消息内容配合文件一起发出频道里看起来更专业。如果你也想搭一套类似的个人资源管道我强烈建议从简单的Webhook接文件开始跑通主流程再逐步加模块。先把核心流程用起来比一开始就追求大而全更能坚持下来。