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.

As of 30. 04. 2025. See the latest version.

// ==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();

})();