Enhanced_Media_Helper

Enhanced media downloader script with support for multiple sites, subtitles and easy extensibility. Optimized for faster button appearance. Includes hypothetical JavGG fix.

Per 30-04-2025. Zie de nieuwste versie.

// ==UserScript==
// @name         Enhanced_Media_Helper  
// @version      2.3
// @description  Enhanced media downloader script with support for multiple sites, subtitles and easy extensibility. Optimized for faster button appearance. Includes hypothetical JavGG fix.
// @author       cores (original) & improved version & Gemini
// @match        https://jable.tv/videos/*/*
// @match        https://tokyolib.com/v/*
// @match        https://fs1.app/videos/*/*
// @match        https://cableav.tv/*/
// @match        https://javgg.net/tag/to-be-release/*
// @match        https://javgg.net/featured/*
// @match        https://javgg.net/
// @match        https://javgg.net/new-post/*
// @match        https://javgg.net/jav/*
// @match        https://javgg.net/star/*
// @match        https://javgg.net/trending/*
// @include      /.*javtxt.[a-z]+\/v/.*$/
// @include      /.*javtext.[a-z]+\/v/.*$/
// @match        https://cableav.tv/?p=*
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        GM_xmlhttpRequest
// @connect      api-shoulei-ssl.xunlei.com
// @license      MPL
// @namespace    cdn.bootcss.com 
// ==/UserScript==

(function () {
    'use strict';

    // 配置对象
    const CONFIG = {
        serverMode: 2, // 服务器模式:1 为 localhost,2 为 IP
        serverPort: 9909, // 服务器端口
        alternateUrl: 'https://123av.com/zh/v/', // 备用 URL
        subtitleApiUrl: 'https://api-shoulei-ssl.xunlei.com/oracle/subtitle', // 字幕 API URL
        elementCheckInterval: 200, // 检查元素存在的间隔 (ms)
        elementCheckTimeout: 7000, // 检查元素存在的超时时间 (ms)
    };

    // 工具函数对象
    const UTILS = {
        // 获取当前页面的域名
        getDomain: () => document.domain,

        // 从 URL 中提取视频代码
        getCodeFromUrl: (url) => {
            const match = url.match(/\/([a-z0-9-]+)\/?$/i); // Improved regex to handle optional trailing slash
            return match ? match[1] : null;
        },

        // 获取海报图像的 URL (Improved: handle cases where plyr__poster might not be the direct element)
        getPosterImage: () => {
            const videoContainer = document.querySelector('.video-player-container, .player-container, #player'); // Add more potential container selectors
            if (videoContainer) {
                const posterElem = videoContainer.querySelector('.plyr__poster, [poster]');
                 if (posterElem) {
                     if (posterElem.hasAttribute('poster')) {
                         return posterElem.getAttribute('poster');
                     }
                    const backgroundImageStyle = window.getComputedStyle(posterElem).getPropertyValue('background-image');
                    const matches = /url\("(.+)"\)/.exec(backgroundImageStyle);
                    return matches ? matches[1] : null;
                }
            }
            // Fallback for specific sites if needed
            const metaPoster = document.querySelector('meta[property="og:image"], meta[name="twitter:image"]');
            return metaPoster ? metaPoster.content : null;
        },

        // 获取女演员名字 (Refined selector for potentially broader compatibility)
        getActressNames: () => {
            const actressLinks = document.querySelectorAll('.video-info .info-item a[href*="/actress/"], .models-list .model a, .attributes a[href*="/star/"]'); // Combine selectors
            return Array.from(actressLinks)
                .map(link => link.getAttribute('title') || link.textContent.trim())
                .filter(name => name)
                .filter((value, index, self) => self.indexOf(value) === index) // Unique names
                .join(',');
        },

        // 构建 API URL
        buildApiUrl: (domain, options) => {
            const queryParams = Object.keys(options.query || {})
                .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(options.query[key])}`)
                .join("&");
            const query = queryParams.length > 0 ? `?${queryParams}` : "";
            return `http://${domain}${options.path || ''}${query}`; // Use http unless https is explicitly required/configured
        },

        // 显示 Toast 通知 (Slightly improved styling)
        showToast: (message, type = 'info') => {
            let toastContainer = document.getElementById('custom-toast-container');
            if (!toastContainer) {
                toastContainer = document.createElement('div');
                toastContainer.id = 'custom-toast-container';
                document.body.appendChild(toastContainer);
            }
            const toast = document.createElement('div');
            toast.className = `custom-toast custom-toast-${type}`;
            toast.textContent = message;
            toastContainer.appendChild(toast);
            // Trigger fade in
            setTimeout(() => toast.classList.add('show'), 10);
            // Set timer to remove
            setTimeout(() => {
                toast.classList.remove('show');
                // Remove element after fade out animation completes
                toast.addEventListener('transitionend', () => toast.remove());
            }, 3000);
        },

        // 复制文本到剪贴板
        copyToClipboard: async (text) => {
            if (!text) {
                 UTILS.showToast("没有可复制的内容", "error");
                 return false;
            }
            try {
                await navigator.clipboard.writeText(text);
                UTILS.showToast("内容已成功复制到剪贴板", "success");
                return true;
            } catch (error) {
                UTILS.showToast("复制失败,请检查浏览器权限", "error");
                console.error("Copy error:", error);
                // Fallback for older browsers or insecure contexts (though less likely in userscripts)
                try {
                    const textArea = document.createElement("textarea");
                    textArea.value = text;
                    textArea.style.position = "fixed"; // Prevent scrolling to bottom of page in MS Edge.
                    textArea.style.top = "0";
                    textArea.style.left = "0";
                    textArea.style.opacity = "0";
                    document.body.appendChild(textArea);
                    textArea.focus();
                    textArea.select();
                    const successful = document.execCommand('copy');
                    document.body.removeChild(textArea);
                    if (successful) {
                       UTILS.showToast("内容已复制 (fallback)", "success");
                       return true;
                    } else {
                       throw new Error('execCommand failed');
                    }
                } catch (fallbackError) {
                     UTILS.showToast("复制到剪贴板时出错", "error");
                     console.error("Fallback copy error:", fallbackError);
                     return false;
                }
            }
        },

       // 添加操作按钮 (Refined structure and class names)
        addActionButtons: (container, videoUrl, videoCode) => {
            const buttonContainer = document.createElement("div");
            buttonContainer.className = "emh-action-buttons"; // Use prefix to avoid conflicts

            // 复制下载链接按钮 (Replaces #tobod link)
            const copyButton = document.createElement("button");
            copyButton.id = "emh-copyLink";
            copyButton.className = "emh-button emh-button-copy";
            copyButton.innerHTML = "📋 复制链接"; // Use emoji for visual cue
            copyButton.title = videoUrl || "无有效视频链接";
            copyButton.dataset.videoUrl = videoUrl || '';
            buttonContainer.appendChild(copyButton);

            // 发送数据按钮
            const sendButton = document.createElement("button");
            sendButton.id = "emh-sendData";
             sendButton.className = "emh-button emh-button-send";
            sendButton.innerHTML = "💾 发送到服务器";
            // Store necessary data directly on the button for the event listener
            sendButton.dataset.videoUrl = videoUrl || '';
            sendButton.dataset.videoCode = videoCode || '';
            buttonContainer.appendChild(sendButton);

            // 获取字幕按钮
            const subtitleButton = document.createElement("button");
            subtitleButton.id = "emh-getSubtitles";
             subtitleButton.className = "emh-button emh-button-subtitle";
            subtitleButton.innerHTML = "📄 获取字幕";
            subtitleButton.dataset.videoCode = videoCode || '';
            buttonContainer.appendChild(subtitleButton);

            container.appendChild(buttonContainer);
            return buttonContainer;
        },

        // 创建弹窗显示字幕内容 (Slight style tweaks)
        createSubtitleModal: (subtitleContent = null, videoCode = null) => { // Added videoCode param
            // Remove existing modal first
            const existingModal = document.getElementById('emh-subtitle-modal');
            if (existingModal) existingModal.remove();

            const modal = document.createElement('div');
            modal.id = 'emh-subtitle-modal';
            modal.className = 'emh-modal'; // Prefix class

            const modalContent = document.createElement('div');
            modalContent.className = 'emh-modal-content';

            const modalHeader = document.createElement('div');
            modalHeader.className = 'emh-modal-header';
            modalHeader.innerHTML = `<h3>字幕列表</h3><span class="emh-modal-close">&times;</span>`;
            modalContent.appendChild(modalHeader);

            const modalBody = document.createElement('div');
            modalBody.className = 'emh-modal-body';

            if (subtitleContent && subtitleContent.data && subtitleContent.data.length > 0) {
                const list = document.createElement('ul'); // Use list for semantics
                list.className = 'emh-subtitle-list';
                subtitleContent.data.forEach((subtitle) => {
                    const item = document.createElement('li');
                    item.className = 'emh-subtitle-item';
                    // Try to create a meaningful download filename
                    const downloadFilename = `${videoCode || subtitle.name || 'subtitle'}.${subtitle.ext || 'srt'}`;
                    item.innerHTML = `
                        <div class="emh-subtitle-info">
                            <h4>${subtitle.name || '未命名字幕'}</h4>
                            <p>格式: ${subtitle.ext || '未知'} | 语言: ${subtitle.languages?.length ? subtitle.languages.join(', ') : '未知'} ${subtitle.extra_name ? '| 来源: ' + subtitle.extra_name : ''}</p>
                        </div>
                        ${subtitle.url ? `<a href="${subtitle.url}" target="_blank" class="emh-button emh-button-download" download="${downloadFilename}">下载</a>` : ''}
                    `; // Use generated download filename
                    list.appendChild(item);
                });
                modalBody.appendChild(list);
            } else {
                modalBody.innerHTML = '<p class="emh-no-subtitle-message">未找到相关字幕</p>';
            }

            modalContent.appendChild(modalBody);
            modal.appendChild(modalContent);
            document.body.appendChild(modal);

            // Event listeners for the new modal
            modal.querySelector('.emh-modal-close').onclick = () => modal.remove();
            modal.onclick = (event) => { // Click outside to close
                if (event.target === modal) {
                    modal.remove();
                }
            };

            // Display modal
             setTimeout(() => modal.classList.add('show'), 10); // Allow transition

            return modal;
        },

       // 获取字幕 (Added loading state)
        fetchSubtitles: (videoCode) => {
            if (!videoCode) {
                UTILS.showToast("无法获取视频代码,无法查询字幕", "error");
                return;
            }

            UTILS.showToast("正在获取字幕信息...", "info");
            const button = document.getElementById('emh-getSubtitles') || document.querySelector(`.emh-subtitle-button-small[data-video-code="${videoCode}"]`);
            if (button) button.disabled = true; // Disable button while loading

            const apiUrl = `${CONFIG.subtitleApiUrl}?name=${encodeURIComponent(videoCode)}`;

            const handleResponse = (responseText) => {
                 if (button) button.disabled = false;
                 try {
                    const data = JSON.parse(responseText);
                    UTILS.createSubtitleModal(data, videoCode); // Pass videoCode for potential download name
                    if (data.data && data.data.length > 0) {
                        UTILS.showToast("成功获取字幕信息", "success");
                    } else {
                         UTILS.showToast("未找到字幕", "info");
                     }
                 } catch (e) {
                    console.error("解析字幕数据时出错:", e);
                    UTILS.showToast("解析字幕数据时出错", "error");
                    UTILS.createSubtitleModal(null, videoCode); // Show empty modal on error
                 }
            };

            const handleError = (error) => {
                 if (button) button.disabled = false;
                 console.error("获取字幕时出错:", error);
                 UTILS.showToast("获取字幕时出错", "error");
                 UTILS.createSubtitleModal(null, videoCode);
            };

            if (typeof GM_xmlhttpRequest !== 'undefined') {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: apiUrl,
                    timeout: 15000, // Add timeout
                    onload: (response) => handleResponse(response.responseText),
                    onerror: handleError,
                    ontimeout: () => {
                        if (button) button.disabled = false;
                        UTILS.showToast("获取字幕超时", "error");
                        UTILS.createSubtitleModal(null, videoCode);
                    }
                });
            } else {
                fetch(apiUrl, { signal: AbortSignal.timeout(15000) }) // Add timeout via AbortSignal
                    .then(response => {
                         if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
                         return response.text(); // Get text first to handle potential non-JSON empty response
                    })
                     .then(text => {
                         if (!text) { // Handle empty response
                            handleResponse('{"data": []}'); // Simulate empty data
                         } else {
                             handleResponse(text);
                         }
                    })
                    .catch(error => {
                         if (error.name === 'AbortError') {
                            UTILS.showToast("获取字幕超时", "error");
                         } else {
                             handleError(error);
                         }
                         UTILS.createSubtitleModal(null, videoCode);
                    });
            }
        }
    };

    // 站点处理对象
    const SITE_HANDLERS = {
        // JavTXT 和类似站点
        javtxt: {
            isMatch: () => UTILS.getDomain().includes('javtxt') || UTILS.getDomain().includes('tokyolib') || UTILS.getDomain().includes('javtext'),
             // Define target selector here for the waitForElement function
             targetSelector: 'body > div.main > div.info > div.attributes > dl > dt:nth-child(2)',
             process: (targetElement) => { // Receive the target element
                if (!targetElement) {
                     console.error("JavTXT: Target element not found.");
                     return;
                 }
                const config = {
                    links: [
                        { urlTemplate: 'https://123av.com/zh/v/$code', target: '_blank', displayText: '123av' },
                        { urlTemplate: 'https://jable.tv/videos/$code/', target: '_blank', displayText: 'Jable' }
                    ]
                };

                const cleanedCode = extractCode(targetElement.innerText);
                if (!cleanedCode) {
                    console.error("JavTXT: Failed to extract code.");
                    return;
                }

                 // Create container for links and buttons
                 const controlsContainer = document.createElement('div');
                 controlsContainer.className = 'emh-controls-container';

                 // Create links
                 config.links.forEach(linkConfig => {
                    const link = document.createElement('a');
                    link.href = linkConfig.urlTemplate.replace('$code', cleanedCode);
                    link.target = linkConfig.target;
                    link.className = 'emh-external-link';
                    link.innerText = linkConfig.displayText;
                    controlsContainer.appendChild(link);
                });

                 // Add subtitle button
                 const subtitleButton = document.createElement('button');
                 subtitleButton.id = 'emh-getSubtitles'; // Consistent ID
                 subtitleButton.className = 'emh-button emh-button-subtitle'; // Consistent class
                 subtitleButton.innerHTML = '📄 获取字幕';
                 subtitleButton.dataset.videoCode = cleanedCode;
                 controlsContainer.appendChild(subtitleButton);


                 // Insert the controls container after the target element
                 targetElement.parentNode.insertBefore(controlsContainer, targetElement.nextSibling);
             }
        },

        // JavGG 站点 (FIXED based on HYPOTHETICAL structure - verify selectors!)
        // JavGG 站点 (FIXED based on ACTUAL provided structure)
        javgg: {
            isMatch: () => UTILS.getDomain().includes('javgg'),
            // --- UPDATED SELECTOR ---
            // Wait for the .data div within the article item to appear
            targetSelector: 'article.item.movies .data',
            process: () => { // process function iterates through all found items
                // Remove sidebar immediately if it exists
                const sidebar = document.querySelector("#contenedor > div > div.sidebar.right.scrolling");
                if (sidebar) sidebar.remove();

                const linkProviders = [
                    { code: "njav", url: CONFIG.alternateUrl + "$p", target: "_blank" },
                    { code: "jable", url: "https://jable.tv/videos/$p/", target: "_blank" },
                    { code: "1cili", url: "https://1cili.com/search?q=$p", target: "_blank" }
                ];

                // --- UPDATED SELECTOR ---
                // Process each video entry using the correct item selector
                document.querySelectorAll("article.item.movies").forEach(entry => { // Select each article item
                    // --- UPDATED SELECTORS ---
                    // Find the .data element and the anchor tag within its h3
                    const dataElement = entry.querySelector(".data"); // Find the .data container
                    const anchorTag = dataElement ? dataElement.querySelector("h3 a") : null; // Find the link inside h3

                    if (anchorTag) { // Check if the anchor tag was found
                        const videoCode = anchorTag.textContent.trim(); // Get code from link's text content
                        if (!videoCode) return; // Skip if code is empty

                        // --- UPDATED CHECK LOCATION ---
                        // Check if controls already added (check within dataElement)
                        if (dataElement.querySelector('.emh-javgg-controls')) return;

                        const controlsDiv = document.createElement('div');
                        controlsDiv.className = 'emh-javgg-controls'; // Container for links/buttons

                        // Add provider links
                        linkProviders.forEach(provider => {
                            const newAnchorTag = document.createElement("a");
                            newAnchorTag.href = provider.url.replace("$p", videoCode);
                            newAnchorTag.target = provider.target;
                            newAnchorTag.className = 'emh-external-link-small';
                            newAnchorTag.textContent = provider.code;
                            controlsDiv.appendChild(newAnchorTag);
                        });

                        // Add subtitle button
                        const subtitleButton = document.createElement('button');
                        subtitleButton.className = 'emh-button emh-subtitle-button-small';
                        subtitleButton.innerHTML = '字幕';
                        subtitleButton.dataset.videoCode = videoCode;
                        controlsDiv.appendChild(subtitleButton);

                        // --- UPDATED INSERT LOCATION ---
                        // Append controls to the .data element, after existing content
                        dataElement.appendChild(controlsDiv);

                    } else {
                         // Optional: Log if the expected structure wasn't found
                         // console.log("EMH: Could not find '.data h3 a' within an article.item.movies", entry);
                    }
                });
            }
        },

        // Jable 和类似视频站点
        jable: {
             isMatch: () => UTILS.getDomain().includes('jable') || UTILS.getDomain().includes('cableav') || UTILS.getDomain().includes('fs1.app'),
             // More specific target selectors, check order matters
             targetSelector: '.video-toolbar, .video-info .level, .video-info .row, .text-center',
             process: (targetElement) => { // Receive the target element
                if (!targetElement) {
                     console.error("Jable-like: Target container not found.");
                     return;
                 }

                 // Check if buttons already added to this target
                 if (targetElement.querySelector('.emh-ui-container')) return;

                const isCableAv = UTILS.getDomain() === "cableav.tv";
                let videoUrl = '';
                let videoCode = UTILS.getCodeFromUrl(window.location.href); // Extract code once

                // Find video URL
                if (!isCableAv) {
                    // Try global variable first (common on Jable)
                    if (typeof hlsUrl !== 'undefined' && hlsUrl) {
                        videoUrl = hlsUrl;
                    } else {
                         // Fallback: Check script tags for player setup
                        const scripts = document.querySelectorAll('script');
                         for (let script of scripts) {
                             if (script.textContent.includes('player.src({')) {
                                 const match = script.textContent.match(/src:\s*['"]([^'"]+\.m3u8[^'"]*)['"]/);
                                 if (match && match[1]) {
                                     videoUrl = match[1];
                                     break;
                                 }
                             }
                         }
                    }
                    // Add title fragment if URL found and code exists
                     if (videoUrl && videoCode) {
                         videoUrl += "#" + videoCode;
                     } else if (videoUrl && !videoCode) {
                         // Try getting code from title if URL exists but code extraction failed
                         const titleCodeMatch = document.title.match(/^([A-Z0-9-]+)/i);
                         if (titleCodeMatch) {
                             videoCode = titleCodeMatch[1];
                             videoUrl += "#" + videoCode;
                         }
                     }

                } else { // CableAV logic
                    const metaTag = document.head.querySelector("meta[property~='og:video:url'][content]");
                    if (metaTag) {
                        videoUrl = metaTag.content;
                    }
                }

                 // Create the main UI container
                 const uiContainer = document.createElement("div");
                 uiContainer.className = "emh-ui-container"; // Prefix class

                 // Add video code display (if found)
                 if (videoCode) {
                     const dataElement = document.createElement("span"); // Use span for inline display
                     dataElement.id = "emh-dataElement";
                     dataElement.className = "emh-data-element";
                     dataElement.innerHTML = `番号: ${videoCode}`;
                     dataElement.title = "点击搜索番号 (1cili)";
                     dataElement.dataset.videoCode = videoCode; // Store code for click event
                     uiContainer.appendChild(dataElement);
                 }

                 // Add action buttons (Copy, Send, Subtitle)
                 UTILS.addActionButtons(uiContainer, videoUrl, videoCode);

                 // Append the container to the target element found by waitForElement
                 targetElement.appendChild(uiContainer);
            }
        }
    };

    // 视频数据管理对象
    const VIDEO_MANAGER = {
        sendVideoData: (button) => { // Receive the button that was clicked
             // Extract data stored on the button
             const videoUrl = button.dataset.videoUrl || '';
             const videoCode = button.dataset.videoCode || UTILS.getCodeFromUrl(window.location.href); // Fallback if needed

             // Try to get other details - might need site-specific selectors if called from different pages
             // Using more generic selectors where possible
             const titleElement = document.querySelector("h4.title, h1.post-title, .video-info h4, meta[property='og:title']");
             let title = titleElement ? (titleElement.content || titleElement.innerText.trim()) : document.title;
             if (videoCode && title.includes(videoCode)) { // Clean up title
                 title = title.split(videoCode).pop().trim().replace(/^[-–—\s]+/, ''); // Remove code and leading separators
             }

             const posterImage = UTILS.getPosterImage();
             const actress = UTILS.getActressNames();

             const videoData = {
                code: videoCode || 'UNKNOWN', // Ensure code is not empty
                name: title || 'Untitled',
                img: posterImage || '',
                url: window.location.href,
                actress: actress || '',
                video: videoUrl || '' // The actual stream/download URL
             };

             // Basic validation
             if (!videoData.code || videoData.code === 'UNKNOWN') {
                 UTILS.showToast("无法获取视频代码,发送中止", "warning");
                 console.warn("Send data aborted, missing video code.", videoData);
                 return;
             }
              if (!videoData.video) {
                 UTILS.showToast("无法获取视频链接,部分信息可能缺失", "warning");
                 console.warn("Missing video stream URL.", videoData);
                 // Allow sending anyway, but warn user.
             }

             console.log("Data to send:", videoData);

             const serverDomain = (CONFIG.serverMode === 1) ? `localhost:${CONFIG.serverPort}` : `YOUR_SERVER_IP:${CONFIG.serverPort}`; // Replace YOUR_SERVER_IP if using mode 2
             if (CONFIG.serverMode === 2 && serverDomain.includes('YOUR_SERVER_IP')) {
                  UTILS.showToast("请先在脚本中配置服务器IP地址", "error");
                  console.error("Server IP not configured in script for serverMode 2.");
                  return;
             }

             const apiUrl = UTILS.buildApiUrl(serverDomain, { path: '/add', query: videoData }); // Assuming '/add' endpoint

             // Use GM_xmlhttpRequest for reliable background request if available
             if (typeof GM_xmlhttpRequest !== 'undefined') {
                 GM_xmlhttpRequest({
                    method: 'GET', // Or 'POST' if your server expects it
                    url: apiUrl,
                    timeout: 10000,
                    onload: (response) => {
                        if (response.status >= 200 && response.status < 300) {
                           UTILS.showToast("数据已发送到服务器", "success");
                        } else {
                           UTILS.showToast(`服务器响应错误: ${response.status}`, "error");
                           console.error("Server response error:", response);
                        }
                    },
                    onerror: (error) => {
                        UTILS.showToast("发送数据时网络错误", "error");
                        console.error("Send data network error:", error);
                    },
                     ontimeout: () => {
                         UTILS.showToast("发送数据超时", "error");
                     }
                 });
             } else {
                 // Fallback using fetch (less reliable for background tasks, might have CORS issues)
                 fetch(apiUrl, { mode: 'no-cors', signal: AbortSignal.timeout(10000) }) // 'no-cors' might be needed for simple GET endpoints
                    .then(response => {
                         // Cannot read response status in no-cors mode
                         UTILS.showToast("数据已尝试发送 (no-cors)", "success");
                    })
                    .catch(error => {
                        if (error.name === 'AbortError') {
                           UTILS.showToast("发送数据超时", "error");
                        } else {
                           UTILS.showToast("发送数据时出错 (fetch)", "error");
                        }
                        console.error("Send data error (fetch):", error);
                    });
             }
             return videoData; // Return data for potential further use
        }
    };

    // --- Helper Functions ---

    // 提取视频代码函数 (No changes needed)
    function extractCode(text) {
        if (!text) return null;
        // Basic code pattern: Letters-Numbers or LettersNumbers-Numbers
        const match = text.match(/([A-Za-z]{2,5}-?\d{2,5})/);
        // Fallback: If pattern doesn't match, try cleaning common additions like (Uncensored Leak)
         return match ? match[1].toUpperCase() : text.replace(/\s*\(.*?\)/g, '').trim().toUpperCase();
    }

    // Function to wait for an element and then execute a callback
    function waitForElement(selector, callback, timeout = CONFIG.elementCheckTimeout) {
        const startTime = Date.now();
        const intervalId = setInterval(() => {
            const element = document.querySelector(selector);
            if (element) {
                clearInterval(intervalId);
                console.log(`EMH: Element found: ${selector}`);
                callback(element); // Pass the found element to the callback
            } else if (Date.now() - startTime > timeout) {
                clearInterval(intervalId);
                console.warn(`EMH: Element "${selector}" not found within ${timeout}ms.`);
                 callback(null); // Indicate failure by passing null
            }
        }, CONFIG.elementCheckInterval); // Check frequently
    }


    // --- Main Logic ---

    // 主函数
    function main() {
        let handlerFound = false;
        for (const [name, handler] of Object.entries(SITE_HANDLERS)) {
            if (handler.isMatch()) {
                console.log(`EMH: Handler matched: ${name}. Waiting for target: ${handler.targetSelector || 'N/A'}`);
                handlerFound = true;
                if (handler.targetSelector) {
                    // Wait for the specific element defined by the handler
                     waitForElement(handler.targetSelector, (targetElement) => {
                         // For JavGG, even if the *specific* target isn't found (maybe timeout),
                         // we still want to run process() because it iterates over all items it *can* find.
                         // For other sites, targetElement must exist.
                         if (targetElement || name === 'javgg') {
                             try {
                                // Give the page a tiny bit more time to render siblings etc. after the target is found
                                setTimeout(() => {
                                     handler.process(targetElement); // Pass the element
                                }, 50); // Small delay
                             } catch (e) {
                                 console.error(`EMH: Error processing handler ${name}:`, e);
                             }
                         } else {
                             console.error(`EMH: Handler ${name} could not find its target element: ${handler.targetSelector}`);
                         }
                     });
                } else {
                     // If no target selector, run immediately (or with a minimal delay if needed)
                     console.log(`EMH: Handler ${name} has no specific targetSelector, running process immediately.`);
                      try {
                        // Small delay for list pages might still be beneficial
                        setTimeout(() => handler.process(null), 150); // Slightly increased delay
                      } catch(e) {
                          console.error(`EMH: Error processing handler ${name} immediately:`, e);
                      }
                }
                // Assuming only one handler should match per page
                break;
            }
        }
        if (!handlerFound) {
            console.log("EMH: No matching handler found for this site.");
        }
        // Setup global event listeners AFTER the main logic has potentially added elements
        setupEventListeners();
    }

    // 设置事件监听器 (Using event delegation)
    function setupEventListeners() {
        // Use jQuery for simplicity since it's required anyway
        // Using document allows listeners to work even if elements are added dynamically after setup
        $(document).off('.emh'); // Remove previous listeners in this namespace

        // Copy Link Button
        $(document).on('click.emh', '#emh-copyLink', function() {
            const url = $(this).data('videoUrl');
            UTILS.copyToClipboard(url);
        });

        // Send Data Button
        $(document).on('click.emh', '#emh-sendData', function() {
            VIDEO_MANAGER.sendVideoData(this); // Pass the button element
        });

        // Get Subtitles Button (Main and Small)
        $(document).on('click.emh', '#emh-getSubtitles, .emh-subtitle-button-small', function() {
             const videoCode = $(this).data('videoCode'); // Get code from data attribute
             UTILS.fetchSubtitles(videoCode);
        });

         // Clickable Video Code Element
         $(document).on('click.emh', '#emh-dataElement', function() {
            const code = $(this).data('videoCode');
            if (code) {
                window.open(`https://1cili.com/search?q=${code}`, "_blank");
            }
        });

        console.log("EMH: Event listeners attached.");
    }

    // 添加自定义样式 (Optimized and using CSS Variables)
    function addCustomStyles() {
        const style = document.createElement('style');
        style.textContent = `
           :root{--emh-primary-color:#4285f4;--emh-secondary-color:#34a853;--emh-danger-color:#ea4335;--emh-warning-color:#fbbc05;--emh-info-color:#00acc1;--emh-light-bg:#f8f9fa;--emh-dark-text:#202124;--emh-light-text:#fff;--emh-border-color:#dadce0;--emh-button-padding:8px 16px;--emh-button-radius:6px;--emh-transition:all 0.25s cubic-bezier(0.4,0,0.2,1);}.emh-button{display:inline-flex;align-items:center;justify-content:center;padding:var(--emh-button-padding);border:none;border-radius:var(--emh-button-radius);color:var(--emh-light-text);text-align:center;text-decoration:none;font-size:14px;font-weight:500;cursor:pointer;transition:var(--emh-transition);margin:3px 5px;line-height:1.5;box-shadow:0 2px 4px rgba(0,0,0,0.15);white-space:nowrap;vertical-align:middle;position:relative;overflow:hidden;}.emh-button:hover{opacity:0.95;transform:translateY(-2px);box-shadow:0 4px 8px rgba(0,0,0,0.2);}.emh-button:active{transform:translateY(1px);box-shadow:0 1px 2px rgba(0,0,0,0.1);}.emh-button:disabled{opacity:0.6;cursor:not-allowed;transform:none;box-shadow:0 1px 2px rgba(0,0,0,0.1);}.emh-button-copy{background-color:var(--emh-primary-color);background-image:linear-gradient(to bottom right,#4285f4,#3b78e7);}.emh-button-send{background-color:var(--emh-danger-color);background-image:linear-gradient(to bottom right,#ea4335,#d23f31);}.emh-button-subtitle{background-color:var(--emh-secondary-color);background-image:linear-gradient(to bottom right,#34a853,#2e9549);}.emh-button-download{background-color:#1a73e8;background-image:linear-gradient(to bottom right,#1a73e8,#1967d2);font-size:14px;padding:6px 12px;display:inline-flex;align-items:center;gap:6px;}.emh-button-download:hover{background-color:#1967d2;color:white;text-decoration:none;}.emh-button-download::before{content:"↓";font-weight:bold;margin-right:2px;}.emh-subtitle-button-small{background-color:var(--emh-secondary-color);background-image:linear-gradient(to bottom right,#34a853,#2e9549);border:none;color:white;padding:4px 8px;margin:0 4px;text-align:center;text-decoration:none;font-size:12px;font-weight:500;cursor:pointer;border-radius:4px;transition:var(--emh-transition);vertical-align:middle;box-shadow:0 1px 2px rgba(0,0,0,0.15);}.emh-subtitle-button-small:hover{background-color:#2e9549;transform:translateY(-1px);box-shadow:0 2px 4px rgba(0,0,0,0.2);}.emh-ui-container{margin:12px 0 8px 0;text-align:center;display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:10px;border-top:1px solid var(--emh-border-color);padding:14px 8px 6px 8px;background-color:rgba(248,249,250,0.5);border-radius:8px;}.emh-action-buttons{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;}.emh-data-element{display:inline-flex;align-items:center;padding:6px 10px;background-color:#e8f0fe;border-radius:var(--emh-button-radius);cursor:pointer;border:1px solid #d2e3fc;transition:var(--emh-transition);color:#1a73e8;font-size:14px;font-weight:500;vertical-align:middle;box-shadow:0 1px 2px rgba(0,0,0,0.05);}.emh-data-element:hover{background-color:#d2e3fc;border-color:#aecbfa;transform:translateY(-1px);box-shadow:0 2px 4px rgba(0,0,0,0.1);}.emh-data-element::after{content:" 🔍";opacity:0.7;margin-left:4px;}.emh-controls-container{margin-top:10px;padding:8px;display:flex;gap:10px;align-items:center;flex-wrap:wrap;background-color:rgba(248,249,250,0.5);border-radius:8px;}.emh-javgg-controls{margin-top:6px;display:inline-flex;flex-wrap:wrap;gap:6px;align-items:center;margin-left:10px;vertical-align:middle;padding:4px 6px;background-color:rgba(248,249,250,0.5);border-radius:6px;}.emh-external-link,.emh-external-link-small{display:inline-flex;align-items:center;justify-content:center;padding:5px 10px;background-color:#5f6368;background-image:linear-gradient(to bottom right,#5f6368,#3c4043);color:white;border-radius:var(--emh-button-radius);text-decoration:none;font-size:13px;font-weight:500;transition:var(--emh-transition);vertical-align:middle;box-shadow:0 1px 2px rgba(0,0,0,0.15);}.emh-external-link-small{font-size:12px;padding:4px 8px;border-radius:4px;}.emh-external-link:hover,.emh-external-link-small:hover{background-color:#3c4043;color:white;text-decoration:none;transform:translateY(-1px);box-shadow:0 3px 5px rgba(0,0,0,0.2);}#custom-toast-container{position:fixed;top:70px;right:20px;z-index:10000;display:flex;flex-direction:column;gap:10px;}.custom-toast{padding:14px 20px;border-radius:8px;color:var(--emh-light-text);box-shadow:0 4px 12px rgba(0,0,0,0.25);transition:opacity 0.3s ease,transform 0.3s ease;opacity:0;transform:translateX(100%);font-size:14px;font-weight:500;display:flex;align-items:center;}.custom-toast.show{opacity:1;transform:translateX(0);}.custom-toast::before{margin-right:8px;font-weight:bold;}.custom-toast-success{background-color:var(--emh-secondary-color);}.custom-toast-success::before{content:"✓";}.custom-toast-error{background-color:var(--emh-danger-color);}.custom-toast-error::before{content:"✕";}.custom-toast-info{background-color:var(--emh-info-color);}.custom-toast-info::before{content:"ℹ";}.custom-toast-warning{background-color:var(--emh-warning-color);color:#202124;}.custom-toast-warning::before{content:"⚠";}.emh-modal{display:flex;align-items:center;justify-content:center;position:fixed;z-index:9998;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgba(0,0,0,0.7);opacity:0;transition:opacity 0.3s ease;backdrop-filter:blur(3px);}.emh-modal.show{opacity:1;}.emh-modal-content{background-color:#fff;margin:auto;padding:0;border-radius:12px;width:90%;max-width:650px;box-shadow:0 8px 24px rgba(0,0,0,0.4);transform:scale(0.9);transition:transform 0.3s ease;overflow:hidden;}.emh-modal.show .emh-modal-content{transform:scale(1);}.emh-modal-header{display:flex;justify-content:space-between;align-items:center;padding:16px 24px;background-color:#f8f9fa;border-bottom:1px solid var(--emh-border-color);}.emh-modal-header h3{margin:0;color:var(--emh-dark-text);font-size:18px;font-weight:500;}.emh-modal-close{color:#5f6368;font-size:28px;font-weight:bold;cursor:pointer;line-height:1;padding:0 5px;transition:color 0.2s ease;}.emh-modal-close:hover{color:#202124;}.emh-modal-body{padding:20px 24px;max-height:65vh;overflow-y:auto;background-color:#fff;}.emh-no-subtitle-message{text-align:center;color:#5f6368;font-size:16px;padding:36px 0;}.emh-subtitle-list{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:14px;}.emh-subtitle-item{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;padding:16px;background-color:#f8f9fa;border-radius:8px;border:1px solid #e8eaed;gap:12px;transition:transform 0.2s ease,box-shadow 0.2s ease;}.emh-subtitle-item:hover{transform:translateY(-2px);box-shadow:0 4px 8px rgba(0,0,0,0.1);}.emh-subtitle-info{flex:1 1 auto;min-width:200px;}.emh-subtitle-info h4{margin:0 0 8px 0;color:var(--emh-dark-text);font-size:16px;font-weight:500;}.emh-subtitle-info p{margin:3px 0;color:#5f6368;font-size:14px;line-height:1.5;}.emh-subtitle-item .emh-button-download{flex-shrink:0;}.emh-modal-body::-webkit-scrollbar{width:10px;}.emh-modal-body::-webkit-scrollbar-track{background:#f1f1f1;border-radius:5px;}.emh-modal-body::-webkit-scrollbar-thumb{background:#dadce0;border-radius:5px;border:2px solid #f1f1f1;}.emh-modal-body::-webkit-scrollbar-thumb:hover{background:#bdc1c6;}@media (max-width:768px){.emh-ui-container{padding:12px 6px 6px;gap:8px;}.emh-button{padding:6px 12px;font-size:13px;}.emh-modal-content{width:95%;}.emh-subtitle-item{padding:12px;}}
        `;
        document.head.appendChild(style);
    }

    // 初始化函数 (Replaces setTimeout with DOM readiness check and element waiting)
    function initialize() {
        addCustomStyles(); // Add styles immediately

        // Wait for the DOM to be at least interactive before running main logic
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', main);
        } else {
            main(); // DOM already loaded or interactive
        }
        console.log("EMH Initialized");
    }

    // --- Script Execution ---
    initialize();

})();