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

// ==UserScript==
// @name    JAVDB/JAVBUS/JAVLIBRARY to Jable.tv Linker
// @namespace    https://tampermonkey.net/
// @version    2025.09.11
// @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.media/*
// @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.media
// @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.media',
        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,.frame{display:none}`,
        JAVDB: `.container:not(.is-max-desktop):not(.is-max-widescreen) { max-width: 100vw; }
            .movie-list .item .video-title { white-space: normal; }
            .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, jable: true, javlibrary: 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, jable: true, javlibrary: true, av01: true, avjoy: true }
        },
        [SITES.JAVLIBRARY]: {
            titleSelector: SELECTORS.JAVLIBRARY_TITLE,
            style: STYLES.JAVLIBRARY,
            pathCheck: '/?v=',
            buttons: { javdb: true, javbus: true, jable: true, av01: true, avjoy: true }
        },
        [SITES.JABLE]: {
            titleSelector: SELECTORS.JABLE_TITLE,
            style: STYLES.JABLE,
            pathCheck: '/videos/',
            buttons: { javdb: true, javbus: true, jable: false, av01: true, avjoy: true }
        },
        [SITES.AVJOY]: {
            titleSelector: SELECTORS.AVJOY_TITLE,
            pathCheck: '/video/',
            style: STYLES.AVJOY,
            buttons: { javdb: true, javbus: true, jable: false, av01: false, avjoy: false }
        },
        [SITES.AV01]: {
            titleSelector: SELECTORS.AV01_TITLE,
            style: STYLES.AV01,
            pathCheck: '/video/',
            buttons: { javdb: true, javbus: true, jable: false, av01: false, avjoy: false }
        },
        [SITES.MDME]: {
            style: STYLES.MDME,
            pathCheck: '/',
            buttons: { javdb: false, javbus: false, jable: false, av01: false, avjoy: false }
        },
        [SITES.MISSAV]: {
            titleSelector: SELECTORS.MISSAV_TITLE,
            style: STYLES.MISSAV,
            pathCheck: '/cn/',
            buttons: { javdb: true, javbus: true, jable: false, av01: false, avjoy: false, avsubtitles: true, subtitlecat: 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);
        },
        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: {
                        marginLeft: '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, showJAVBUS, showJAVDB, showJavlibrary, showJable, showAV01, showAVJOY, showAVSubtitles, showSubtitleCat, showMissav, showBtdig, showBtsow) {
            if (!code) return;
            // JAVDB 跳转按钮
            if (showJAVDB) {
                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);
            }
            // JAVBUS 跳转按钮
            if (showJAVBUS) {
                dom.createButton(selector, '跳转JAVBUS', 'javbus-link-btn', '#3498db', `https://www.javbus.com/${code}`);
            }
            // Jable 跳转按钮
            if (showJable) {
                dom.createButton(selector, '跳转Jable', 'jable-link-btn', '#27ae60', `https://jable.tv/videos/${encodeURIComponent(code)}/`);
            }
            // Javlibrary 跳转按钮
            if (showJavlibrary) {
                dom.createButton(selector, '跳转Javlibrary', 'javlibrary-link-btn', '#6027ae', `https://www.javlibrary.com/cn/vl_searchbyid.php?keyword=${encodeURIComponent(code)}/`);
            }
            // AV01 跳转按钮
            if (showAV01) {
                dom.createButton(selector, '跳转AV01', 'av01-link-btn', '#9b59b6', `https://www.av01.media/cn/search?q=${encodeURIComponent(code)}`);
            }
            // AVJOY 跳转按钮
            if (showAVJOY) {
                dom.createButton(selector, '跳转AVJOY', 'avjoy-link-btn', '#f39c12', `https://avjoy.me/search/videos/${encodeURIComponent(code)}`);
            }
            // AVSubtitles 跳转按钮
            if (showAVSubtitles) {
                dom.createButton(selector, '跳转AVSubtitles', 'avsubtitles-link-btn', '#e91e63', `https://www.avsubtitles.com/search_results.php?search=${encodeURIComponent(code)}`);
            }
            // SubtitleCat 跳转按钮
            if (showSubtitleCat) {
                dom.createButton(selector, '跳转SubtitleCat', 'subtitlecat-link-btn', '#ff9800', `https://www.subtitlecat.com/index.php?search=${encodeURIComponent(code)}`);
            }
            // MISSAV 跳转按钮
            if (showMissav) {
                dom.createButton(selector, '跳转MISSAV', 'missav-link-btn', '#3f51b5', `https://missav.ws/cn/search/${encodeURIComponent(code)}`);
            }
            // BTDIG 跳转按钮
            if (showBtdig) {
                dom.createButton(selector, '跳转BTDIG', 'btdig-link-btn', '#795548', `https://btdig.com/search?q=${encodeURIComponent(code)}`);
            }
            // BTSOW 跳转按钮
            if (showBtsow) {
                dom.createButton(selector, '跳转BTSOW', 'btsow-link-btn', '#607d8b', `https://btsow.pics/#/search/${encodeURIComponent(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.javbus,
                config.buttons.javdb,
                config.buttons.javlibrary,
                config.buttons.jable,
                config.buttons.av01,
                config.buttons.avjoy,
                config.buttons.avsubtitles,
                config.buttons.subtitlecat,
                config.buttons.missav,
                config.buttons.btdig,
                config.buttons.btsow
            );
        }
    };

    // ==================== 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();
            // dom.injectStyleOnce(SITE_CONFIG[SITES.JAVLIBRARY].style, 'av01');
            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();
            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();
})();