JAVDB/JAVBUS/JAVLIBRARY to Jable.tv Linker (Class Refactor)

Add Jable.tv link button to JAVDB, JAVBUS, JAVLIBRARY, AV01 and AVJOY pages, and cross-site navigation buttons, plus subtitle site links. Refactored as JableLinker class for maintainability.

As of 2025-10-02. See the latest version.

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 (Class Refactor)
// @namespace   https://tampermonkey.net/
// @version     2025.10.03
// @description Add Jable.tv link button to JAVDB, JAVBUS, JAVLIBRARY, AV01 and AVJOY pages, and cross-site navigation buttons, plus subtitle site links. Refactored as JableLinker class for maintainability.
// @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/*
// @match       https://*.btdig.com/*
// @match       https://sukebei.nyaa.si/*
// @match       https://btdig.com/*
// @match       https://en.btdig.com/*
// @match       https://btsow.pics/*
// @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==

class JableLinker {
    constructor() {
        this.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",
            SUKEBEI: "sukebei.nyaa.si",
            BTDIG: "btdig.com",
            BTSOW: "btsow.pics"
        };

        this.SELECTORS = {
            // JAV Site Selectors
            JAVDB_CODE: ".value", JAVDB_TITLE: ".title", JAVDB_PREVIEW_IMAGES: ".preview-images .tile-item", JAVBUS_SAMPLE_BOX: ".sample-box", JABLE_TITLE: "h4", JAVBUS_TITLE: ".container h3", JAVLIBRARY_TITLE: "h3.text", AVJOY_TITLE: ".video-title h1", AV01_TITLE: "h1", MISSAV_TITLE: ".mt-4 h1",
        };

        this.STYLES = {
            SUKEBEI: `.container .ad,.hdr-link,tr td:nth-child(1),tr td:nth-child(3),tr td:nth-child(6),tr td:nth-child(7),.text-center{display:none !important;} .table {width: 100%;} .torrent-list>tbody>tr>td { white-space: normal; max-width:90vw;} .navbar-form .input-group {position: fixed; left:0;width: 100vw;}`,
            BTDIG: ``,
            BTSOW: `.search {position: sticky !important;top: 80px !important;} .form-inline .input-group {width: 100%;} .hidden-xs:not(.tags-box,.text-right,.search,.search-container)/*,.data-list:not(.detail) .size,.data-list:not(.detail) .date*/{ display: none !important;} .q-container {max-width: 50vw !important;margin-left: 0 !important;}`,
            JAVBUS: `.row > #waterfall, .container { width: 100vw !important; } .masonry #waterfall,.mb20 { display: grid; grid-template-columns: repeat(3, 1fr); } .movie-box { width: auto !important; height: 100% !important; margin: 0 !important; } #waterfall .masonry-brick { position: relative !important; top: 0px !important; left: 0 !important; } .screencap img {width:auto  !important; }.movie-box .photo-frame, .movie-box img { width: 100% !important; height: auto !important; margin: 0 !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; } .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: 100vw !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;} .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}.right-sidebar .video-img-box .img-box {min-width: 100%; max-width: 100%;}.plyr--video {width: 80%;margin: auto;}`,
            AV01: `.group:has(> div.overflow-hidden){display:none} .gap-8 > 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);}`,
            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;} .content-right{display: grid;grid: auto-flow /repeat(4, 1fr) ;} .ad-content-bot,.ad-content-side{display:none}`,
            MISSAV: `.sm\\:container {max-width: 100vw !important;}.order-first{width:100vw !important;} .video-player-container {width: 100%;}
            .xl\\:grid-cols-4 {gap: 10px;}
            .-mt-6 {width: 80%;height: 80%;margin: 0 auto !important;}.order-last,.order-last >div {max-width: 100% !important;min-width: 100% !important;}
            .related-videos,.order-last > div  {display: grid;grid-template-columns: repeat(4, 1fr);gap: 10px;}
            .ad-banner,.advertisement,.ads,.list-none,.space-y-6{display:none !important;}
            .content-without-search > .flex, .order-last > div >.flex {flex-direction: column;}
            .ml-6 {margin-left: 0 !important;}.mb-6>div {width:100% !important}
            .truncate {white-space: normal;}`
        };


        const commonButtons = {
            javdb: true, javbus: true, javlibrary: true, jable: true, av01: true, avjoy: true, avsubtitles: true, subtitlecat: true, missav: true, btdig: true, btsow: true, hayav: true
        };

        this.SITE_CONFIG = {
            [this.SITES.JAVDB]: { titleSelector: this.SELECTORS.JAVDB_TITLE, codeSelector: this.SELECTORS.JAVDB_CODE, style: this.STYLES.JAVDB, pathCheck: "/v/", buttons: { ...commonButtons, javdb: false } },
            [this.SITES.JAVBUS]: { titleSelector: this.SELECTORS.JAVBUS_TITLE, style: this.STYLES.JAVBUS, pathCheck: /[A-Za-z]+-\d+/, buttons: { ...commonButtons, javbus: false } },
            [this.SITES.JAVLIBRARY]: { titleSelector: this.SELECTORS.JAVLIBRARY_TITLE, style: this.STYLES.JAVLIBRARY, pathCheck: "/?v=", buttons: { ...commonButtons } },
            [this.SITES.JABLE]: { titleSelector: this.SELECTORS.JABLE_TITLE, style: this.STYLES.JABLE, pathCheck: "/videos/", buttons: { ...commonButtons, jable: false } },
            [this.SITES.AVJOY]: { titleSelector: this.SELECTORS.AVJOY_TITLE, style: this.STYLES.AVJOY, pathCheck: "/video/", buttons: { ...commonButtons, avjoy: false } },
            [this.SITES.AV01]: { titleSelector: this.SELECTORS.AV01_TITLE, style: this.STYLES.AV01, pathCheck: "/video/", buttons: { ...commonButtons, av01: false } },
            [this.SITES.MDME]: { style: this.STYLES.MDME, pathCheck: "/", buttons: { ...commonButtons, javdb: false, javbus: false } },
            [this.SITES.MISSAV]: { titleSelector: this.SELECTORS.MISSAV_TITLE, style: this.STYLES.MISSAV, pathCheck: "/cn/", buttons: { ...commonButtons, missav: false } },
            // Magnet Site Configs
            [this.SITES.SUKEBEI]: { style: this.STYLES.SUKEBEI, isMagnetSite: true, magnetSelectors: { list: "tbody tr", hashLink: "td:nth-child(3) a:last-child", title: "td:nth-child(2) a", size: "td:nth-child(4)", date: "td:nth-child(5)", insertPoint: "td:nth-child(3)" } },
            [this.SITES.BTDIG]: { style: this.STYLES.BTDIG, isMagnetSite: true, magnetSelectors: { list: ".one_result", hashLink: ".torrent_name a", title: ".torrent_name a", size: ".torrent_size", date: ".torrent_age", insertPoint: ".torrent_magnet" } },
            [this.SITES.BTSOW]: { style: this.STYLES.BTSOW, isMagnetSite: true, magnetSelectors: { list: ".data-list .row", hashLink: "a", title: "a", size: ".size", date: ".date", insertPoint: "a" } },
        };
    }

    // ==================== 2. UTILITY FUNCTIONS ====================
    utils = {
        isIncludes: (domain) => window.location.hostname.includes(domain),
        isValidPath: (pathCheck) => typeof pathCheck === "string" ? window.location.href.includes(pathCheck) : window.location.href.match(pathCheck),
        waitForElement: (selector, timeoutMs = 3000) => 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);
        }),
        moveElementToNextSiblingFirstChild: (selector) => {
            const element = document.querySelector(selector);

            if (!element) {
                console.error("The provided element is invalid.");
                return;
            }
            const nextSibling = element.nextElementSibling;

            if (!nextSibling) {
                console.warn("The element has no next sibling to move to.");
                return;
            }

            nextSibling.insertBefore(element, nextSibling.firstElementChild);
        },
        getCodeFromCurrentSite: (domain, selector) => {
            if (!this.utils.isIncludes(domain)) return null;
            const titleElement = document.querySelector(selector);
            const title = titleElement ? titleElement.textContent.trim() : "";
            let codeMatch = title.match(/[A-Za-z]+-\d+/) || document.title.match(/[A-Za-z]+-\d+/);
            if (codeMatch && codeMatch[0]) return codeMatch[0];
            const lastSegment = decodeURIComponent(window.location.pathname.split("/").filter(Boolean).pop() || "");
            codeMatch = lastSegment.match(/[A-Za-z]+-\d+/);
            return codeMatch ? codeMatch[0] : null;
        },
        gmFetch: (url, options = {}) => new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url,
                ...options,
                onload: (response) => {
                    if (response.status >= 200 && response.status < 300) {
                        resolve(response);
                    } else {
                        reject(new Error(`Request failed with status ${response.status}`));
                    }
                },
                onerror: (error) => reject(new Error("Network error during request", { cause: error })),
            });
        }),
        NewElement: (tagName = "div", config = {}) => {
            const element = document.createElement(tagName);
            if (config.textContent) element.textContent = config.textContent;
            if (config.className) element.className = config.className;
            if (config.id) element.id = config.id;
            if (config.attributes) Object.entries(config.attributes).forEach(([key, value]) => element.setAttribute(key, value));
            if (config.events) Object.entries(config.events).forEach(([event, handler]) => element.addEventListener(event, handler));
            if (config.style) Object.assign(element.style, config.style);
            return element;
        },

    };

    // ==================== 3. DOM & UI MANIPULATION ====================
    dom = {
        injectStyleOnce: (style, idKey) => {
            if (!style || document.getElementById(idKey)) return;
            const styleElement = this.utils.NewElement("style", { id: idKey, textContent: style });
            document.head.appendChild(styleElement);
        },

        linkButtonDefs: {
            javbus: { text: "JAVBUS", color: "#3498db", url: (code) => `https://www.javbus.com/${code.replace("DSVR", "3DSVR")}` },
            javlibrary: { text: "Javlibrary", color: "#6027ae", url: (code) => `https://www.javlibrary.com/cn/vl_searchbyid.php?keyword=${code}` },
            jable: { text: "Jable", color: "#27ae60", url: (code) => `https://jable.tv/videos/${code}/` },
            av01: { text: "AV01", color: "#9b59b6", url: (code) => `https://www.av01.tv/cn/search?q=${code}` },
            avjoy: { text: "AVJOY", color: "#f39c12", url: (code) => `https://avjoy.me/search/videos/${code}` },
            avsubtitles: { text: "AVSubtitles", color: "#e91e63", url: (code) => `https://www.avsubtitles.com/search_results.php?search=${code}` },
            subtitlecat: { text: "SubtitleCat", color: "#ff9800", url: (code) => `https://www.subtitlecat.com/index.php?search=${code}` },
            missav: { text: "MISSAV", color: "#3f51b5", url: (code) => `https://missav.ws/cn/search/${code}` },
            btdig: { text: "BTDIG", color: "#795548", url: (code) => `https://btdig.com/search?q=${code}` },
            btsow: { text: "BTSOW", color: "#607d8b", url: (code) => `https://btsow.pics/#/search/${code}` },
            hayav: { text: "HAYAV", color: "#07d8b6", url: (code) => `https://hayav.com/search/${code}` }
        },

        addLinkButtons: async (code, selector, buttonsConfig) => {
            const titleElement = await this.utils.waitForElement(selector).catch(() => document.querySelector(selector));
            if (!code || !titleElement) return;

            const buttonContainer = this.utils.NewElement("div", {
                id: "cross-site-button-container",
                style: { display: "inline-block", marginLeft: "10px" }
            });

            // JAVDB special handling (async search)
            if (buttonsConfig?.javdb) {
                try {
                    const response = await this.utils.gmFetch(`https://javdb.com/search?q=${encodeURIComponent(code)}`);
                    const doc = new DOMParser().parseFromString(response.responseText, "text/html");
                    const videoLink = doc.querySelector('a[href*="/v/"]');
                    const url = videoLink ? `https://javdb.com${videoLink.getAttribute("href")}` : `https://javdb.com/search?q=${encodeURIComponent(code)}`;
                    const btn = this.dom.createSingleButton("JAVDB", videoLink ? "#e74c3c" : "#e67e22", url);
                    buttonContainer.appendChild(btn);
                } catch (error) {
                    console.error("JAVDB search failed:", error);
                    const btn = this.dom.createSingleButton("搜JAVDB", "#e67e22", `https://javdb.com/search?q=${encodeURIComponent(code)}`);
                    buttonContainer.appendChild(btn);
                }
            }

            // Other sites
            Object.entries(this.dom.linkButtonDefs).forEach(([key, def]) => {
                if (buttonsConfig?.[key]) {
                    const btn = this.dom.createSingleButton(def.text, def.color, def.url(code));
                    buttonContainer.appendChild(btn);
                }
            });

            titleElement.parentNode.insertBefore(buttonContainer, titleElement.nextSibling);
        },

        createSingleButton: (text, bgColor, url) => {
            return this.utils.NewElement("a", {
                textContent: text,
                attributes: { href: url, target: "_blank" },
                style: { marginRight: "8px", padding: "4px 8px", color: "#fff", background: bgColor, border: "none", borderRadius: "3px", cursor: "pointer", textDecoration: "none", fontSize: "14px" }
            });
        },

        enhanceMagnetLinks: (config) => {
            const processNode = (node) => {
                if (!node || node.__tm_magnet_enhanced__) return;

                const selectors = config.magnetSelectors;
                const hashLinkEl = node.querySelector(selectors.hashLink);
                if (!hashLinkEl) return;

                const hashMatch = hashLinkEl.href.match(/[0-9a-fA-F]{40}/);
                if (!hashMatch) return;
                const hash = hashMatch[0].toLowerCase();

                const title = node.querySelector(selectors.title)?.innerText || `magnet-${hash.substring(0, 8)}`;
                const size = node.querySelector(selectors.size)?.innerText || "";
                const date = node.querySelector(selectors.date)?.innerText || "";
                const magnetUrl = `magnet:?xt=urn:btih:${hash}&dn=${encodeURIComponent(title)} ${size} ${date}`;

                const insertPoint = node.querySelector(selectors.insertPoint);
                if (!insertPoint) return;

                const parent = insertPoint.parentElement;

                // Copy button
                const copyBtn = this.utils.NewElement("a", {
                    textContent: "📋",
                    attributes: { href: "#", title: "Copy Magnet Link" },
                    style: { marginLeft: "8px", cursor: "pointer", textDecoration: "none" },
                    events: {
                        click: async (e) => {
                            e.preventDefault();
                            e.stopPropagation();
                            await navigator.clipboard.writeText(decodeURIComponent(magnetUrl));
                            e.currentTarget.textContent = "✅";
                            setTimeout(() => { e.currentTarget.textContent = "📋"; }, 2000);
                        }
                    }
                });

                // Preview button
                const previewBtn = this.utils.NewElement("a", {
                    textContent: "👁️",
                    attributes: { href: "#", title: "Preview Torrent Content" },
                    style: { marginLeft: "6px", cursor: "pointer", textDecoration: "none" },
                    events: {
                        click: (e) => {
                            e.preventDefault();
                            e.stopPropagation();
                            this.drawer.open(`https://magnet.pics/m/${hash}`, title);
                        }
                    }
                });

                parent.append(copyBtn, previewBtn);
                node.__tm_magnet_enhanced__ = true;
            };

            const run = () => document.querySelectorAll(config.magnetSelectors.list).forEach(processNode);
            run(); // Initial run

            // Observe for dynamically added nodes
            const observer = new MutationObserver((mutations) => {
                mutations.forEach(m => {
                    if (m.addedNodes.length) run();
                });
            });
            observer.observe(document.body, { childList: true, subtree: true });
        },
        replacePreviewImagesFromJAVBUS: async function() {
            try {
                const code = document.querySelector(this.SELECTORS.JAVDB_CODE)?.textContent.trim();
                if (!code) {
                    console.log("JableLinker: JAVDB code not found for image replacement.");
                    return;
                }

                const javbusUrl = `https://www.javbus.com/${code}`;
                const userCookie = await GM_getValue("javbus_cookie", "");
                const headers = {};
                if (userCookie) {
                    headers["Cookie"] = userCookie;
                }

                const response = await this.utils.gmFetch(javbusUrl, { headers });
                const doc = new DOMParser().parseFromString(response.responseText, "text/html");
                const anchorElements = Array.from(doc.querySelectorAll(this.SELECTORS.JAVBUS_SAMPLE_BOX));
                const javbusHrefs = anchorElements.map(a => a.getAttribute("href")).filter(Boolean);

                if (javbusHrefs.length > 0) {
                    if ((javbusHrefs[0] || "").includes("javdb")) {
                        console.log("JableLinker: JAVBUS links back to JAVDB, skipping image replacement.");
                        return;
                    }

                    const previewImagesContainer = document.querySelector(".preview-images");
                    if (previewImagesContainer) {
                        Array.from(document.querySelectorAll(this.SELECTORS.JAVDB_PREVIEW_IMAGES)).forEach(item => item.remove());

                        javbusHrefs.forEach(href => {
                            const a = this.utils.NewElement("a", {
                                className: "tile-item",
                                attributes: {
                                    href: href,
                                    "data-fancybox": "gallery",
                                    "data-caption": ""
                                }
                            });
                            const img = this.utils.NewElement("img", {
                                attributes: {
                                    src: href,
                                    alt: "",
                                    loading: "lazy"
                                }
                            });
                            a.appendChild(img);
                            previewImagesContainer.appendChild(a);
                        });
                        console.log(`JableLinker: Replaced preview images with ${javbusHrefs.length} items from JAVBUS for code: ${code}`);
                    }
                } else {
                    console.log(`JableLinker: No preview images found on JAVBUS for code: ${code}`);
                }
            } catch (error) {
                console.error("JableLinker: Error during replacePreviewImagesFromJAVBUS:", error);
            }
        }
    };

    // ==================== 4. DRAWER CLASS FOR PREVIEWS ====================
    drawer = (() => {
        let drawerInstance = null;

        const create = (options) => {
            const { direction = 'right', width = '40vw', title = 'Preview' } = options;

            if (drawerInstance) {
                try { document.body.removeChild(drawerInstance.element); } catch (e) { }
            }

            const element = this.utils.NewElement('div', {
                style: {
                    position: 'fixed', top: '0', [direction]: '0', width, height: '100vh', background: 'white',
                    boxShadow: '0 0 10px rgba(0,0,0,0.1)', transform: `translateX(${direction === 'left' ? '-100%' : '100%'})`,
                    transition: 'transform 0.3s ease', zIndex: '2147483647'
                }
            });

            const header = this.utils.NewElement('div', {
                style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px', borderBottom: '1px solid #eee' }
            });
            const titleEl = this.utils.NewElement('h5', { textContent: title, style: { margin: '0' } });
            const closeBtn = this.utils.NewElement('button', {
                textContent: '×',
                style: { background: 'none', border: 'none', fontSize: '24px', cursor: 'pointer', color: '#666' }
            });
            const content = this.utils.NewElement('div', {
                style: { padding: '0', height: 'calc(100vh - 60px)', overflowY: 'auto' }
            });

            closeBtn.onclick = () => close();

            header.append(titleEl, closeBtn);
            element.append(header, content);
            document.body.appendChild(element);

            drawerInstance = { element, content };
            return drawerInstance;
        };

        const open = (url, title, width, direction) => {
            const instance = create({ title, width, direction });
            instance.content.innerHTML = `<iframe width='100%' height='100%' src='${url}' style='border:none;'></iframe>`;
            requestAnimationFrame(() => instance.element.style.transform = 'translateX(0)');
        };

        const close = () => {
            if (!drawerInstance) return;
            const { element } = drawerInstance;
            const direction = element.style.left === '0px' ? 'left' : 'right';
            element.style.transform = `translateX(${direction === 'left' ? '-100%' : '100%'})`;
            setTimeout(() => {
                if (drawerInstance && drawerInstance.element === element) {
                    drawerInstance.content.innerHTML = '';
                }
            }, 300);
        };

        return { open };
    })();

    // ==================== 5. SITE HANDLERS ====================
    handlers = {
        handleGenericSite: (siteKey, hooks = {}) => {
            console.log(siteKey, hooks)
            const config = this.SITE_CONFIG[siteKey];
            if (!config) return;

            // ===== JAVBUS.COM SPECIAL HANDLING =====
            if (config.style) this.dom.injectStyleOnce(config.style, siteKey.replace(/\./g, "-"));

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

            if (siteKey === this.SITES.JAVBUS) {
                this.utils.moveElementToNextSiblingFirstChild(".masonry-brick:has(.avatar-box)");

                // 1. 替换所有 .photo-frame img 缩略图为大图
                [...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');
                    }
                });
                // 2. 你可以在这里添加更多JAVBUS专属逻辑
            }else if (siteKey === this.SITES.JAVDB) {
                if (isDetailPage) this.dom.replacePreviewImagesFromJAVBUS.call(this);
            }

            if (config.isMagnetSite) {
                this.dom.enhanceMagnetLinks(config);
                return;
            }

            if (config.isVideoSite & isDetailPage) {
                this.dom.addSubtitle(config);//给视频网站添加外挂字幕功能
                return;
            }
            if (isDetailPage) {
                const code = this.utils.getCodeFromCurrentSite(siteKey, config.titleSelector);
                if (!code) return;

                // if (!hooks.skipDefaultDetail) {
                this.dom.addLinkButtons(code, config.titleSelector, config.buttons);
                // }
                // if (typeof hooks.onDetailPage === "function") hooks.onDetailPage(config, code);

            } else {
                if (typeof hooks.onListPage === "function") hooks.onListPage(config);
            }
        },
        setupCookieMenu: () => {
            GM_registerMenuCommand("设置JAVBUS Cookie", async () => {
                const current = await GM_getValue("javbus_cookie", "");
                const input = prompt("请输入JAVBUS的Cookie:", current || "");
                if (input !== null) await GM_setValue("javbus_cookie", input.trim());
            });
            GM_registerMenuCommand("设置JAVDB Cookie", async () => {
                const current = await GM_getValue("javdb_cookie", "");
                const input = prompt("请输入JAVDB的Cookie:", current || "");
                if (input !== null) await GM_setValue("javdb_cookie", input.trim());
            });
        }
    };

    // ==================== 6. INITIALIZATION ====================
    init() {
        this.handlers.setupCookieMenu();
        const currentSiteKey = Object.keys(this.SITE_CONFIG).find(site => this.utils.isIncludes(site));

        if (currentSiteKey) {
            // Simplified routing
            this.handlers.handleGenericSite(currentSiteKey);
        } else {
            console.log("JableLinker: No handler for current site.");
        }
    }
}

// Launch the script
(function () {
    'use strict';
    new JableLinker().init();
})();