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

As of 17.09.2025. See ბოლო ვერსია.

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.18
// @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
// @run-at    document-idle
// @license    MIT
// ==/UserScript==

(function () {
    'use strict';//document-idle

    // ==================== 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: auto !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; }
            //.fancybox-slide {width: auto;height: auto;position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);}
            // .fancybox-content {height: 100% !important;width: 100% !important;}
            .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}`
    };

    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 = 10000) {
            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 = 10000) {
            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() || '';
                console.log('Copy button for:', text);

                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 = '複製番號';
                                }, 2000);
                            } 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) return;
            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: () => {
                        try { if (track.track) { track.track.mode = 'showing'; } } catch (_) { }
                    }
                }
            });
            video.appendChild(track);
            try {
                const textTracks = video.textTracks;
                if (textTracks && textTracks.length) {
                    for (let i = 0; i < textTracks.length; i++) {
                        const tt = textTracks[i];
                        tt.mode = (tt.label === track.label || tt.language === 'en') ? 'showing' : 'disabled';
                    }
                }
            } catch (_) { }
        },
        _attachToAllVideos() {
            const videos = document.querySelectorAll('video');
            videos.forEach(v => this._attachSubtitleToVideo(v));
        },
        _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()
                }
            });

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

            container.appendChild(button);
            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._attachSubtitleToVideo(node);
                                } else {
                                    const vids = node.querySelectorAll?.('video');
                                    vids && vids.forEach(v => this._attachSubtitleToVideo(v));
                                }
                            }
                        });
                    }
                }
            });
            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.JAVLIBRARY].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', 2000);
                    utils.waitForElement(config.titleSelector, 2000)
                        .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();
            handleGenericSite(SITES.MISSAV);
        }
    };

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