JAVDB/JAVBUS/JAVLIBRARY to Jable.tv Linker

Add Jable.tv link button to JAVDB, JAVBUS, JAVLIBRARY, AV01 and AVJOY pages, and cross-site navigation buttons, plus subtitle site links

La data de 20-09-2025. Vezi ultima versiune.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name    JAVDB/JAVBUS/JAVLIBRARY to Jable.tv Linker
// @namespace    https://tampermonkey.net/
// @version    2025.09.21
// @description    Add Jable.tv link button to JAVDB, JAVBUS, JAVLIBRARY, AV01 and AVJOY pages, and cross-site navigation buttons, plus subtitle site links
// @author    庄引X@https://x.com/zhuangyin8
// @match    https://javdb.com/*
// @match    https://www.javbus.com/*
// @match    https://www.javlibrary.com/*
// @match    https://jable.tv/*
// @match    https://avjoy.me/video/*
// @match    https://www.av01.tv/*
// @match    https://91md.me/*
// @match    https://missav.ws/*
// @grant    GM_addStyle
// @grant    GM_xmlhttpRequest
// @grant    GM_getValue
// @grant    GM_setValue
// @grant    GM_registerMenuCommand
// @run-at    document-end
// @connect    jable.tv
// @connect    javdb.com
// @connect    javbus.com
// @connect    avjoy.me
// @connect    av01.tv
// @license    MIT
// ==/UserScript==

(function () {
    'use strict';

    // ==================== 1. 常量与配置 ====================
    const SITES = {
        JAVDB: 'javdb.com',
        JAVBUS: 'javbus.com',
        JAVLIBRARY: 'javlibrary.com',
        JABLE: 'jable.tv',
        AVJOY: 'avjoy.me',
        AV01: 'av01.tv',
        MDME: '91md.me',
        MISSAV: 'missav.ws'
    };

    const SELECTORS = {
        JAVDB_CODE: '.value',
        JAVDB_TITLE: '.title',
        JAVDB_PREVIEW_IMAGES: '.preview-images .tile-item',
        JAVDB_PREVIEW_SMALL_IMAGES: '.preview-images .tile-item img',
        JAVBUS_SAMPLE_BOX: '.sample-box',
        JABLE_TITLE: 'h4',
        JAVBUS_TITLE: '.container h3',
        JAVLIBRARY_TITLE: 'h3.text',//'#video_id',
        AVJOY_TITLE: '.video-title h1',
        AV01_TITLE: 'h1',
        MISSAV_TITLE: '.mt-4 h1',
    };

    const STYLES = {
        JAVBUS: `.row > #waterfall, .container { width: 100vw !important; }
            .masonry #waterfall { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0px; padding: 0px; }
            .movie-box { width: auto !important; height: 100% !important; margin: 0 !important; }
            #related-waterfall a { width: 33% !important; }
            #waterfall .masonry-brick { position: relative !important; top: 0px !important; left: 0 !important; margin: 0px; }
            .item-tag { display: inline-block; }
            .movie-box .photo-frame, .movie-box img { width: 100% !important; height: 100% !important; }
            .screencap img { width: auto !important; height: 100% !important; }
            iframe,.banner728,.banner300,.bcpic2{display:none}`,
        JAVDB: `.container:not(.is-max-desktop):not(.is-max-widescreen) { max-width: 100vw; }
            .movie-list .item .video-title { white-space: normal; }
            .tag:not(body){ font-size:1rem;}
            .moj-content{display:none}`,
        JAVLIBRARY: `#leftmenu > table:last-child,#topbanner11{display:none}`,
        JABLE: `.video-img-box .title,.title{white-space:normal;max-height:auto}.container {max-width: 80% !important;}
        .right-sidebar {display: contents;max-width: 500px;}
        .right-sidebar .video-img-box {display:inline-block !important;}
        .right-sidebar>.gutter-20>.col-lg-12 {flex: 0 0 25% !important;}
        .right-sidebar .video-img-box .img-box,.right-sidebar .video-img-box .detail {min-width:100%;max-width: 100%;}
        .justify-content-center,.h5,iframe,.asg-interstitial,.root--ujvuu ,.text-sponsor,.pb-3 .row .col-6:nth-child(2),.right-sidebar .row .col-6:nth-child(1){display:none}`,
        AV01: `.group:has(> div.overflow-hidden){display:none} .grid > div{grid-column:span 3 / span 3}
        .max-w-7xl{max-width: 90vw;}.space-y-4 > div{width: 33vw;display: -webkit-box;}`,
        MDME: `.detail_left ol li {height: 115px; }.detail_left {  writing-mode: tb;}.width1200>div{float:none;width:100vw;}
        .width1200 {width: 100vw;   min-width: 100vw; }.detail_right_div ul,.sugetVideo ul {display: grid;grid-template-columns: repeat(4, 1fr);}
        .detail_right_div ul li,.sugetVideo ul li {width:100%;}.detail_right_div ul li img,.sugetVideo ul li img{width:100%;height:100%;}`,
        AVJOY: `.related-video .thumb-overlay,.related-video .content-info {width: 100%;}.container {max-width: 100vw;}
        .content-right{ width: 100%;}.content-left {;max-width: 70%;margin:0 auto;}/*width: calc(100% - 315px)*/
        .content-right{display: grid;grid: auto-flow /repeat(4, 1fr) ;} .related-video {display: inline-grid;}
        .tag-list {display: inline-grid;grid-template-columns: repeat(10, 100px);}
        .ad-content-bot,.ad-content-side{display:none}`,
        MISSAV: `.sm\:container {max-width: 100vw !important;}.order-first{width:100vw !important;}
        .content-without-search{max-width: 100vw;}
        .video-player-container {width: 100%;} .-mt-6 {width: 50%;height: 50%;margin: 0 auto !important;}
        .aspect-w-16 {width: 100%;margin: auto;}
        .related-videos {display: grid;grid-template-columns: repeat(4, 1fr);gap: 10px;}
        .related-videos .video-item,{width: 100%;}
        .order-last,.order-last >div {max-width: 100% !important;min-width: 100% !important;}
        .order-last > div {display:grid; grid-template-columns: repeat(4,minmax(0,1fr));gap: 1.25rem;}
        .mb-6{ flex-direction: column; }.mb-6 > div{ width: 100% !important;}
        .ad-banner,.advertisement,.ads,.list-none,.space-y-6{display:none !important;}
        .flex {flex-direction: column;}.ml-6 {margin-left: 0  !important;}`
    };

    const SITE_CONFIG = {
        [SITES.JAVDB]: {
            titleSelector: SELECTORS.JAVDB_TITLE,
            codeSelector: SELECTORS.JAVDB_CODE,
            style: STYLES.JAVDB,
            pathCheck: '/v/',
            buttons: { javdb: false, javbus: true, javlibrary: true, jable: true, av01: true, avjoy: true, avsubtitles: true, subtitlecat: true, missav: true, btdig: true, btsow: true }
        },
        [SITES.JAVBUS]: {
            titleSelector: SELECTORS.JAVBUS_TITLE,
            style: STYLES.JAVBUS,
            pathCheck: /[A-Za-z]+-\d+/, // 番号格式
            buttons: { javdb: true, javbus: false, javlibrary: true, jable: true, av01: true, avjoy: true, avsubtitles: true, subtitlecat: true, missav: true, btdig: true, btsow: true }
        },
        [SITES.JAVLIBRARY]: {
            titleSelector: SELECTORS.JAVLIBRARY_TITLE,
            style: STYLES.JAVLIBRARY,
            pathCheck: '/?v=',
            buttons: { javdb: true, javbus: true, javlibrary: true, jable: true, av01: true, avjoy: true, avsubtitles: true, subtitlecat: true, missav: true, btdig: true, btsow: true }
        },
        [SITES.JABLE]: {
            titleSelector: SELECTORS.JABLE_TITLE,
            style: STYLES.JABLE,
            pathCheck: '/videos/',
            buttons: { javdb: true, javbus: true, javlibrary: true, jable: false, av01: true, avjoy: true, avsubtitles: true, subtitlecat: true, missav: true, btdig: true, btsow: true }
        },
        [SITES.AVJOY]: {
            titleSelector: SELECTORS.AVJOY_TITLE,
            pathCheck: '/video/',
            style: STYLES.AVJOY,
            buttons: { javdb: true, javbus: true, javlibrary: true, jable: true, av01: true, avjoy: false, avsubtitles: true, subtitlecat: true, missav: true, btdig: true, btsow: true }
        },
        [SITES.AV01]: {
            titleSelector: SELECTORS.AV01_TITLE,
            style: STYLES.AV01,
            pathCheck: '/video/',
            buttons: { javdb: true, javbus: true, javlibrary: true, jable: true, av01: false, avjoy: true, avsubtitles: true, subtitlecat: true, missav: true, btdig: true, btsow: true }
        },
        [SITES.MDME]: {
            style: STYLES.MDME,
            pathCheck: '/',
            buttons: { javdb: false, javbus: false, javlibrary: true, jable: true, av01: true, avjoy: true, avsubtitles: true, subtitlecat: true, missav: true, btdig: true, btsow: true }
        },
        [SITES.MISSAV]: {
            titleSelector: SELECTORS.MISSAV_TITLE,
            style: STYLES.MISSAV,
            pathCheck: '/cn/',
            buttons: { javdb: true, javbus: true, javlibrary: true, jable: true, av01: true, avjoy: true, avsubtitles: true, subtitlecat: true, missav: false, btdig: true, btsow: true }
        }
    };

    // ==================== 2. 工具函数 ====================
    const utils = {
        isIncludes(domain) {
            return window.location.hostname.includes(domain);
        },
        isValidPath(pathCheck) {
            return typeof pathCheck === 'string'
                ? window.location.href.includes(pathCheck)
                : window.location.href.match(pathCheck);
        },
        waitForElement(selector, timeoutMs = 1000) {
            return new Promise((resolve, reject) => {
                const existing = document.querySelector(selector);
                if (existing) return resolve(existing);
                const observer = new MutationObserver(() => {
                    const el = document.querySelector(selector);
                    if (el) {
                        observer.disconnect();
                        resolve(el);
                    }
                });
                observer.observe(document.documentElement, { childList: true, subtree: true });
                if (timeoutMs > 0) {
                    setTimeout(() => {
                        observer.disconnect();
                        reject(new Error(`waitForElement timeout: ${selector}`));
                    }, timeoutMs);
                }
            });
        },
        getCodeFromCurrentSite(domain, selector) {
            if (!utils.isIncludes(domain)) return null;
            const titleElement = document.querySelector(selector);
            const title = titleElement ? titleElement.textContent.trim() : '';
            let codeMatch = title.match(/[A-Za-z]+-\d+/);
            if (codeMatch && codeMatch[0]) return codeMatch[0];
            // Fallback: try document.title
            if (document.title) {
                codeMatch = document.title.match(/[A-Za-z]+-\d+/);
                if (codeMatch && codeMatch[0]) return codeMatch[0];
            }
            // Fallback: try to extract from URL path (e.g., /cn/video/165808/nmsl-004)
            const pathSegments = window.location.pathname.split('/').filter(Boolean);
            const lastSegment = decodeURIComponent(pathSegments[pathSegments.length - 1] || '');
            codeMatch = lastSegment.match(/[A-Za-z]+-\d+/);
            return codeMatch ? codeMatch[0] : null;
        },
        requestPage(url, onSuccess, onError, headers = {}) {
            GM_xmlhttpRequest({
                method: 'GET',
                url,
                headers,
                onload: function (response) {
                    if (response.status === 200) {
                        onSuccess(response.responseText);
                    } else if (onError) {
                        onError(response);
                    }
                },
                onerror: onError
            });
        },
        async getJavbusCookie() {
            return await GM_getValue('javbus_cookie', '');
        },
        async getJavdbCookie() {
            return await GM_getValue('javdb_cookie', '');
        },
        createNewElement(selector = '.video-title', position = 'before', NewElement) {
            // 通用方法:在指定选择器元素上创建新元素
            // NewElement 应该是一个函数,接收当前元素作为参数,返回要插入的 DOM 元素
            // 例如:createNewElement('.video-title', 'before', (titleElement) => {
            //   const btn = document.createElement('button');
            //   btn.textContent = titleElement.textContent;
            //   btn.className = 'test-btn';
            //   btn.addEventListener('click', () => console.log('clicked'));
            //   return btn;
            // });
            if (typeof NewElement !== 'function') {
                console.error('createNewElement: NewElement must be a function that returns a DOM element');
                return;
            }

            const titleElements = document.querySelectorAll(selector);
            if (!titleElements || titleElements.length === 0) return;

            titleElements.forEach(titleElement => {
                // 检查是否已经添加过相同类型的新元素(通过检查第一个子元素的类名)
                const newElement = NewElement(titleElement);
                if (!newElement || !newElement.tagName) {
                    console.error('createNewElement: NewElement function must return a valid DOM element');
                    return;
                }

                // 生成唯一标识符来避免重复添加
                const elementType = newElement.tagName.toLowerCase();
                const elementClass = newElement.className || '';
                const uniqueId = `${elementType}-${elementClass.split(' ')[0] || 'default'}`;
                const existingElement = titleElement.querySelector(`.${uniqueId}`);
                if (existingElement) return; // 避免重复添加

                // 为元素添加唯一标识类
                newElement.classList.add(uniqueId);

                // 根据 position 参数决定在元素内部插入位置
                if (position === 'before') {
                    titleElement.insertBefore(newElement, titleElement.firstChild);
                } else if (position === 'after') {
                    titleElement.appendChild(newElement);
                } else {
                    console.error('createNewElement: position must be "before" or "after"');
                }
            });
        },
        NewElement(tagName = 'div', config = {}) {
            // 创建新元素的辅助方法
            // config 可以包含:textContent, className, id, attributes, events, style
            // 例如:NewElement('button', {
            //   textContent: 'Click me',
            //   className: 'btn btn-primary',
            //   attributes: { type: 'button', 'data-test': 'value' },
            //   events: { click: () => console.log('clicked') },
            //   style: { color: 'red', fontSize: '14px' }
            // });
            const element = document.createElement(tagName);

            // 设置文本内容
            if (config.textContent) element.textContent = config.textContent;

            // 设置类名
            if (config.className) element.className = config.className;

            // 设置ID
            if (config.id) element.id = config.id;

            // 设置属性
            if (config.attributes && typeof config.attributes === 'object') {
                Object.entries(config.attributes).forEach(([key, value]) => {
                    element.setAttribute(key, value);
                });
            }

            // 设置事件监听器
            if (config.events && typeof config.events === 'object') {
                Object.entries(config.events).forEach(([event, handler]) => {
                    if (typeof handler === 'function') {
                        element.addEventListener(event, handler);
                    }
                });
            }

            // 设置样式
            if (config.style && typeof config.style === 'object') {
                Object.entries(config.style).forEach(([property, value]) => {
                    element.style[property] = value;
                });
            }

            return element;
        }
    };

    // ==================== 3. 页面操作 ====================
    const dom = {
        injectStyleOnce(style, idKey) {
            if (!style) return;
            const key = `style-${idKey}`;
            if (document.getElementById(key)) return;
            const styleElement = utils.NewElement('style', {
                id: key,
                textContent: style
            });
            document.head.appendChild(styleElement);
        },
        linkButtonDefs: {
            javbus: {
                text: '跳转JAVBUS', className: 'javbus-link-btn', color: '#3498db',
                url: (code) => `https://www.javbus.com/${code.replace('DSVR', '3DSVR')}`
            },
            javlibrary: {
                text: '跳转Javlibrary', className: 'javlibrary-link-btn', color: '#6027ae',
                url: (code) => `https://www.javlibrary.com/cn/vl_searchbyid.php?keyword=${encodeURIComponent(code)}/`
            },
            jable: {
                text: '跳转Jable', className: 'jable-link-btn', color: '#27ae60',
                url: (code) => `https://jable.tv/videos/${encodeURIComponent(code)}/`
            },
            av01: {
                text: '跳转AV01', className: 'av01-link-btn', color: '#9b59b6',
                url: (code) => `https://www.av01.tv/cn/search?q=${encodeURIComponent(code)}`
            },
            avjoy: {
                text: '跳转AVJOY', className: 'avjoy-link-btn', color: '#f39c12',
                url: (code) => `https://avjoy.me/search/videos/${encodeURIComponent(code)}`
            },
            avsubtitles: {
                text: '跳转AVSubtitles', className: 'avsubtitles-link-btn', color: '#e91e63',
                url: (code) => `https://www.avsubtitles.com/search_results.php?search=${encodeURIComponent(code)}`
            },
            subtitlecat: {
                text: '跳转SubtitleCat', className: 'subtitlecat-link-btn', color: '#ff9800',
                url: (code) => `https://www.subtitlecat.com/index.php?search=${encodeURIComponent(code)}`
            },
            missav: {
                text: '跳转MISSAV', className: 'missav-link-btn', color: '#3f51b5',
                url: (code) => `https://missav.ws/cn/search/${encodeURIComponent(code)}`
            },
            btdig: {
                text: '跳转BTDIG', className: 'btdig-link-btn', color: '#795548',
                url: (code) => `https://btdig.com/search?q=${encodeURIComponent(code)}`
            },
            btsow: {
                text: '跳转BTSOW', className: 'btsow-link-btn', color: '#607d8b',
                url: (code) => `https://btsow.pics/#/search/${encodeURIComponent(code)}`
            }
        },
        appendIntoPreviousElementEnd(selector) {
            const elements = document.querySelectorAll(selector);
            if (!elements || elements.length === 0) return;
            elements.forEach(element => {
                const previous = element.previousElementSibling;
                if (!previous) return;
                previous.appendChild(element);
            });
        },
        appendIntoPreviousElementEndWhenReady(selector, timeoutMs = 1000) {
            utils.waitForElement(selector, timeoutMs)
                .then(() => {
                    dom.appendIntoPreviousElementEnd(selector);
                })
                .catch(() => {
                    dom.appendIntoPreviousElementEnd(selector);
                });
            // Also observe for dynamically added matching nodes and adjust them when they appear
            const observer = new MutationObserver(() => {
                dom.appendIntoPreviousElementEnd(selector);
            });
            observer.observe(document.body, { childList: true, subtree: true });
            // Stop observing after a grace period to avoid permanent observers
            setTimeout(() => observer.disconnect(), Math.max(4000, Math.min(timeoutMs, 15000)));
        },

        createButton(selector, text, className, bgColor, url) {
            const titleElement = document.querySelector(selector);
            if (!titleElement) return;
            if (!titleElement.parentNode.querySelector(`.${className}`)) {
                const btn = utils.NewElement('a', {
                    textContent: text,
                    className: className,
                    attributes: { href: url },
                    style: {
                        marginRight: '8px',
                        padding: '2px 8px',
                        color: '#fff',
                        background: bgColor,
                        border: 'none',
                        borderRadius: '3px',
                        cursor: 'pointer'
                    }
                });
                titleElement.parentNode.insertBefore(btn, titleElement.nextSibling);
            }
        },
        addCopyButtonsOnJAVDBList(selector = '.video-title', position = 'before') {
            // 在 javdb 列表页为每个指定选择器元素添加复制按钮
            utils.createNewElement(selector, position, (titleElement) => {
                const text = titleElement.textContent?.trim() || '';

                const copyButton = utils.NewElement('a', {
                    className: 'button is-white copy-to-clipboard',
                    attributes: {
                        title: '複製番號',
                        'data-clipboard-text': text
                    },
                    events: {
                        click: async (event) => {
                            event.preventDefault();
                            event.stopPropagation();
                            try {
                                await navigator.clipboard.writeText(text);
                                event.target.title = '已複製番號';
                                // Reset title after 2 seconds
                                setTimeout(() => {
                                    event.target.title = '複製番號';
                                }, 1000);
                            } catch (error) {
                                console.error('Copy failed:', error.message);
                            }
                        }
                    }
                });

                const icon = utils.NewElement('i', {
                    className: 'icon-copy'
                });

                copyButton.appendChild(icon);
                return copyButton;
            });
        },
        async addLinkButtons(code, selector, buttons) {
            if (!code) return;
            // JAVDB 跳转按钮
            if (buttons?.javdb) {
                const userCookie = await utils.getJavdbCookie();
                const headers = {
                    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
                    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Safari/605.1.15'
                };
                if (userCookie) headers['Cookie'] = userCookie;
                const addSearchButton = () => {
                    dom.createButton(selector, '搜JAVDB', 'javdb-search-btn', '#e67e22', `https://javdb.com/search?q=${encodeURIComponent(code)}`);
                };
                utils.requestPage(`https://javdb.com/search?q=${encodeURIComponent(code)}`, function (responseText) {
                    const parser = new DOMParser();
                    const doc = parser.parseFromString(responseText, 'text/html');
                    const videoLink = doc.querySelector('a[href*="/v/"]');
                    if (videoLink) {
                        dom.createButton(selector, '跳转JAVDB', 'javdb-link-btn', '#e74c3c', `https://javdb.com${videoLink.getAttribute('href')}`);
                    } else {
                        addSearchButton();
                    }
                }, function (error) {
                    // 网络或访问受限时退化为搜索按钮
                    addSearchButton();
                }, headers);
            }
            // 其余站点按钮(基于统一定义循环生成)
            Object.entries(dom.linkButtonDefs).forEach(([key, def]) => {
                if (buttons?.[key]) {
                    dom.createButton(selector, def.text, def.className, def.color, def.url(code));
                }
            });
        },
        async replacePreviewImagesFromJAVBUS() {
            const code = document.querySelector(SELECTORS.JAVDB_CODE)?.textContent.trim();
            if (!code) return;
            const javbusUrl = `https://www.javbus.com/${code}`;
            const userCookie = await utils.getJavbusCookie();
            const headers = {
                'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
                'Accept-Language': 'en-US,en;q=0.5',
                'Referer': javbusUrl,
                'Origin': 'https://www.javbus.com',
                'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Safari/605.1.15',
            };
            if (userCookie) headers['Cookie'] = userCookie;
            utils.requestPage(javbusUrl, (responseText) => {
                const parser = new DOMParser();
                const doc = parser.parseFromString(responseText, 'text/html');
                const anchorElements = Array.from(doc.querySelectorAll(SELECTORS.JAVBUS_SAMPLE_BOX));
                const javbusHrefs = anchorElements
                    .map(a => a.getAttribute('href'))
                    .filter(Boolean);
                if (javbusHrefs.length > 0 && (javbusHrefs[0] || '').includes('javdb')) return;
                if (javbusHrefs.length > 0) {
                    // 删除所有 JAVDB 预览图片
                    Array.from(document.querySelectorAll(SELECTORS.JAVDB_PREVIEW_IMAGES)).forEach(img => img.remove());
                    // 用 javbusHrefs 创建新的预览图
                    const previewImagesContainer = document.querySelector('.preview-images');
                    if (previewImagesContainer) {
                        javbusHrefs.forEach(href => {
                            const a = utils.NewElement('a', {
                                className: 'tile-item',
                                attributes: {
                                    href: href,
                                    'data-fancybox': 'gallery',
                                    'data-caption': ''
                                }
                            });
                            const img = utils.NewElement('img', {
                                attributes: {
                                    src: href,
                                    alt: '',
                                    loading: 'lazy'
                                }
                            });
                            a.appendChild(img);
                            previewImagesContainer.appendChild(a);
                        });
                        console.log(`[replacePreviewImagesFromJAVBUS] Replaced preview images with ${javbusHrefs.length} items for code: ${code}`);
                    }
                }
            }, (error) => {
                console.error(`[replacePreviewImagesFromJAVBUS] Error fetching JAVBUS data for ${code}:`, error);
            }, headers);
        },
        observeJAVDBListForCopyButtons() {
            dom.addCopyButtonsOnJAVDBList(".video-title");
            const observer = new MutationObserver(() => {
                dom.addCopyButtonsOnJAVDBList(".video-title");
            });
            observer.observe(document.body, { childList: true, subtree: true });
        },
        insertCrossSiteButtonsForCode(config, code) {
            dom.addLinkButtons(
                code,
                config.titleSelector,
                config.buttons
            );
        }
    };

    // ==================== 3.1 本地字幕模块 ====================
    const subtitles = {
        _inited: false,
        _state: {
            vttText: null,
            subtitleLabel: null,
            objectUrlsByVideo: new WeakMap(),
            hasLoadedSubtitle: false
        },
        _isLikelyVtt(text) {
            const firstLine = (text || '').split(/\r?\n/)[0]?.trim();
            return firstLine === 'WEBVTT' || (firstLine || '').startsWith('WEBVTT');
        },
        _convertSrtToVtt(srtText) {
            let text = (srtText || '').replace(/\r\n?/g, '\n');
            text = text.replace(/^\uFEFF/, '');
            text = text.replace(/(\d\d:\d\d:\d\d),(\d\d\d)/g, '$1.$2');
            text = text.replace(/\s+-->\s+/g, ' --> ');
            text = text.replace(/\n\d+\n(?=\d\d:\d\d:\d\d\.\d{3}\s+-->)/g, '\n');
            text = text.replace(/\n{3,}/g, '\n\n');
            return 'WEBVTT\n\n' + text.trim() + '\n';
        },
        _toVtt(text) {
            return this._isLikelyVtt(text) ? text : this._convertSrtToVtt(text);
        },
        _revokeExistingObjectUrl(video) {
            try {
                const url = this._state.objectUrlsByVideo.get(video);
                if (url) {
                    URL.revokeObjectURL(url);
                    this._state.objectUrlsByVideo.delete(video);
                }
            } catch (_) { }
        },
        _removeExistingLocalTracks(video) {
            const tracks = Array.from(video.querySelectorAll('track[data-local-subtitle="1"]'));
            tracks.forEach(t => t.remove());
        },
        _attachSubtitleToVideo(video) {
            if (!this._state.vttText) {
                console.log('[Local Subtitles] No VTT text available');
                return;
            }

            console.log('[Local Subtitles] Attaching subtitle to video:', video);
            this._revokeExistingObjectUrl(video);
            this._removeExistingLocalTracks(video);
            const blob = new Blob([this._state.vttText], { type: 'text/vtt' });
            const url = URL.createObjectURL(blob);
            this._state.objectUrlsByVideo.set(video, url);

            const track = utils.NewElement('track', {
                attributes: {
                    kind: 'subtitles',
                    label: this._state.subtitleLabel || 'Local Subtitle',
                    srclang: 'en',
                    src: url,
                    default: true,
                    'data-local-subtitle': '1'
                },
                events: {
                    load: () => {
                        console.log('[Local Subtitles] Track loaded:', track);
                        try {
                            if (track.track) {
                                track.track.mode = 'showing';
                                console.log('[Local Subtitles] Track mode set to showing');
                            }
                        } catch (e) {
                            console.error('[Local Subtitles] Error setting track mode:', e);
                        }
                    },
                    error: (e) => {
                        console.error('[Local Subtitles] Track load error:', e);
                    }
                }
            });

            video.appendChild(track);
            console.log('[Local Subtitles] Track element added to video');

            try {
                const textTracks = video.textTracks;
                console.log('[Local Subtitles] Video textTracks:', textTracks);
                if (textTracks && textTracks.length) {
                    for (let i = 0; i < textTracks.length; i++) {
                        const tt = textTracks[i];
                        const shouldShow = (tt.label === track.label || tt.language === 'en');
                        tt.mode = shouldShow ? 'showing' : 'disabled';
                        console.log(`[Local Subtitles] Track ${i}: label="${tt.label}", language="${tt.language}", mode="${tt.mode}"`);
                    }
                }
            } catch (e) {
                console.error('[Local Subtitles] Error managing textTracks:', e);
            }
        },
        _attachToAllVideos() {
            // 查找所有可能的视频元素
            const videoSelectors = [
                'video',
                'iframe[src*="player"]',
                'iframe[src*="video"]',
                '.video-player video',
                '.player video',
                '#player video',
                '.jwplayer video',
                '.video-js video',
                '.plyr video'
            ];

            let allVideos = [];
            videoSelectors.forEach(selector => {
                const elements = document.querySelectorAll(selector);
                allVideos = allVideos.concat(Array.from(elements));
            });

            // 去重
            const uniqueVideos = [...new Set(allVideos)];

            console.log('[Local Subtitles] Found videos:', uniqueVideos.length);
            console.log('[Local Subtitles] Video elements:', uniqueVideos);

            if (uniqueVideos.length === 0) return;

            // 找到width最大的video标签
            let maxWidthVideo = null;
            let maxWidth = 0;

            uniqueVideos.forEach((video, index) => {
                const rect = video.getBoundingClientRect();
                const width = rect.width;
                console.log(`[Local Subtitles] Video ${index} width:`, width);

                if (width > maxWidth) {
                    maxWidth = width;
                    maxWidthVideo = video;
                }
            });

            if (maxWidthVideo) {
                console.log(`[Local Subtitles] Attaching subtitle to video with max width: ${maxWidth}px`, maxWidthVideo);
                this._attachSubtitleToVideo(maxWidthVideo);
            }
        },
        _ensureUi() {
            if (document.getElementById('tm-add-local-subtitle-btn')) return;

            const container = utils.NewElement('div', {
                id: 'tm-add-local-subtitle-container',
                style: {
                    position: 'fixed',
                    zIndex: '2147483647',
                    bottom: '16px',
                    left: '16px',
                    display: 'flex',
                    gap: '8px',
                    alignItems: 'center'
                }
            });

            const input = utils.NewElement('input', {
                attributes: {
                    type: 'file',
                    accept: '.vtt,.srt,text/vtt,text/plain,.sub'
                },
                style: {
                    display: 'none'
                }
            });

            const label = utils.NewElement('span', {
                id: 'tm-add-local-subtitle-label',
                textContent: '',
                style: {
                    fontSize: '12px',
                    color: '#fff',
                    textShadow: '0 1px 2px rgba(0,0,0,0.6)'
                }
            });

            const button = utils.NewElement('button', {
                id: 'tm-add-local-subtitle-btn',
                textContent: '本地字幕',
                attributes: {
                    title: '为页面视频加载本地字幕 (.srt/.vtt)'
                },
                style: {
                    padding: '6px 10px',
                    fontSize: '12px',
                    color: '#fff',
                    background: 'rgba(0,0,0,0.6)',
                    border: '1px solid rgba(255,255,255,0.2)',
                    borderRadius: '6px',
                    cursor: 'pointer',
                    backdropFilter: 'saturate(150%) blur(6px)',
                    userSelect: 'none'
                },
                events: {
                    click: () => input.click()
                }
            });

            // 添加调试按钮(仅在 MISSAV 站点显示)
            const debugButton = utils.NewElement('button', {
                id: 'tm-debug-subtitle-btn',
                textContent: '调试字幕',
                attributes: {
                    title: '检查字幕加载状态和视频信息'
                },
                style: {
                    padding: '6px 10px',
                    fontSize: '12px',
                    color: '#fff',
                    background: 'rgba(255,165,0,0.8)',
                    border: '1px solid rgba(255,255,255,0.2)',
                    borderRadius: '6px',
                    cursor: 'pointer',
                    backdropFilter: 'saturate(150%) blur(6px)',
                    userSelect: 'none',
                    display: utils.isIncludes('missav.ws') ? 'block' : 'none'
                },
                events: {
                    click: () => {
                        const videos = document.querySelectorAll('video');
                        const subtitleInfo = {
                            hasLoadedSubtitle: subtitles._state.hasLoadedSubtitle,
                            vttText: subtitles._state.vttText ? '已加载' : '未加载',
                            subtitleLabel: subtitles._state.subtitleLabel || '无',
                            videoCount: videos.length,
                            videos: Array.from(videos).map((v, i) => ({
                                index: i,
                                src: v.src || '无src',
                                tracks: v.textTracks ? v.textTracks.length : 0,
                                localTracks: v.querySelectorAll('track[data-local-subtitle="1"]').length
                            }))
                        };

                        console.log('[MISSAV Debug] Subtitle Info:', subtitleInfo);
                        alert(`字幕调试信息:\n\n已加载字幕:${subtitleInfo.hasLoadedSubtitle ? '是' : '否'}\n字幕文件:${subtitleInfo.subtitleLabel}\n视频数量:${subtitleInfo.videoCount}\n\n详细信息请查看控制台`);
                    }
                }
            });

            const retryButton = utils.NewElement('button', {
                id: 'tm-retry-subtitle-btn',
                textContent: '重新附加',
                attributes: {
                    title: '重新尝试附加字幕到视频'
                },
                style: {
                    padding: '6px 10px',
                    fontSize: '12px',
                    color: '#fff',
                    background: 'rgba(255,165,0,0.8)',
                    border: '1px solid rgba(255,255,255,0.2)',
                    borderRadius: '6px',
                    cursor: 'pointer',
                    backdropFilter: 'saturate(150%) blur(6px)',
                    userSelect: 'none'
                },
                events: {
                    click: () => {
                        console.log('[Local Subtitles] Manual retry triggered');
                        this._attachToAllVideos();
                    }
                }
            });

            input.addEventListener('change', async () => {
                const file = input.files?.[0];
                if (!file) return;
                this._state.subtitleLabel = file.name;
                try {
                    const text = await file.text();
                    this._state.vttText = this._toVtt(text);
                    this._state.hasLoadedSubtitle = true;
                    label.textContent = file.name;

                    // 立即尝试附加字幕
                    this._attachToAllVideos();

                    // 延迟重试,因为有些视频可能是动态加载的
                    setTimeout(() => {
                        console.log('[Local Subtitles] Retrying subtitle attachment after delay');
                        this._attachToAllVideos();
                    }, 1000);

                    setTimeout(() => {
                        console.log('[Local Subtitles] Final retry for subtitle attachment');
                        this._attachToAllVideos();
                    }, 3000);

                } catch (err) {
                    console.error('[Local Subtitles] Failed to read file:', err);
                    alert('读取字幕文件失败,请重试。');
                } finally {
                    input.value = '';
                }
            });

            container.appendChild(button);
            container.appendChild(debugButton);
            container.appendChild(retryButton);
            container.appendChild(label);
            container.appendChild(input);
            document.documentElement.appendChild(container);
        },
        _ensureCueStyle() {
            if (document.getElementById('tm-local-subtitle-cue-style')) return;
            const style = utils.NewElement('style', {
                id: 'tm-local-subtitle-cue-style',
                textContent: `\nvideo::cue {\n  -webkit-text-stroke: 2px #000000;\n  text-shadow: 1px 0 0 #000000, -1px 0 0 #000000, 0 1px 0 #000000, 0 -1px 0 #000000, 1px 1px 0 #000000, -1px -1px 0 #000000, 1px -1px 0 #000000, -1px 1px 0 #000000;\n}`
            });
            document.head.appendChild(style);
        },
        _observeNewVideos() {
            const observer = new MutationObserver((mutations) => {
                if (!this._state.hasLoadedSubtitle) return;
                for (const m of mutations) {
                    if (m.type === 'childList') {
                        m.addedNodes.forEach((node) => {
                            if (node && node.nodeType === 1) {
                                if (node.nodeName.toLowerCase() === 'video') {
                                    // 当新视频添加时,重新检查所有视频并只给最大的添加字幕
                                    this._attachToAllVideos();
                                } else {
                                    const vids = node.querySelectorAll?.('video');
                                    if (vids && vids.length > 0) {
                                        // 当新视频添加时,重新检查所有视频并只给最大的添加字幕
                                        this._attachToAllVideos();
                                    }
                                }
                            }
                        });
                    }
                }
            });
            observer.observe(document.documentElement || document.body, { childList: true, subtree: true });
        },
        init() {
            if (this._inited) return;
            this._inited = true;
            this._ensureUi();
            this._ensureCueStyle();
            this._observeNewVideos();
        }
    };

    // ==================== 4. 站点处理器 ====================
    function handleGenericSite(siteKey, hooks = {}) {
        const config = SITE_CONFIG[siteKey];
        if (!config) return;

        const isDetailPage = utils.isValidPath(config.pathCheck);

        if (isDetailPage) {
            const code = utils.getCodeFromCurrentSite(siteKey, config.titleSelector);

            if (!code) return;

            // Default: inject style and add cross-site buttons, unless explicitly skipped
            if (!hooks.skipDefaultDetail) {
                if (config.style) dom.injectStyleOnce(config.style, siteKey.replace(/\./g, '-'));
                dom.insertCrossSiteButtonsForCode(config, code);
            }

            if (typeof hooks.isDetailPage === 'function') {
                hooks.isDetailPage(config, code);
            }
        } else {
            if (typeof hooks.onListPage === 'function') {
                hooks.onListPage(config);
            }
        }
    }

    const handlers = {
        [SITES.JAVDB]: function () {
            handleGenericSite(SITES.JAVDB, {
                onListPage: (config) => {
                    dom.observeJAVDBListForCopyButtons();
                    dom.injectStyleOnce(config.style, 'javdb');
                },
                isDetailPage: (config, code) => {
                    // Additional enhancement for detail page
                    if (config.codeSelector) dom.replacePreviewImagesFromJAVBUS();
                }
            });
        },
        [SITES.JAVBUS]: function () {
            [...document.querySelectorAll('.photo-frame img')].forEach(img => {
                if (img.src.includes('/pics/thumb/')) {
                    img.src = img.src.replace('/pics/thumb/', '/pics/cover/').replace('.jpg', '_b.jpg');
                }
            });
            handleGenericSite(SITES.JAVBUS, {
                onListPage: (config) => {
                    dom.injectStyleOnce(config.style, 'javbus');
                },
                // Detail page handled by generic defaults
                isDetailPage: () => { }
            });
        },
        [SITES.JAVLIBRARY]: function () {
            dom.injectStyleOnce(SITE_CONFIG[SITES.JAVLIBRARY].style, 'javlibrary');
            handleGenericSite(SITES.JAVLIBRARY);
        },
        [SITES.JABLE]: function () {
            subtitles.init();
            dom.injectStyleOnce(SITE_CONFIG[SITES.JABLE].style, 'jable');
            handleGenericSite(SITES.JABLE);
        },
        [SITES.AVJOY]: function () {
            subtitles.init();
            handleGenericSite(SITES.AVJOY);
        },
        [SITES.AV01]: function () {
            subtitles.init();
            handleGenericSite(SITES.AV01, {
                // skipDefaultDetail: true,
                isDetailPage: (config, code) => {
                    dom.appendIntoPreviousElementEndWhenReady('.space-y-1', 1000);
                    utils.waitForElement(config.titleSelector, 1000)
                        .then(() => {
                            if (config.style) dom.injectStyleOnce(config.style, 'av01');
                            dom.insertCrossSiteButtonsForCode(config, code);
                        })
                        .catch(() => {
                            if (config.style) dom.injectStyleOnce(config.style, 'av01');
                            dom.insertCrossSiteButtonsForCode(config, code);
                        });
                }
            });
        },
        [SITES.MDME]: function () {
            handleGenericSite(SITES.MDME);
        },
        [SITES.MISSAV]: function () {
            subtitles.init();

            // 为 MISSAV 添加特殊的视频检测逻辑
            const checkForVideos = () => {
                const videos = document.querySelectorAll('video');
                console.log('[MISSAV] Checking for videos, found:', videos.length);

                // 如果找到视频且已加载字幕,立即附加
                if (videos.length > 0 && subtitles._state.hasLoadedSubtitle) {
                    console.log('[MISSAV] Videos found, attaching subtitles');
                    subtitles._attachToAllVideos();
                }
            };

            // 定期检查视频
            const videoCheckInterval = setInterval(checkForVideos, 1000);

            // 10秒后停止检查
            setTimeout(() => {
                clearInterval(videoCheckInterval);
            }, 10000);

            // handleGenericSite(SITES.MISSAV);
            handleGenericSite(SITES.MISSAV, {
                // skipDefaultDetail: true,
                isDetailPage: (config, code) => {
                    // dom.appendIntoPreviousElementEndWhenReady('.space-y-1', 1000);
                    utils.waitForElement(config.titleSelector, 1000)
                        .then(() => {
                            if (config.style) dom.injectStyleOnce(config.style, 'missav');
                            dom.insertCrossSiteButtonsForCode(config, code);
                        })
                        .catch(() => {
                            if (config.style) dom.injectStyleOnce(config.style, 'missav');
                            dom.insertCrossSiteButtonsForCode(config, code);
                        });
                }
            });
        }
    };

    // ==================== 5. 用户交互 ====================
    function setupCookieMenu() {
        GM_registerMenuCommand('设置JAVBUS Cookie', async function () {
            const current = await GM_getValue('javbus_cookie', '');
            const input = prompt('请输入JAVBUS的Cookie(完整字符串):', current || '');
            if (input !== null) {
                await GM_setValue('javbus_cookie', input.trim());
                alert('javbus_cookie 已保存!');
            }
        });
        GM_registerMenuCommand('设置JAVDB Cookie', async function () {
            const current = await GM_getValue('javdb_cookie', '');
            const input = prompt('请输入JAVDB的Cookie(完整字符串):', current || '');
            if (input !== null) {
                await GM_setValue('javdb_cookie', input.trim());
                alert('javdb_cookie已保存!');
            }
        });
    }

    // ==================== 6. 主入口 ====================
    function init() {
        setupCookieMenu();
        const currentSite = Object.keys(SITE_CONFIG).find(site => utils.isIncludes(site));
        if (currentSite && handlers[currentSite]) {
            handlers[currentSite]();
        } else {
            console.log('未识别的网站');
        }
    }

    // 启动脚本
    init();
})();