Twitch VOD 自動匯出助手 By Minidoracat

輔助將 Twitch VOD 匯出到 YouTube,自動填寫日期和遊戲標題(保留原有描述),追蹤已處理影片(可設快取時效),並支援自動化順序匯出、多頁處理、清理快取、單獨清除影片快取及拖動控制面板。新增可客製化的 YouTube 匯出資訊模板及描述附加選項。根據 URL 控制面板顯隱,使用 JS 過濾改進卡片選擇器。

// ==UserScript==
// @name         Twitch VOD 自動匯出助手 By Minidoracat
// @namespace    https://github.com/Minidoracat
// @version      0.9.5
// @description  輔助將 Twitch VOD 匯出到 YouTube,自動填寫日期和遊戲標題(保留原有描述),追蹤已處理影片(可設快取時效),並支援自動化順序匯出、多頁處理、清理快取、單獨清除影片快取及拖動控制面板。新增可客製化的 YouTube 匯出資訊模板及描述附加選項。根據 URL 控制面板顯隱,使用 JS 過濾改進卡片選擇器。
// @author       Minidoracat
// @homepageURL  https://github.com/Minidoracat/twitch-vod-auto-exporter
// @supportURL   https://github.com/Minidoracat/twitch-vod-auto-exporter/issues
// @icon         https://www.google.com/s2/favicons?sz=64&domain=twitch.tv
// @match        https://dashboard.twitch.tv/u/*
// @grant        GM.setValue
// @grant        GM.getValue
// @grant        GM.addStyle
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    console.log('Twitch VOD 自動匯出助手腳本已載入! (v0.9.5)');

    // --- 使用者可配置參數 ---
    const CACHE_EXPIRY_HOURS = 0;  // 快取過期時間(小時),0 表示永不過期
    const DEBOUNCE_SCAN_DELAY = 500; // DOM 掃描延遲(毫秒),用於延遲處理頻繁的 DOM 變動事件
    const INITIAL_SCRIPT_DELAY = 2000; // 腳本整體初始化延遲(毫秒),等待頁面初步加載完成
    const SCAN_RETRY_COUNT = 10;      // 初始掃描影片卡片的重試次數
    const SCAN_RETRY_DELAY = 1000;    // 每次重試掃描影片卡片的間隔(毫秒)

    // YouTube 匯出資訊客製化模板
    // 可用變數:
    // {originalTitle} - Twitch 影片的原始標題
    // {videoDate}     - 影片發布日期 (格式 YYYY-MM-DD)
    // {videoRawDate}  - 影片發布日期 (Twitch 原始格式,例如 "2025年4月25日")
    // {gameName}      - 影片的遊戲/分類名稱
    // {gameNameNoSpace} - 影片的遊戲/分類名稱 (無空格)
    // {existingDescription} - YouTube 描述欄位中已有的內容 (如果 YOUTUBE_APPEND_TO_EXISTING_DESCRIPTION 為 true 且原有描述存在)
    const YOUTUBE_TITLE_TEMPLATE = "{originalTitle} [{videoDate}]"; // YouTube 影片標題模板
    const YOUTUBE_DESCRIPTION_PREPEND_TEXT = ""; // 加在 YouTube 原有描述之前的文字 (僅當 YOUTUBE_APPEND_TO_EXISTING_DESCRIPTION 為 true 且原有描述存在時有效)
    const YOUTUBE_DESCRIPTION_APPEND_TEMPLATE = "剪輯日期:{videoRawDate}\n#{gameName}\n#{gameNameNoSpace}\n"; // 要附加到 YouTube 描述的內容模板,或當不附加時作為完整描述
    const YOUTUBE_TAGS_TEMPLATE = "{gameName}"; // YouTube 影片標籤模板
    const YOUTUBE_VISIBILITY = "private"; // YouTube 影片能見度,可選 "private" (私人) 或 "public" (公開)
    const YOUTUBE_APPEND_TO_EXISTING_DESCRIPTION = true; // 是否將生成的描述附加到 YouTube 現有描述之後 (true),或完全覆蓋 (false)

    const PROCESSED_VIDEOS_STORAGE_KEY = 'twitch_youtube_exporter_processed_videos_v3';
    let isAutoExporting = false;
    let autoExportQueue = [];
    let currentExportPromise = null;

    const controlPanel = document.createElement('div');
    controlPanel.id = 'auto-exporter-control-panel';
    controlPanel.style.display = 'none';

    const dragHandle = document.createElement('div');
    dragHandle.id = 'auto-exporter-drag-handle';
    dragHandle.textContent = '匯出控制面板 (可拖動)';

    const startButton = document.createElement('button');
    startButton.id = 'start-auto-export-button';
    startButton.textContent = '開始自動匯出';
    const stopButton = document.createElement('button');
    stopButton.id = 'stop-auto-export-button';
    stopButton.textContent = '停止自動匯出';
    stopButton.disabled = true;
    const clearCacheButton = document.createElement('button');
    clearCacheButton.id = 'clear-cache-button';
    clearCacheButton.textContent = '清理已處理快取';
    const statusDisplay = document.createElement('div');
    statusDisplay.id = 'auto-exporter-status';
    statusDisplay.textContent = '狀態:待命中';

    controlPanel.appendChild(dragHandle);
    controlPanel.appendChild(startButton);
    controlPanel.appendChild(stopButton);
    controlPanel.appendChild(clearCacheButton);
    controlPanel.appendChild(statusDisplay);

    const videoProducerRegex = /^https:\/\/dashboard\.twitch\.tv\/u\/[^\/]+\/content\/video-producer/;
    let panelInitialized = false;
    let initialScanComplete = false;

    function getVideoCardElements() {
        const allLinks = Array.from(document.querySelectorAll('a'));
        return allLinks.filter(link => link.href && link.href.includes('/content/video-producer/edit/'));
    }

    function tryScanWithRetries(maxRetries = SCAN_RETRY_COUNT, delayBetweenRetries = SCAN_RETRY_DELAY) {
        let attempt = 0;
        console.log(`tryScanWithRetries: 準備開始嘗試掃描,最多 ${maxRetries} 次,間隔 ${delayBetweenRetries}ms。`);
        function scan() {
            attempt++;
            const currentTime = new Date().toLocaleTimeString();
            console.log(`tryScanWithRetries: 第 ${attempt}/${maxRetries} 次嘗試掃描 (${currentTime})。`);
            if (typeof initialScanAndAttachListeners === 'function') {
                initialScanAndAttachListeners(true, (foundCards) => {
                    if (foundCards > 0) {
                        console.log(`tryScanWithRetries: 第 ${attempt} 次嘗試成功找到 ${foundCards} 個卡片,停止重試。`);
                        initialScanComplete = true; return;
                    } else {
                        console.log(`tryScanWithRetries: 第 ${attempt} 次嘗試未找到卡片。`);
                        if (attempt < maxRetries) {
                            console.log(`tryScanWithRetries: ${delayBetweenRetries}ms 後進行下一次嘗試。`);
                            setTimeout(scan, delayBetweenRetries);
                        } else {
                            console.warn(`tryScanWithRetries: 已達到最大嘗試次數 ${maxRetries},仍未找到影片卡片。`);
                            initialScanComplete = true;
                        }
                    }
                });
            } else {
                 console.error("tryScanWithRetries: initialScanAndAttachListeners 函數未定義!");
                 if (attempt < maxRetries) setTimeout(scan, delayBetweenRetries); else initialScanComplete = true;
            }
        }
        scan();
    }

    function checkUrlAndTogglePanel() {
        if (!panelInitialized || !controlPanel || (document.body && !document.body.contains(controlPanel))) { return; }
        const currentUrl = window.location.href;
        const isOnVideoProducerPage = videoProducerRegex.test(currentUrl);
        if (isOnVideoProducerPage) {
            if (controlPanel.style.display === 'none') {
                console.log("URL 符合 video-producer 且面板隱藏,顯示面板並準備掃描。");
                controlPanel.style.display = 'flex'; initialScanComplete = false; tryScanWithRetries();
            } else if (!initialScanComplete) {
                console.log("URL 符合 video-producer,面板已顯示,但初始掃描未完成,嘗試再次掃描。"); tryScanWithRetries();
            }
        } else {
            if (controlPanel.style.display !== 'none') {
                console.log("URL 不符合 video-producer 且面板顯示,隱藏面板。"); controlPanel.style.display = 'none';
            }
            initialScanComplete = false;
        }
    }

    const originalPushState = history.pushState;
    history.pushState = function(...args) { const r = originalPushState.apply(this, args); setTimeout(checkUrlAndTogglePanel, 50); return r; };
    const originalReplaceState = history.replaceState;
    history.replaceState = function(...args) { const r = originalReplaceState.apply(this, args); setTimeout(checkUrlAndTogglePanel, 50); return r; };
    window.addEventListener('popstate', () => { setTimeout(checkUrlAndTogglePanel, 50); });

    function initializeControlPanelDOM() {
        if (document.body && !document.body.contains(controlPanel)) {
            document.body.appendChild(controlPanel); panelInitialized = true; checkUrlAndTogglePanel();
        } else if (document.body && document.body.contains(controlPanel)) {
            panelInitialized = true; checkUrlAndTogglePanel();
        }
    }

    GM.addStyle(`
        #auto-exporter-control-panel { position: fixed; bottom: 20px; right: 20px; background-color: #2c2c2e; flex-direction: column; padding: 0; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.4); z-index: 9999; border: 1px solid #444; }
        #auto-exporter-drag-handle { padding: 8px 15px; background-color: #3a3a3d; color: #f0f0f0; cursor: move; text-align: center; font-size: 13px; border-top-left-radius: 7px; border-top-right-radius: 7px; border-bottom: 1px solid #444; user-select: none; }
        #auto-exporter-control-panel button { background-color: #772ce8; color: white; border: none; padding: 10px 15px; border-radius: 5px; cursor: pointer; font-size: 14px; transition: background-color 0.2s ease; margin: 8px 15px 0px 15px; }
        #auto-exporter-control-panel button#clear-cache-button { background-color: #e91e63; }
        #auto-exporter-control-panel button#clear-cache-button:hover { background-color: #c2185b; }
        #auto-exporter-control-panel button:hover { background-color: #5c1f99; }
        #auto-exporter-control-panel button:disabled { background-color: #555; cursor: not-allowed; }
        #auto-exporter-status { color: #e0e0e0; font-size: 12px; text-align: center; padding: 10px 15px 12px 15px; }
        .video-status-label-minidoracat { position: absolute !important; top: 8px !important; right: 8px !important; padding: 2px 6px !important; font-size: 10px !important; font-weight: bold !important; border-radius: 3px !important; z-index: 1001 !important; color: white; text-shadow: 0 0 2px rgba(0,0,0,0.7); }
        .clear-single-cache-button-minidoracat { position: absolute !important; top: 8px !important; right: 65px !important; padding: 1px 5px !important; font-size: 10px !important; font-weight: bold !important; line-height: 1.2 !important; border-radius: 3px !important; z-index: 1002 !important; background-color: #e74c3c; color: white; border: none; cursor: pointer; box-shadow: 0 1px 2px rgba(0,0,0,0.2); display: none; }
        .clear-single-cache-button-minidoracat:hover { background-color: #c0392b; }
        a[href*="/content/video-producer/edit/"] div[data-target="video-card"] { position: relative !important; }
    `);

    let isDragging = false; let initialMouseX, initialMouseY, initialPanelLeft, initialPanelTop;
    dragHandle.addEventListener('mousedown', (e) => { if (e.button !== 0) return; isDragging = true; initialMouseX = e.clientX; initialMouseY = e.clientY; const rect = controlPanel.getBoundingClientRect(); controlPanel.style.left = `${rect.left}px`; controlPanel.style.top = `${rect.top}px`; controlPanel.style.right = 'auto'; controlPanel.style.bottom = 'auto'; initialPanelLeft = controlPanel.offsetLeft; initialPanelTop = controlPanel.offsetTop; dragHandle.style.cursor = 'grabbing'; document.body.style.userSelect = 'none'; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); e.preventDefault(); });
    function onMouseMove(e) { if (!isDragging) return; e.preventDefault(); const dx = e.clientX - initialMouseX; const dy = e.clientY - initialMouseY; let newLeft = initialPanelLeft + dx; let newTop = initialPanelTop + dy; if (newLeft < 0) newLeft = 0; if (newTop < 0) newTop = 0; if (newLeft + controlPanel.offsetWidth > window.innerWidth) newLeft = window.innerWidth - controlPanel.offsetWidth; if (newTop + controlPanel.offsetHeight > window.innerHeight) newTop = window.innerHeight - controlPanel.offsetHeight; controlPanel.style.left = `${newLeft}px`; controlPanel.style.top = `${newTop}px`; }
    function onMouseUp() { if (!isDragging) return; isDragging = false; dragHandle.style.cursor = 'move'; document.body.style.userSelect = ''; document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); }

    function debounce(func, delay) { let timeout; return function (...args) { const context = this; clearTimeout(timeout); timeout = setTimeout(() => func.apply(context, args), delay); }; }
    async function getProcessedVideosWithExpiryCheck() { const d = await GM.getValue(PROCESSED_VIDEOS_STORAGE_KEY, '{}'); let pd; try { pd = JSON.parse(d); } catch (e) { return {}; } if (CACHE_EXPIRY_HOURS <= 0) { const v = {}; for (const k in pd) if (pd[k] === true || typeof pd[k] === 'number') v[k] = pd[k]; return v; } const n = Date.now(), exp = CACHE_EXPIRY_HOURS * 3600000, vv = {}; for (const k in pd) { const t = pd[k]; if (typeof t === 'number' && (n - t < exp)) vv[k] = t; else if (pd[k] === true && CACHE_EXPIRY_HOURS <= 0) vv[k] = true; } return vv; }
    async function saveProcessedVideo(videoId) { let p = await getProcessedVideosWithExpiryCheck(); p[videoId] = Date.now(); await GM.setValue(PROCESSED_VIDEOS_STORAGE_KEY, JSON.stringify(p)); console.log(`影片 ${videoId} 已標記處理。`); }
    async function clearProcessedVideoCache() { if (confirm("確定清除所有已處理影片的快取嗎?")) { await GM.setValue(PROCESSED_VIDEOS_STORAGE_KEY, '{}'); statusDisplay.textContent = '狀態:快取已清除。'; if (typeof initialScanAndAttachListeners === 'function') await initialScanAndAttachListeners(true, ()=>{}); alert("快取已清除!"); } }
    clearCacheButton.addEventListener('click', clearProcessedVideoCache);
    function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
    function isElementVisible(el) { if (!el) return false; const s = window.getComputedStyle(el); return s.display !== 'none' && s.visibility !== 'hidden' && s.opacity !== '0' && (el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0); }
    function waitForElement(selector, timeout = 10000, parent = document) { return new Promise((resolve, reject) => { const iT = 100; let eT = 0; const i = setInterval(() => { const el = parent.querySelector(selector); if (el && isElementVisible(el)) { clearInterval(i); resolve(el); } else if (eT >= timeout) { clearInterval(i); reject(new Error(`E ${selector} not found`)); } eT += iT; }, iT); }); }
    function dispatchEventOnElement(element, eventName) { if (element) { try { element.dispatchEvent(new Event(eventName, { bubbles: true, cancelable: true })); } catch (e) {} } }
    function setNativeValue(element, value) { const vs = Object.getOwnPropertyDescriptor(element, 'value')?.set; const p = Object.getPrototypeOf(element); const pvs = Object.getOwnPropertyDescriptor(p, 'value')?.set; if (vs && vs !== pvs) pvs.call(element, value); else element.value = value; }

    async function clearSingleVideoCache(videoId, videoCardElement) { let p = await getProcessedVideosWithExpiryCheck(); if (p.hasOwnProperty(videoId)) { delete p[videoId]; await GM.setValue(PROCESSED_VIDEOS_STORAGE_KEY, JSON.stringify(p)); statusDisplay.textContent = `狀態:影片 ${videoId} 快取已清除。`; if (videoCardElement && typeof addStatusLabel === 'function') addStatusLabel(videoCardElement, videoId, false); } }
    function addStatusLabel(videoCardElement, videoId, isProcessed) { const iCD = videoCardElement.querySelector('div[data-target="video-card"]'); if (!iCD) return; let sL = iCD.querySelector('.video-status-label-minidoracat'); if (!sL) { sL = document.createElement('div'); sL.classList.add('video-status-label-minidoracat'); iCD.appendChild(sL); } sL.textContent = isProcessed ? '已處理' : '未處理'; sL.style.backgroundColor = isProcessed ? 'green' : 'orange'; sL.style.color = isProcessed ? 'white' : 'black'; let cB = iCD.querySelector(`.clear-single-cache-button-minidoracat[data-videoid="${videoId}"]`); if (!cB) { cB = document.createElement('button'); cB.classList.add('clear-single-cache-button-minidoracat'); cB.dataset.videoid = videoId; cB.title = `清除影片 ${videoId} 快取`; cB.textContent = '✕'; iCD.appendChild(cB); cB.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); if (confirm(`確定清除影片 ${videoId} 快取?`)) await clearSingleVideoCache(videoId, videoCardElement); }); } cB.style.display = isProcessed ? 'inline-block' : 'none'; }

    async function processSingleVideoForAutoExport(videoCardElement) { if (!isAutoExporting) return Promise.reject("Auto export stopped"); const vIdEl = videoCardElement.querySelector('div[data-video-id]'); const vId = vIdEl?.dataset.videoId; if (!vId) return; statusDisplay.textContent = `狀態:處理影片 ${vId}...`; const oTitle = videoCardElement.querySelector('h5.CoreText-sc-1txzju1-0.crZNHn')?.textContent.trim() || '無標題'; const rDate = videoCardElement.querySelector('div[data-test-selector="video-card-publish-date-selector"]')?.textContent.trim() || '未知日期'; const gName = videoCardElement.querySelector('p.CoreText-sc-1txzju1-0.hufCyP > a > span')?.textContent.trim() || '未知遊戲'; try { const cT = videoCardElement.querySelector('div[data-a-target="video-card-container"]') || videoCardElement; if (!cT) return; cT.click(); await delay(2500); const eMSel = 'div.edit-video-properties-modal__content'; const eM = await waitForElement(eMSel, 8000); const exBtnEM = await waitForElement('button[data-test-selector="export-selector"]', 3500, eM); exBtnEM.click(); await delay(2200); const yM = await waitForElement('div.export-youtube-modal', 8000); const tI = yM.querySelector('input#ye-title'); const dA = yM.querySelector('textarea#ye-description'); const tagsI = yM.querySelector('input#ye-tags'); const sExBtn = yM.querySelector('button[data-test-selector="save"]'); if (!tI || !dA || !tagsI || !sExBtn) { yM.querySelector('button[aria-label="關閉強制回應"]')?.click(); document.querySelector(`${eMSel} button[data-test-selector="CANCEL_TEST_SELECTOR"], ${eMSel} button[aria-label*="關閉"]`)?.click(); return; } let fVD = ''; if (rDate !== '未知日期') { const dM = rDate.match(/(\d{4})年(\d{1,2})月(\d{1,2})日/); if (dM) fVD = `${dM[1]}-${dM[2].padStart(2, '0')}-${dM[3].padStart(2, '0')}`; } const dD = fVD || rDate; const tVs = { originalTitle: oTitle, videoDate: dD, videoRawDate: rDate, gameName: gName, gameNameNoSpace: gName.replace(/\s+/g, '') }; const fT = YOUTUBE_TITLE_TEMPLATE.replace(/{originalTitle}/g, tVs.originalTitle).replace(/{videoDate}/g, tVs.videoDate).replace(/{videoRawDate}/g, tVs.videoRawDate); let nDC = YOUTUBE_DESCRIPTION_APPEND_TEMPLATE.replace(/{videoRawDate}/g, tVs.videoRawDate).replace(/{gameName}/g, tVs.gameName).replace(/{gameNameNoSpace}/g, tVs.gameNameNoSpace); let fD; const eD = dA.value.trim(); if (YOUTUBE_APPEND_TO_EXISTING_DESCRIPTION) { fD = YOUTUBE_DESCRIPTION_PREPEND_TEXT; if (eD) fD += eD + "\n" + nDC; else fD += nDC; } else fD = nDC; const fTags = YOUTUBE_TAGS_TEMPLATE.replace(/{gameName}/g, tVs.gameName).replace(/{gameNameNoSpace}/g, tVs.gameNameNoSpace); const fs = [{ el: tI, v: fT, n: "標題" }, { el: dA, v: fD, n: "描述" }, { el: tagsI, v: fTags, n: "標籤" }]; for (const f of fs) { if (f.el) { f.el.focus(); await delay(100); setNativeValue(f.el, f.v); dispatchEventOnElement(f.el, 'input'); await delay(100); dispatchEventOnElement(f.el, 'change'); await delay(100); f.el.blur(); await delay(100); } } const tRV = YOUTUBE_VISIBILITY === "private" ? "true" : "false"; const vR = yM.querySelector(`input[type="radio"][value="${tRV}"]`); if (vR && !vR.checked) { vR.click(); await delay(500); } sExBtn.click(); await saveProcessedVideo(vId); if (typeof addStatusLabel === 'function') addStatusLabel(videoCardElement, vId, true); await delay(1200); yM.querySelector('button[aria-label="關閉強制回應"]')?.click(); await delay(200); document.querySelector(`${eMSel} button[data-test-selector="CANCEL_TEST_SELECTOR"], ${eMSel} button[aria-label*="關閉"]`)?.click(); await delay(200); } catch (error) { statusDisplay.textContent = `狀態:處理影片 ${vId} 失敗。`; document.querySelector('div.export-youtube-modal button[aria-label="關閉強制回應"]')?.click(); await delay(200); document.querySelector(`div.edit-video-properties-modal__content button[data-test-selector="CANCEL_TEST_SELECTOR"]`)?.click(); } }

    async function startAutoExport() { if (isAutoExporting) return; isAutoExporting = true; startButton.disabled = true; stopButton.disabled = false; statusDisplay.textContent = '狀態:開始自動匯出多頁...'; let cP = 1; let hNP = true; do { if (!isAutoExporting) break; statusDisplay.textContent = `狀態:掃描第 ${cP} 頁...`; await delay(1500); let foundOnPage = 0; await new Promise(resolve => initialScanAndAttachListeners(true, (count) => { foundOnPage = count; resolve(); })); const pVids = await getProcessedVideosWithExpiryCheck(); const cards = getVideoCardElements(); const toProc = cards.filter(c => { const id = c.querySelector('div[data-video-id]')?.dataset.videoId; return id && !pVids[id]; }); if (toProc.length === 0) { statusDisplay.textContent = `狀態:第 ${cP} 頁無未處理影片。`; } else { statusDisplay.textContent = `狀態:第 ${cP} 頁找到 ${toProc.length} 個影片。`; for (let i = 0; i < toProc.length; i++) { if (!isAutoExporting) break; const card = toProc[i]; const id = card.querySelector('div[data-video-id]')?.dataset.videoId || '未知ID'; try { currentExportPromise = processSingleVideoForAutoExport(card); await currentExportPromise; currentExportPromise = null; if (isAutoExporting) await delay(4500 + Math.random() * 2000); } catch (e) { if (String(e).includes("Auto export stopped")) break; } } if (!isAutoExporting) break; } const nPB = document.querySelector('button[aria-label="下一頁"]:not([disabled])'); if (nPB && isAutoExporting) { statusDisplay.textContent = `狀態:第 ${cP} 頁處理完畢,前往下一頁...`; nPB.click(); cP++; hNP = true; await delay(5000); } else { hNP = false; if (isAutoExporting) statusDisplay.textContent = '狀態:已到達最後一頁或下一頁按鈕不可用。'; } } while (isAutoExporting && hNP); if (isAutoExporting) statusDisplay.textContent = '狀態:所有頁面影片已處理完畢!'; stopAutoExport(false); }
    function stopAutoExport(manual = true) { isAutoExporting = false; startButton.disabled = false; stopButton.disabled = true; if (manual) statusDisplay.textContent = '狀態:自動匯出已停止。'; else if (statusDisplay.textContent !== '狀態:所有頁面影片已處理完畢!') statusDisplay.textContent = '狀態:自動匯出已完成或停止。'; if (currentExportPromise) {} autoExportQueue = []; }
    startButton.addEventListener('click', startAutoExport); stopButton.addEventListener('click', () => stopAutoExport(true));

    async function initialScanAndAttachListeners(forceUpdate = false, callback = () => {}) {
        if (controlPanel.style.display === 'none' && !forceUpdate) {
            callback(0); return;
        }
        const processedVideos = await getProcessedVideosWithExpiryCheck();
        const allVideoCardElements = getVideoCardElements(); // 使用新的 JS 過濾方法
        console.log(`initialScanAndAttachListeners: 使用 JS 過濾找到 ${allVideoCardElements.length} 個影片卡片元素。`);

        if (allVideoCardElements.length === 0 && videoProducerRegex.test(window.location.href)) {
            console.warn("initialScanAndAttachListeners: 在 video-producer 頁面未找到任何影片卡片 (即使使用 JS 過濾)。開始詳細調試...");
            const allLinks = document.querySelectorAll('a');
            let foundMatchByHrefIncludes = 0;
            allLinks.forEach(link => { if(link.href && link.href.includes('/content/video-producer/edit/')) foundMatchByHrefIncludes++; });
            console.log(`initialScanAndAttachListeners (調試): 頁面中所有 <a> 標籤數量: ${allLinks.length}`);
            console.log(`initialScanAndAttachListeners (調試): 透過 href.includes('/content/video-producer/edit/') 找到 ${foundMatchByHrefIncludes} 個 <a> 標籤。`);
            const allDivsWithVideoId = document.querySelectorAll('div[data-video-id]');
            console.log(`initialScanAndAttachListeners (調試): 頁面中所有 div[data-video-id] 數量: ${allDivsWithVideoId.length}`);
            const currentUsername = window.location.pathname.split('/u/')[1]?.split('/')[0];
            if (currentUsername) {
                const specificUserLinkSelector = `a[href*="/u/${currentUsername}/content/video-producer/edit/"]`;
                const specificUserLinks = document.querySelectorAll(specificUserLinkSelector);
                console.log(`initialScanAndAttachListeners (調試): 使用特定用戶名 CSS 選擇器 '${specificUserLinkSelector}' 找到 ${specificUserLinks.length} 個元素。`);
            } else {
                console.log(`initialScanAndAttachListeners (調試): 未能從 URL (${window.location.pathname}) 提取用戶名。`);
            }
        }

        allVideoCardElements.forEach((cardElement) => {
            const videoId = cardElement.querySelector('div[data-video-id]')?.dataset.videoId;
            if (videoId) {
                if (forceUpdate || !cardElement.dataset.scriptProcessed) {
                    if (typeof addStatusLabel === 'function') addStatusLabel(cardElement, videoId, !!processedVideos[videoId]);
                }
            }
            cardElement.dataset.scriptProcessed = 'true';
        });
        callback(allVideoCardElements.length);
    }

    const debouncedScan = debounce(async () => {
        if (controlPanel.style.display !== 'none') {
            if (typeof initialScanAndAttachListeners === 'function') await initialScanAndAttachListeners(true, ()=>{});
        }
    }, DEBOUNCE_SCAN_DELAY);

    const observer = new MutationObserver(async (mutationsList) => {
        if (controlPanel.style.display === 'none') return;
        let significantChange = false;
        for (const mutation of mutationsList) {
            if (mutation.type === 'childList' && (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)) {
                const nodes = [...mutation.addedNodes, ...mutation.removedNodes];
                if (nodes.some(node => {
                    if (node.nodeType !== Node.ELEMENT_NODE) return false;
                    // 直接檢查節點本身是否是我們感興趣的連結
                    if (node.matches && node.matches('a') && node.href && node.href.includes('/content/video-producer/edit/')) return true;
                    // 或者檢查節點內部是否包含我們感興趣的連結 (針對容器節點被添加/移除的情況)
                    if (node.querySelector && node.querySelector('a[href*="/content/video-producer/edit/"]')) return true;
                    return false;
                })) {
                    significantChange = true; break;
                }
            }
        }
        if (significantChange && !isAutoExporting) debouncedScan();
    });

    function initializeObserverAndScan() {
        let targetNodeForObserver;
        const mainContentArea = document.querySelector('main div[class*="video-producer-page"], main div[class*="producer-content-wrapper"]');
        const mainElement = document.querySelector('main');
        if (mainContentArea) targetNodeForObserver = mainContentArea;
        else if (mainElement) targetNodeForObserver = mainElement;
        else { targetNodeForObserver = document.body; console.warn("initializeObserverAndScan: 未找到 mainContentArea 或 main 元素,MutationObserver 將觀察 document.body。"); }
        console.log('initializeObserverAndScan: 最終觀察器目標:', targetNodeForObserver.id || targetNodeForObserver.className || targetNodeForObserver.tagName);
        observer.observe(targetNodeForObserver, { childList: true, subtree: true });
    }

    function initializeScript() {
        console.log("initializeScript: 開始初始化腳本...");
        initializeControlPanelDOM();
        initializeObserverAndScan();
    }

    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        setTimeout(initializeScript, INITIAL_SCRIPT_DELAY);
    } else {
        window.addEventListener('load', () => {
            setTimeout(initializeScript, INITIAL_SCRIPT_DELAY);
        });
    }
})();