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 statusframework、build step、PWA、share target
部署 (reclip.sh, 49 行 + Dockerfile, 15 行)venv auto-setup、prerequisite check、Docker one-shotsystemd、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 appreclip.sh 開頭 abort,提示安裝
yt-dlp真正下載影片同上
ffmpegmerge 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 環境變數

變數預設說明
PORT8899Flask 監聽 port
HOST127.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

MethodPath行 (app.py)功能
GET/76–78回傳 index.html
POST/api/info81–124yt-dlp -j <url> 抓 metadata → 整理出 formats[]
POST/api/download127–145建 job、spawn daemon thread 跑 run_download
GET/api/status/<job_id>148–157回傳 {status, error, filename}
GET/api/file/<job_id>160–165send_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 拆解。三個常見坑:

症狀原因解法
啟動立刻 abortffmpegbrew install ffmpeg
YouTube 全部 403yt-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 為 MP3MP3 audio mode注意版權
批次下載 playlist⚠️ 不支援 — 程式碼用 --no-playlist (line 20, 88)改用 yt-dlp CLI
NAS 自架docker-compose + reverse proxy(加 basic auth)務必加 auth + 限 IP(見 §6)
抓會議錄影 (Loom / Streamable)直接貼 share URLyt-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.1app.py line 170)localhost only,不暴露公網
🟢前端 XSS 用 esc() 包過(textContent → innerHTML,line 432–436)防 yt-dlp metadata 注入
🟢無 hardcoded secret / token / API keyrepo 乾淨
🟢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 加固建議

若要對外提供:

  1. 前置 nginx / caddy + basic auth + Let’s Encrypt
  2. Docker 加 --read-only --tmpfs /tmp + --cap-drop=ALL
  3. 加 rate limit(每 IP 每分鐘 N 個 job)
  4. URL scheme 白名單(只允許 https:// 開頭、已知 host)
  5. 跑在 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 行 Pythonapp.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.sh 90% 解決
  • 無 auth / queue / rate limit — 公網暴露 = 開放 RCE proxy
  • License: MIT — 可 fork / 商用,但內容版權要自負

§11 進一步閱讀