如何在本機上建立一個可以自動下載X上的影片流程,以tampermonkey及cobalt docker搭配進行

【技術實踐】自建 Cobalt Docker 實例:徹底解決 X (Twitter) 影片下載失效問題

身為軟體開發團隊領導與 CS 專業學生,在 X (Twitter) 上收集技術動態或專案靈感時,最常遇到的挫折就是下載腳本頻繁失效。從 Error 812 (Token 過期)HTTP 403 (IP 封鎖),這些問題本質上都是目標網站與公用伺服器之間的對抗。

這篇筆記將分享如何利用家用住宅 IP 自建私有 Cobalt 實例,搭配我微調多次的 Tampermonkey 腳本,打造最穩定的下載環境。


一、 環境準備:Docker 部署私有實例

自建實例最大的優勢在於:你的家用寬頻 IP 不在 X 的資料中心黑名單中,能有效繞過針對 AWS 或 DigitalOcean 等雲端服務商的爬蟲封鎖。

1. 執行 Docker 指令

請在終端機 (Terminal) 執行以下指令。這會確保舊容器被清理並以正確的環境變數重啟:

# 停止並移除舊有的容器
docker stop cobalt-api && docker rm cobalt-api

# 啟動新容器並對應 9000 端口
docker run -d \
  --name cobalt-api \
  -p 9000:9000 \
  -e API_URL="http://localhost:9000/" \
  -e API_DEFAULT_LANG="zh-tw" \
  ghcr.io/imputnet/cobalt:latest

2. 服務驗證

啟動後,瀏覽 http://localhost:9000/。若顯示 JSON 錯誤訊息 "api is for post requests only",代表後端運作正常。


二、 Tampermonkey 腳本配置

此腳本採用 V18 經典橘色外觀,並具備「自動故障轉移」邏輯:優先存取本地實例,若本地未開啟則嘗試公用鏡像站點。

完整腳本內容

// ==UserScript==
// @name         X Video Downloader (V23 Final Local Fix)
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  優先透過本地 Docker 下載,解決 X 全面封鎖問題,修復語法錯誤
// @author       Gemini (CS Major)
// @match        https://x.com/*
// @match        https://twitter.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// @connect      localhost
// @connect      127.0.0.1
// @connect      api.cobalt.tools
// @connect      co.wuk.sh
// ==/UserScript==

(function() {
    'use strict';

    // 實例優先級:本地 Docker -> 備援公用站點
    const API_LIST = [
        "http://localhost:9000/",
        "https://co.wuk.sh/",
        "https://api.cobalt.tools/"
    ];

    const style = document.createElement('style');
    style.textContent = `
        .gm-dl-container { position: absolute !important; top: 10px; left: 10px; z-index: 2147483647; }
        .gm-dl-btn {
            background-color: #ffad1f !important;
            color: #000 !important;
            border: 2px solid #000 !important;
            padding: 4px 10px !important;
            border-radius: 6px !important;
            font-size: 11px !important;
            font-weight: 900 !important;
            cursor: pointer !important;
        }
    `;
    document.head.appendChild(style);

    function inject() {
        const players = document.querySelectorAll('[data-testid="videoPlayer"]:not(.gm-v23)');
        players.forEach(player => {
            player.classList.add('gm-v23');
            const wrap = document.createElement('div');
            wrap.className = 'gm-dl-container';
            const btn = document.createElement('button');
            btn.className = 'gm-dl-btn';
            btn.textContent = '📥 下載影片';

            btn.onclick = (e) => {
                e.preventDefault(); e.stopPropagation();
                const article = player.closest('article');
                const link = article?.querySelector('a[href*="/status/"]');
                const rawUrl = link ? link.href : window.location.href;
                
                // 清洗追蹤碼並規範化網域
                const cleanUrl = rawUrl.split('?')[0].replace('x.com', 'twitter.com');
                startDownloadFlow(cleanUrl, btn, 0);
            };
            wrap.appendChild(btn);
            player.appendChild(wrap);
        });
    }

    function startDownloadFlow(url, btn, idx) {
        if (idx >= API_LIST.length) {
            alert("❌ 所有實例均無法突破 X 的封鎖。請檢查 Docker 狀態或更換 IP。");
            resetBtn(btn);
            return;
        }

        const endpoint = API_LIST[idx];
        const serverName = new URL(endpoint).hostname;
        btn.textContent = `⌛ 站點: ${serverName}...`;

        GM_xmlhttpRequest({
            method: "POST",
            url: endpoint,
            headers: { "Content-Type": "application/json", "Accept": "application/json" },
            data: JSON.stringify({ url: url, videoQuality: "1080" }),
            timeout: 12000,
            onload: function(response) {
                try {
                    const res = JSON.parse(response.responseText);
                    if (res.status === "stream" || res.status === "redirect") {
                        GM_download({
                            url: res.url,
                            name: `X_Video_${Date.now()}.mp4`,
                            onload: () => { 
                                btn.textContent = '✅ 完成'; 
                                setTimeout(() => resetBtn(btn), 3000); 
                            }
                        });
                    } else {
                        startDownloadFlow(url, btn, idx + 1);
                    }
                } catch (e) {
                    startDownloadFlow(url, btn, idx + 1);
                }
            },
            onerror: () => startDownloadFlow(url, btn, idx + 1)
        });
    }

    function resetBtn(btn) { btn.textContent = '📥 下載影片'; btn.disabled = false; }

    setInterval(inject, 2000);
    new MutationObserver(inject).observe(document.body, { childList: true, subtree: true });

})();

三、 常見問題排除 (Troubleshooting)

  • 連線失敗: 第一次執行時,請確認是否有彈出視窗詢問「跨網域請求權限」,並點擊「總是允許」。
  • 語法錯誤 (SyntaxError): 此版本已改用標準 createElement 邏輯,能避免節點注入時的解析衝突。
  • API 無法獲取連結: X 的封鎖具有週期性。若私有實例也失效,建議更換手機 5G 熱點試試,家用/手機 IP 是突破封鎖的最強武器。

結語: 在開發與 PM 的日常中,這類工具的維護雖然繁雜,但自建服務帶來的穩定性與掌控感是無價的。希望這篇筆記能讓你在資訊收集的路上更加順遂。

留言

此網誌的熱門文章

草屯美食深度指南_2025年版