视频下载这件事,桌面上从来不缺工具,缺的是「干净」。要么弹窗、要么假按钮、要么把页面劫持到广告站。GitHub 上由 Pablo Stanley 开源的 yoinks(4.3k⭐、MIT、TypeScript)走了一条完全相反的路:把 yt-dlp 塞进一个用 React 写的全屏 TUI,敲回车就下载,零干扰、零脚本、零浏览器扩展。

本文把这套工具的安装命令、核心特性、底层架构、Roadmap 一次拆完。

项目地址:https://github.com/pablostanley/yoinks
许可证:MIT
技术栈:Node 18+ + Ink(React for terminal)+ yt-dlp + ffmpeg-static
当前版本:0.3.1(2026-07-17)


一、它到底解决了什么问题

视频下载工具并不稀缺,稀缺的是「不污染你」的工具。市面上常见的方案都有硬伤:

方案 痛点
浏览器在线下载站 满屏广告、假按钮、跳转追踪、捆绑安装
浏览器扩展 需要为每种浏览器装一次,权限大、隐私风险高
Python 命令行(yt-dlp 原生) 需要装 Python、终端界面简陋、参数表长
桌面客户端 体量大、跨平台分裂、经常塞推广

yoinks 想做的是装一行 npm 就跑、全屏终端 UI、零广告的实现。它直接站在 yt-dlp 这个”已经支持 1800+ 站点的成熟下载引擎”肩膀上,把下载的人本身做成一份有质感的终端应用:

  • 没有 Python 依赖(Node 18+ 即可)
  • 没有”哪个分辨率下哪个参数”的记忆负担(UI 里直接选)
  • 没有假按钮、推广、捆绑(MIT、纯终端、无浏览器污染)

yoinks 主界面:粘贴链接、回车下载

二、三分钟跑起来

1. 一行安装

1
npm install -g yoinks

或者不想装全局:

1
npx yoinks

要求系统装了 Node 18+。yt-dlp 和 ffmpeg 都不用自己准备——yoinks 第一次运行时会把独立 yt-dlp 二进制下载到 ~/.yoinks/bin;ffmpeg 会从 PATH 找,没有就用 ffmpeg-static 兜底。

2. 三种启动方式

1
2
3
yoinks https://youtu.be/dQw4w9WgXcQ    # 直接进入下载器,没有就不问
yoinks # 不带 URL,进入输入页
yoinks --theme light # 强制浅色主题(auto/light/dark)

支持站点包括但不限于:

  • YouTube / YouTube Music
  • X / Twitter
  • Instagram
  • Threads
  • TikTok
  • Vimeo / Twitch / Reddit / Facebook
  • 以及 yt-dlp 支持的 1800+ 其他站点

3. 五秒操作流程

下图是选择下载格式的全屏 UI——左侧是视频标题 + 平台标签 + 时长 + UP 主,右侧是格式列表,每行都标了分辨率 + 估算大小,末行是音频 only:

yoinks 下载格式选择界面:分辨率 + 估算大小 + 音频 only

1
2
3
4
5
6
7
8
9
yoinks https://youtu.be/xxx
↓
[probe] 拉取视频元数据(标题、时长、分辨率列表)
↓
[picking] 全屏 UI 列出分辨率 + 估算文件大小 + 音频 only
↓ ↑↓ 选择,↵ 确认,esc 返回,^t 切主题
[downloading] 进度条 + 速度 + 剩余时间 + part N/M
↓
[done] 输出文件路径

整个过程对电脑和手机都是键盘 + 鼠标都能用:

  • 键盘:↑↓ 或 j/k 或数字键选格式,↵ 下载,esc 返回,^c 退出,^t 切换主题
  • 鼠标:yoink 按钮、格式列表、底部快捷键提示、Logo 全部可点(点击 Logo 直接回首页)

文件默认保存到 ~/Downloads,完成后在终端打印 ✓ yoinked → <文件路径>。

三、核心特性拆解

下面这些是 README 没全展开、但源码里实打实存在的功能。

1. 全屏 TUI,进入即接管

进入 yoinks 后,终端会切换到 alternate screen(\x1b[?1049h),整个屏幕交给 Ink 渲染,退出时恢复 scrollback。src/cli.tsx 里同时为 uncaughtException / unhandledRejection 注册了退出前强制还原屏幕的逻辑,避免崩溃时栈跟踪被 alternate screen 一起擦掉、看起来像静默崩溃。

1
app.tsx 里 phase 是个 discriminated union: 'input' | 'probing' | 'picking' | 'downloading' | 'done' | 'error'

整个生命周期是一个有限状态机,每种 phase 对应一组快捷键提示(HINTS 字典),底部 <Shortcuts> 组件统一渲染。

2. 三档主题:auto / light / dark

src/theme.ts 维护 auto light dark 三个主题对象:

  • auto(默认):不设颜色,让 ANSI 颜色跟终端原生前景/背景。比”尝试检测终端深浅色”更靠谱,避免误判。
  • light / dark:显式指定调色板(#18181b 主色,#52525b 灰等)。

按 ^t 或点击底部主题按钮可在三档之间循环。--theme auto|light|dark 可为单次启动指定初始主题。

3. 解析度信息自动对齐:probe 元数据复用

这是 yoinks 性能上最聪明的一笔。下载流程是两步:

  1. probe:ytdlp -J --no-playlist --no-warnings <url> 拿 JSON 元数据,顺便写到 os.tmpdir()/yoinks-info-<pid>-<ts>.json
  2. download:直接传 --load-info-json <path> 给 yt-dlp,跳过重新提取媒体 URL

媒体直链经常有过期时间,第一次失败时 yoinks 会自动 refreshing: true 状态重试一次,用 URL 重跑而非缓存 JSON,所以即便链接过期也不会卡死。

4. 自定义进度模板:让 yt-dlp 输出能解析

yt-dlp 默认进度输出是一段自然语言,yoinks 用 --progress-template 让它输出机器可解析的格式化串:

1
download:YOINK|<downloaded>|<total>|<estimate>|<speed>|<eta>

然后在 lib/ytdlp.ts 里逐行切割 buffer、以 YOINK| 前缀识别:

1
2
3
4
5
if (line.startsWith(PROGRESS_PREFIX)) {
const [downloaded, total, totalEstimate, speed, eta] =
line.slice(PROGRESS_PREFIX.length).split('|')
// 推给 React:让进度条、速度、ETA 实时更新
}

进度条宽度按当前显示长度固定槽位(speed.padStart(10) eta.padEnd(12)),所以进度更新时整体布局不会抖——这在 Ink 的 yoga 布局下是个常见坑。

5. 鼠标点击 hit-testing:靠 frame 内容反查

ink 没有”绝对坐标”API,每个组件的命中区得自己算。yoinks 的做法很特别:

把 ink 每帧写出去的 stdout 缓存一份,点击时按文本内容反查哪一行是哪个可点击元素。

实现见 src/lib/click-map.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export function captureFrames<T extends NodeJS.WriteStream>(stream: T): T {
return new Proxy(stream, {
get(target, prop) {
if (prop === 'write') {
return (chunk, ...rest) => {
const lines = String(chunk).split('\n').map(stripAnsi)
if (lines.some(l => l.trim() !== '')) frameLines = lines
return (target.write as never)(chunk, ...rest)
}
}
// ...
},
})
}

命中检查时遍历候选 ClickTarget,按 match 字符串在 frameLines 里 indexOf,加上 padX/padY 容差。这样不需要维护任何”组件 → 矩形”的布局数学,布局改了不影响命中,代码量极小。

6. 平台识别 + URL 校验

src/lib/platforms.ts 维护了一份知名平台 → 显示名映射:

hosts 平台
youtube.com / youtu.be / music.youtube.com YouTube
x.com / twitter.com X / Twitter
instagram.com Instagram
threads.net / threads.com Threads
tiktok.com TikTok
vimeo.com / twitch.tv / reddit.com / facebook.com / fb.watch …

URL 校验用 new URL() 校验协议(只接受 http: / https:),避免把”看起来像 URL”的脏字符串(比如多行剪贴板内容)当 URL 处理。

7. 剪贴板预检测:Tab 一键粘贴

不带 URL 启动时,yoinks 会用 execFileSync 调平台命令读剪贴板:

平台 命令
macOS pbpaste
Windows powershell Get-Clipboard
Linux wl-paste / xclip / xsel(按顺序尝试)

剪贴板里有合法 URL 时,输入框下方会提示「link in your clipboard — ⇥ to paste it」,按 Tab 一键填入。剪贴板内容会做单行校验(/\s/.test()),避免 new URL() 静默吃掉换行后误判。

8. 历史记录

输入过的 URL 会被存到本地(src/lib/history.ts 的 addToHistory / loadHistory),下次启动 ↑ 方向键可在历史里翻。文件位置和具体持久化格式以源码为准。

9. 取消清理 part 文件

downloading 阶段按 esc 或 ^c 取消时,ytdlp 子进程会被 abort signal 跟踪到过的 AbortController 中断,然后 removePartials() 把 yt-dlp 写过的 .part / .ytdl 半成品清掉,不会留垃圾。

process.on('exit', () => activeChild?.kill('SIGTERM')) 保证即便 React effect 没清理,yt-dlp 子进程也不会变孤儿。

10. ffmpeg 自动寻路

findFfmpeg() 顺序:

  1. PATH 里有 ffmpeg → 返回 undefined(让 yt-dlp 自己找)
  2. PATH 里没有 → 加载 ffmpeg-static,验证可执行后返回绝对路径
  3. 都没有 → 返回 undefined(yt-dlp 仍能下载单文件格式,但合并 / mp3 提取就跳过了)

四、底层架构:从代码看工程质感

1. 目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
src/
├── app.tsx # 主 React 组件,状态机
├── cli.tsx # CLI 入口,alternate screen / 异常恢复
├── theme.ts # auto/light/dark 主题 + Context
├── components/
│ ├── fullscreen.tsx # 居中容器,监听 stdout resize
│ ├── framed-input.tsx # 带边框的输入框
│ ├── panel.tsx # 下载选择面板
│ ├── progress-bar.tsx # 固定宽度进度条
│ ├── shortcuts.tsx # 底部快捷键提示
│ ├── text-input.tsx # URL 输入 + 历史
│ └── logo.tsx # SVG Logo
└── lib/
├── args.ts # 命令行参数解析
├── clipboard.ts # 跨平台剪贴板读取
├── click-map.ts # frame 缓存 + hit-testing
├── format.ts # 字节/时长/速度格式化
├── history.ts # URL 历史
├── platforms.ts # 平台识别
├── use-mouse-click.ts # 鼠标点击 hook
└── ytdlp.ts # yt-dlp 包装层(核心)

2. 关键设计取舍

异步 vs 同步:检测 yt-dlp --version 这种”必须”检查,用 child_process.spawn + Promise 异步跑。注释里明确写了”async on purpose: a spawnSync here blocks the event loop, which freezes ink mid-frame”——这就是 Ink + sync spawn 的经典坑。

布局稳定:下载阶段每个分支都固定三行(bar / gap / meta),保证状态切换时不会跳位。Gap 组件用显式 <Text> </Text> 行而非 <Box height={1}/> 空盒,因为 yoga 会优先压缩空盒 spacer。

Alt screen 恢复:exit 事件 + uncaughtException/unhandledRejection 都注册了 leaveAltScreen(),崩溃时栈跟踪能正常显示。

OS-aware 文件名:默认输出模板是 %(title).60s.%(ext)s,避免文件名过长触发文件系统错误。

3. 测试与质量门禁

1
2
3
4
5
6
7
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"test": "tsx --test src/**/*.test.ts",
"typecheck": "tsc --noEmit",
"prepublishOnly": "npm test && npm run typecheck && npm run build"
}

发布前强制走完「测试 + 类型检查 + 构建」三关。tsconfig 用 strict: true + ES2022 + react-jsx,模块系统走 Bundler。

五、Roadmap:接下来会加什么

README 列出的待办:

  • --best / --mp3 flags,跳过 picker 直接下载(脚本化模式)
  • -o <dir> 自定义输出目录
  • 播放列表 / 多视频 Threads 支持
  • 不带参数启动时自动建议剪贴板 URL(部分已实现)
  • 内置 yt-dlp 自更新(yt-dlp -U)
  • 发布到 npm(已完成)
  • curl yoinks.sh | sh 一键安装脚本

其中「剪贴板启动时自动建议」已经做了检测,但没自动填入——刻意保留一个 Tab 按键,避免误触发消耗输入。

七、判断:什么时候用 yoinks

适合:

  • 经常从 YouTube / X / Threads / Instagram / TikTok 下载视频保存到本地
  • 想在终端里直接完成”复制链接 → 下载”全链路,不开浏览器
  • 不喜欢 yt-dlp 原生 CLI 的参数表,记不住 -f bv*+ba/b 这种 selector
  • Node 开发者、终端控,对工具链有审美要求
  • 需要在脚本里调用(等 --best / --mp3 落地后会更顺手)

不适合:

  • 想批量下载整个频道 / 播放列表(目前未支持,Roadmap 中)
  • 想抓取需要登录的内容(和 yt-dlp 一样需要自己带 cookie)
  • 找能转码 / 剪辑 / 加字幕的全功能工具(yoinks 只做下载,剪辑请接 ffmpeg 或剪辑软件)
  • Windows 旧版本用户(要求 Node 18+,且 ffmpeg-static 对 Win7 之前的支持不一定全)

八、合规提醒

yoinks 是一个个人归档工具。README 明确说:

Downloading content may violate a platform’s terms of service — only download what you have the right to keep, and be excellent to creators.

简单说就是:只下载自己有权利保留的内容,不要拿去做二次分发,尊重创作者。这点和 yt-dlp、youtube-dl、gallery-dl、wx_channels_download 等同类工具的立场一致。

九、总结

yoinks 不是一个”颠覆性”的工具——它的下载能力完全来自成熟的 yt-dlp。它的真正价值是把”装 Python、记参数表、看错误日志”这套流程打包成一份三分钟能上手、有质感、零干扰的终端体验:

  • 底层:yt-dlp + ffmpeg-static,1800+ 站点覆盖
  • 界面:Ink 全屏 TUI,鼠标+键盘双操作,三档主题
  • 工程:progress-template 解析进度、frame 内容反查做 hit-testing、info.json 复用加速、取消自动清理
  • 分发:npm i -g yoinks 或 npx yoinks,无需 Python

如果你是终端里干活的人,且经常从网上保存视频,yoinks 是目前最值得装的那一个。

延伸阅读:想了解另一个”为下载这件事做工程化”的工具,可以看 微信视频号下载神器 wx_channels_download 全解析——它走的是 MITM + 注入下载按钮路线,覆盖的站点完全不同;想了解 AI 工具链怎么省 Token,可以看 Codex(ChatGPT) 半年更新盘点:4 个省 Token 硬技巧 + 8 个新奇玩法;想看更多 GitHub 开源周榜的玩法切片,可以看 GitHub 9 月第一周开源热榜 Top 5。

欢迎在评论区聊聊你用 yoinks 录下的第一个视频、或者你常用的另一个下载神器。