ReClip 完整教學 — Self-Hosted 影音下載器(Flask + yt-dlp)
把 yt-dlp 的全部能力包成「貼 URL → 下載 MP4 / MP3」的網頁介面,後端 ~170 行 Python,零前端 framework。
§1 專案定位
ReClip = yt-dlp 的 GUI 包裝層。它不重寫下載邏輯(那是 yt-dlp 的工作),不做檔案管理(直接 send_file 給瀏覽器),不做 user system(純單機)。整個專案的價值在於:
| 層級 | 做什麼 | 不做什麼 |
|---|---|---|
後端 (app.py, 171 行) | Flask 包 yt-dlp subprocess、uuid 管 job、threading 跑下載 | DB、auth、queue、retries、persistent storage |
前端 (templates/index.html, 689 行) | vanilla JS、fetch 呼叫 4 個 API、poll status | framework、build step、PWA、share target |
部署 (reclip.sh, 49 行 + Dockerfile, 15 行) | venv auto-setup、prerequisite check、Docker one-shot | systemd、reverse proxy、TLS、multi-user |
適合的人:
- 想自架不依賴 SaaS、不想看廣告、不想被限流
- 願意接受「重啟就忘記所有 job」(jobs 存記憶體 dict)
- 信任在 localhost 跑 subprocess
不適合的人:
- 想公開部署給多人用(沒 auth、沒 rate limit、沒沙箱)
- 需要長期 archive workflow(無 job persistence)
- 想自訂下載邏輯(請直接用 yt-dlp CLI)
§2 安裝指南
2.1 三條路線
flowchart TD
A[選擇安裝方式] --> B{你有什麼?}
B -->|macOS + brew| C[brew 路線]
B -->|Linux + apt| D[apt 路線]
B -->|想隔離環境| E[Docker 路線]
C --> C1[brew install yt-dlp ffmpeg]
C1 --> F[git clone && cd reclip]
D --> D1[sudo apt install ffmpeg]
D1 --> D2[pip install yt-dlp]
D2 --> F
F --> G[./reclip.sh]
G --> H[http://localhost:8899]
E --> E1[docker build -t reclip .]
E1 --> E2[docker run -p 8899:8899 reclip]
E2 --> H
2.2 三個必要工具
| 工具 | 用途 | 沒裝會怎樣 |
|---|---|---|
python3 | 跑 Flask app | reclip.sh 開頭 abort,提示安裝 |
yt-dlp | 真正下載影片 | 同上 |
ffmpeg | merge video+audio、轉 MP3 | 影片可下載但無音軌、無 MP3 |
2.3 啟動腳本做了什麼(reclip.sh)
1#!/bin/bash
2set -e
3cd "$(dirname "$0")" # 切到 script 所在目錄
4
5# 1. 檢查 python3 / yt-dlp / ffmpeg → 缺一即 abort
6# 2. 若無 venv/,建立並 pip install flask yt-dlp
7# 3. source venv/bin/activate
8# 4. PORT=${PORT:-8899}(環境變數可覆寫)
9# 5. python3 app.py
關鍵設計:每次啟動都不會自動升級 yt-dlp。要更新解 YouTube 反爬 → rm -rf venv && ./reclip.sh。
2.4 環境變數
| 變數 | 預設 | 說明 |
|---|---|---|
PORT | 8899 | Flask 監聽 port |
HOST | 127.0.0.1 | 預設 localhost only;公網開放設 0.0.0.0(注意 §6 風險) |
§3 核心架構解析
3.1 後端結構(app.py)
flowchart LR
subgraph Browser
UI[index.html
vanilla JS]
end
subgraph Flask app.py
R1[GET /
render_template]
R2[POST /api/info
yt-dlp -j]
R3[POST /api/download
uuid + Thread]
R4[GET /api/status/<id>
poll jobs dict]
R5[GET /api/file/<id>
send_file]
JOBS[(jobs dict
in-memory)]
end
subgraph Subprocess
YTDLP[yt-dlp + ffmpeg]
DL[downloads/]
end
UI -->|paste URL| R2
R2 --> YTDLP
UI -->|click Download| R3
R3 --> JOBS
R3 -.thread.-> YTDLP
YTDLP --> DL
UI -->|poll 1s| R4
R4 --> JOBS
UI -->|done| R5
R5 --> DL
3.2 四個 API endpoint
| Method | Path | 行 (app.py) | 功能 |
|---|---|---|---|
| GET | / | 76–78 | 回傳 index.html |
| POST | /api/info | 81–124 | yt-dlp -j <url> 抓 metadata → 整理出 formats[] |
| POST | /api/download | 127–145 | 建 job、spawn daemon thread 跑 run_download |
| GET | /api/status/<job_id> | 148–157 | 回傳 {status, error, filename} |
| GET | /api/file/<job_id> | 160–165 | send_file(as_attachment=True) |
3.3 job lifecycle
sequenceDiagram
participant UI as Browser
participant API as Flask
participant TH as Thread
participant YT as yt-dlp
UI->>API: POST /api/download
API->>API: job_id = uuid.uuid4().hex[:10]
API->>API: jobs[id] = {status: downloading}
API-->>UI: {job_id}
API->>TH: Thread(run_download).start()
TH->>YT: subprocess.run(["yt-dlp", ...])
Note over UI,API: UI polls /api/status every 1s
YT-->>TH: returncode
alt success
TH->>TH: glob job_id.* → pick chosen
TH->>TH: jobs[id]["status"] = "done"
else error
TH->>TH: jobs[id]["status"] = "error"
end
UI->>API: GET /api/file/<job_id>
API-->>UI: send_file(chosen, download_name=safe_title)
3.4 前端(templates/index.html, 689 行)
- 單檔含 inline CSS(~370 行)+ inline JS(~250 行)
- Font from Google Fonts CDN(
Instrument Serif/DM Mono) - 無 framework、無 build、無 service worker
- 有 XSS 防護:
esc()function 用textContent + innerHTML來逃逸(line 432–436)
§4 Helper Scripts 詳細用法
ReClip 只有兩個「腳本」檔,都很短:
4.1 reclip.sh(49 行)
完整流程已在 §2.3 拆解。三個常見坑:
| 症狀 | 原因 | 解法 |
|---|---|---|
| 啟動立刻 abort | 缺 ffmpeg | brew install ffmpeg |
| YouTube 全部 403 | yt-dlp 太舊 | rm -rf venv && ./reclip.sh |
| port 已被佔用 | 預設 8899 衝突 | PORT=9999 ./reclip.sh |
4.2 Dockerfile(15 行)
1FROM python:3.12-slim
2RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
3WORKDIR /app
4COPY requirements.txt .
5RUN pip install --no-cache-dir -r requirements.txt
6COPY . .
7EXPOSE 8899
8ENV HOST=0.0.0.0
9CMD ["python", "app.py"]
注意:Docker 預設 HOST=0.0.0.0,container 內部 listen 所有介面;對外 expose 用 -p 控制。但不建議直接 -p 0.0.0.0:8899:8899 開到公網(見 §6)。
掛載 downloads 目錄:
1docker run -p 127.0.0.1:8899:8899 \
2 -v $(pwd)/downloads:/app/downloads \
3 reclip
§5 應用場景
| 場景 | 怎麼用 | 注意 |
|---|---|---|
| 個人保存 YouTube 教學 | 貼 URL → MP4 1080p | 教育用、合理使用 |
| 抓 podcast / SoundCloud 為 MP3 | MP3 audio mode | 注意版權 |
| 批次下載 playlist | ⚠️ 不支援 — 程式碼用 --no-playlist (line 20, 88) | 改用 yt-dlp CLI |
| NAS 自架 | docker-compose + reverse proxy(加 basic auth) | 務必加 auth + 限 IP(見 §6) |
| 抓會議錄影 (Loom / Streamable) | 直接貼 share URL | yt-dlp 自動處理 |
| 抓 IG / Threads 短影音 | 直接貼 post URL | 私密帳號可能需要 cookies(見 Issue #32 workaround) |
§6 資安掃描報告
掃描範圍:
app.py/reclip.sh/Dockerfile/templates/index.html。 工具:grep -rn -E "eval|exec|os\.system|subprocess|shell=True|fetch|secret|api_key|password|token|innerHTML|eval|XMLHttpRequest"
6.1 紅黃綠燈總結
| 等級 | 項目 | 影響 |
|---|---|---|
| 🟢 | subprocess.run(cmd, ...) 全部用 list 形式(cmd = ["yt-dlp", ..., url]),不用 shell=True | 無 shell injection 風險 |
| 🟢 | 無 eval() / exec() / pickle / os.system | 不引入動態執行 |
| 🟢 | 預設 HOST=127.0.0.1(app.py line 170) | localhost only,不暴露公網 |
| 🟢 | 前端 XSS 用 esc() 包過(textContent → innerHTML,line 432–436) | 防 yt-dlp metadata 注入 |
| 🟢 | 無 hardcoded secret / token / API key | repo 乾淨 |
| 🟢 | MIT license,無 telemetry 回傳 | 純本機 |
| 🟡 | URL 直接餵 yt-dlp subprocess(無 URL scheme 白名單) | yt-dlp 自身能擋大多數,但 --exec / 自訂 extractor 若被未來社群擴充可能放大攻擊面 |
| 🟡 | jobs 是進程內 dict,無清理機制 | 長期跑會 OOM;不適合多人共用 |
| 🟡 | /api/file/<job_id> 用 10 字元 hex UUID(~40 bit entropy) | 內部單機用安全;公網暴露 + 多人併發理論上可暴力枚舉 |
| 🟡 | safe_title 截斷到 20 字元(line 64) | 不是 path traversal 漏洞(用 download_name=,但長 title 會被截短) |
| 🔴 | 若部署到公網(HOST=0.0.0.0 無 auth)= 開放 RCE-via-yt-dlp + SSRF | 任何人可餵任意 URL;攻擊者可丟內網 URL(http://169.254.169.254/ AWS metadata)試探;可塞大檔耗盡磁碟;可丟惡意檔誘騙 yt-dlp 後處理執行 |
6.2 結論
🟡 中等風險,且風險高度取決於部署方式:
- 本機自用 + Docker 綁 127.0.0.1:🟢 安全;可放心
- LAN 內 NAS 自架 + 加 basic auth + 限 IP:🟢 可接受
- 直接
HOST=0.0.0.0開公網無 auth:🔴 絕對不要- 雖然 yt-dlp 本身有 sanitization,但 attack surface 是「任意外部 URL 餵給能執行 ffmpeg 後處理的 subprocess」
- 歷史上 yt-dlp 與類似工具都出過 RCE CVE(如 CVE-2023-40581 yt-dlp 模板 RCE)
- 你會變成 open proxy + 攻擊跳板
6.3 加固建議
若要對外提供:
- 前置 nginx / caddy + basic auth + Let’s Encrypt
- Docker 加
--read-only --tmpfs /tmp+--cap-drop=ALL - 加 rate limit(每 IP 每分鐘 N 個 job)
- URL scheme 白名單(只允許
https://開頭、已知 host) - 跑在 unprivileged user、
downloads/用獨立磁碟限額
§7 FAQ
Q1:為什麼貼 YouTube URL 全部 403?
A:yt-dlp 太舊,YouTube 改 SABR streaming(2026 起)。rm -rf venv && ./reclip.sh 重新拉 yt-dlp(Issue #8 確認解法是 ≥ 2026.3.17)。
Q2:能下載 playlist 嗎?
A:不行,app.py line 20 / 88 都帶 --no-playlist。改用 yt-dlp CLI yt-dlp <playlist-url>。
Q3:能加 cookies 抓需登入的內容?
A:原生不支援。Workaround 見 Issue #32:手動 docker cp cookies.txt container:/app/cookies.txt + 改 app.py 加 --cookies 參數。
Q4:MP3 mode 抓不到?只有影片沒聲音?
A:通常是 ffmpeg 沒裝(reclip.sh 啟動時應該已擋,但 Docker 有時忘記)。檢查 ffmpeg -version。
Q5:能跑在遠端伺服器並從筆電連嗎?
A:可以,但不要直接公網。SSH port forward 最安全:ssh -L 8899:localhost:8899 user@server,再連 http://localhost:8899。
Q6:下載到一半重啟 server 會怎樣?
A:所有 job 都消失(in-memory dict)。檔案可能還在 downloads/ 但無法從 UI 重新拿到。
Q7:可以同時下載多少? A:理論無限(每個 download spawn 一個 daemon thread + subprocess)。實務上同時 >5 個會吃滿 CPU / 網路;無 queue。
Q8:下載檔案放哪?
A:<repo>/downloads/<uuid>.<ext>,Docker 內是 /app/downloads/。.gitignore 已排除。
§8 進階技巧
8.1 自訂 yt-dlp 參數
修改 app.py line 20 / 25 / 27 的 cmd list。例如加字幕:
1cmd = ["yt-dlp", "--no-playlist", "--write-subs", "--sub-langs", "en", "-o", out_template]
8.2 加 cookies 支援
1cookies_file = os.path.join(os.path.dirname(__file__), "cookies.txt")
2if os.path.exists(cookies_file):
3 cmd += ["--cookies", cookies_file]
8.3 加 download queue(避免並發爆炸)
把 threading.Thread 換成 concurrent.futures.ThreadPoolExecutor(max_workers=2),並把 jobs 改成持久化(SQLite / Redis)。
8.4 加 progress callback
yt-dlp 支援 --newline --progress-template '%(progress.downloaded_bytes)s/%(progress.total_bytes)s',可以從 stdout 解析後寫進 jobs[id]["progress"],前端 polling 同步顯示百分比。
8.5 集中所有下載到 NAS
1docker run -p 127.0.0.1:8899:8899 \
2 -v /mnt/nas/media-downloads:/app/downloads \
3 --restart=unless-stopped \
4 reclip
§9 整合進其他工作流
9.1 配 Tailscale + iOS shortcut
- Tailscale 自架 → ReClip 跑在 NAS 上、家用裝置同網
- iOS Shortcut「Share Sheet → Get URL → POST to /api/download」
- 在 YouTube app / Twitter app 直接「分享 → ReClip」一鍵下載到 NAS
9.2 配 cron 自動清理
1# 每小時清掉 24h 前的下載檔
20 * * * * find /path/to/reclip/downloads -mmin +1440 -delete
9.3 配 Caddy reverse proxy + basic auth
1reclip.home.lan {
2 basicauth {
3 me $2a$14$...bcrypt...
4 }
5 reverse_proxy 127.0.0.1:8899
6}
9.4 配 RSS(YouTube 頻道自動下載)
- 用
youtube-dl-server或自寫 cron 每天yt-dlp --download-archive archive.txt <channel-rss> - ReClip 只負責「臨時、互動式」下載,不是 archive solution
9.5 與本 knowledge template 的關係
- 把 ReClip 抓到的 Loom / 教學影片放到
inbox/→ 用docling抽逐字稿 → 用paper-qa-lite做問答 RAG - ReClip 是「取得知識的入口 (knowledge ingestion entry)」,後續加工交給 docling / paper-qa-lite
§10 重點摘要 Checklist
- 後端只有 ~170 行 Python —
app.py全讀完不到 5 分鐘 - 核心邏輯就是
subprocess.run(["yt-dlp", ...])— 沒有任何下載演算法在本專案 -
jobs是進程內 dict — 重啟丟失、不適合多人 - 預設
HOST=127.0.0.1— Docker 預設改0.0.0.0要小心 - 不支援 playlist — 寫死
--no-playlist - YouTube 失效 = yt-dlp 版本過舊 —
rm -rf venv && ./reclip.sh90% 解決 - 無 auth / queue / rate limit — 公網暴露 = 開放 RCE proxy
- License: MIT — 可 fork / 商用,但內容版權要自負
§11 進一步閱讀
- 上游倉庫:https://github.com/averygan/reclip
- yt-dlp 文件:https://github.com/yt-dlp/yt-dlp/blob/master/README.md
- yt-dlp 支援站點:https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md
- ffmpeg:https://ffmpeg.org/
- Flask
send_file:https://flask.palletsprojects.com/en/3.0.x/api/#flask.send_file - 相關工具比較:
- MeTube:https://github.com/alexta69/metube(Angular + aiohttp,功能更多但較重)
- yt-dlp-webui:https://github.com/marcopiovanello/yt-dlp-web-ui(Go + React,類似但較大)
- ReClip 的位置:「最小、最直接、最易讀」,適合當作學習 Flask + subprocess 的範本
- SABR streaming 背景:https://github.com/yt-dlp/yt-dlp/issues/12482
Comments