☰

Jav Jumper

Jump between JavDB, MissAV, SupJav, OneJav, and U9A9

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         Jav Jumper
// @namespace    https://javdb.com/
// @version      1.5
// @description  Jump between JavDB, MissAV, SupJav, OneJav, and U9A9
// @license      MIT
// @author       YourName
// @match        https://javdb.com/*
// @match        https://missav.ws/*
// @match        https://missav.ai/*
// @match        https://supjav.com/*
// @match        *://onejav.com/*
// @match        https://u9a9.com/*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_notification
// @grant        GM_setClipboard
// @grant        GM_getResourceURL
// @grant        GM_registerMenuCommand
// @grant        GM_info
// @grant        GM_openInTab
// @grant        GM_xmlhttpRequest
// @connect      *
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // ═══════════════════════════════════════════════════════════
    //  全局樣式 (以 d.js 風格為主)
    // ═══════════════════════════════════════════════════════════
    const styles = `
        #jav-jumper-container {
            position: fixed;
            left: 20px;
            bottom: 20px;
            z-index: 99999;
            display: flex;
            flex-direction: column;
            gap: 10px;
            font-family: Arial, sans-serif;
        }
        .jav-jumper-btn {
            display: inline-block;
            padding: 8px 12px;
            color: white;
            text-decoration: none;
            border: none;
            border-radius: 6px;
            font-weight: bold;
            font-size: 12px;
            cursor: pointer;
            transition: all 0.3s ease;
            text-align: center;
            min-width: 70px;
        }
        .jav-jumper-btn:hover {
            transform: translateY(-2px);
        }
        .jj-missav {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
        }
        .jj-missav:hover {
            box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
        }
        .jj-supjav {
            background: linear-gradient(135deg, #00bcd4 0%, #008ba3 100%);
            box-shadow: 0 4px 15px rgba(0, 188, 212, 0.4);
        }
        .jj-supjav:hover {
            box-shadow: 0 6px 20px rgba(0, 188, 212, 0.6);
        }
        .jj-javdb {
            background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
            box-shadow: 0 4px 15px rgba(17, 153, 142, 0.4);
        }
        .jj-javdb:hover {
            box-shadow: 0 6px 20px rgba(17, 153, 142, 0.6);
        }
        .jj-copy {
            background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
            box-shadow: 0 4px 15px rgba(240, 147, 251, 0.4);
        }
        .jj-copy:hover {
            box-shadow: 0 6px 20px rgba(240, 147, 251, 0.6);
        }
        .jj-copied {
            background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%) !important;
        }
        /* OneJav 按鈕容器 */
        .jj-onejav-container {
            display: flex;
            align-items: center;
            gap: 10px;
            margin-bottom: 10px;
        }
    `;

    const styleSheet = document.createElement('style');
    styleSheet.textContent = styles;
    document.head.appendChild(styleSheet);

    // ═══════════════════════════════════════════════════════════
    //  番號格式化 (OneJav / U9A9 共用)
    // ═══════════════════════════════════════════════════════════
    function getAvCode(avid) {
        if (!avid || typeof avid !== 'string') return 'Unknown';

        // FC2PPV 格式統一處理
        const fc2Match = avid.match(/^fc2ppv[-_]?(\d+)$/i);
        if (fc2Match) return 'fc2-ppv-' + fc2Match[1];

        // 保留特殊格式
        if (/^fc2-ppv-\d+$/i.test(avid)) return avid.toLowerCase();
        if (/^[-a-z0-9]+$/i.test(avid) && avid.includes('-')) return avid;

        // OneJav 原有邏輯
        if (avid.match(/-[^0]/g)) return avid;
        if (avid.match(/^[0-9-_]+$/g)) return avid;
        if (avid.match(/^(crazyasia|sm|video_|BrazzersExxtra)+/gi)) return avid;
        if (avid.match(/^FC2PPV\d+$/i)) {
            let numMatch = avid.match(/\d+$/);
            let num = numMatch ? numMatch[0] : '000';
            return "FC2-PPV-" + num;
        }

        // 數字前綴 AV code 格式:例如 300MIUM1416 -> MIUM-1416
        const prefixedAvMatch = avid.match(/^\d+([a-z]{3,5})(\d+)$/i);
        if (prefixedAvMatch) {
            const letter = prefixedAvMatch[1].toUpperCase();
            const num = prefixedAvMatch[2].replace(/^0+/, '').padStart(3, '0');
            return `${letter}-${num}`;
        }

        // 移除 FC2 前綴並標準化
        avid = avid.replace(/\b(FC2+)/gi, "");
        const letter = (avid.match(/^[a-z]+/i)?.[0] || 'ID').toUpperCase();
        const num = (avid.match(/-?(\d+)$/)?.[1] || '000').replace(/^0+/, '').padStart(3, '0');
        return `${letter}-${num}`;
    }

    // ═══════════════════════════════════════════════════════════
    //  取得番號 (javdb / missav / supjav)
    // ═══════════════════════════════════════════════════════════
    function getCode() {
        if (window.location.href.includes('javdb.com')) {
            var link = document.querySelector('.panel-block.first-block a.button.is-white.copy-to-clipboard');
            return link ? link.getAttribute('data-clipboard-text') : '';
        }
        if (window.location.href.includes('missav.ws') || window.location.href.includes('missav.ai')) {
            var el = document.querySelector('.text-secondary span.font-medium');
            if (!el) return '';
            return el.innerText.replace(/-UNCENSORED-LEAK|-CHINESE-SUBTITLE/g, '');
        }
        if (window.location.href.includes('supjav.com')) {
            var m = window.location.search.match(/[?&]s=([^&]+)/);
            return m ? decodeURIComponent(m[1]) : '';
        }
        return '';
    }

    // ═══════════════════════════════════════════════════════════
    //  U9A9 專用:從標題提取 AV code
    // ═══════════════════════════════════════════════════════════
    function extractU9a9Code() {
        const panelTitle = document.querySelector('.panel-heading .panel-title')?.textContent.trim();
        if (!panelTitle) return null;

        // 優先匹配 FC2PPV- 格式
        const fc2Match = panelTitle.match(/FC2PPV[-_]?\d+/i);
        if (fc2Match) return fc2Match[0];

        // 匹配數字前綴 AV code 格式:例如 390JAC-232 -> JAC-232
        const prefixedAvCodeMatch = panelTitle.match(/\b\d+([A-Za-z]{3,5})[-_\s]?(\d{2,6})\b/);
        if (prefixedAvCodeMatch) return `${prefixedAvCodeMatch[1]}-${prefixedAvCodeMatch[2]}`;

        // 匹配 AV code 格式:3-5個字母 + - + 2-6位數字
        const avCodeMatch = panelTitle.match(/\b([A-Za-z]{3,5})[-_]?(\d{2,6})\b/);
        if (avCodeMatch) return avCodeMatch[0];

        return null;
    }

    // ═══════════════════════════════════════════════════════════
    //  SupJav 專用:FC2 番號只取尾部數字
    // ═══════════════════════════════════════════════════════════
    function getSupjavSearchQuery(code) {
        const fc2Match = code.match(/^fc2-ppv-(\d+)$/i);
        if (fc2Match) return fc2Match[1];
        return code;
    }

    // ═══════════════════════════════════════════════════════════
    //  通用:開新分頁
    // ═══════════════════════════════════════════════════════════
    function openInTab(url) {
        var a = document.createElement('a');
        a.href = url;
        a.target = '_blank';
        a.style.display = 'none';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
    }

    // ═══════════════════════════════════════════════════════════
    //  通用:生成按鈕 (d.js 風格)
    // ═══════════════════════════════════════════════════════════
    function mkLinkBtn(text, href, cls) {
        return Object.assign(document.createElement('a'), {
            href: href,
            target: '_blank',
            textContent: text,
            className: 'jav-jumper-btn ' + cls
        });
    }

    function mkActionBtn(text, cls) {
        return Object.assign(document.createElement('button'), {
            textContent: text,
            className: 'jav-jumper-btn ' + cls
        });
    }

    // ═══════════════════════════════════════════════════════════
    //  通用:創建浮動面板 (三個按鈕:複製 + 兩個跳轉)
    // ═══════════════════════════════════════════════════════════
    function createFloatingPanel(番號, btn1Target, btn2Target, btn1Label, btn2Label, btn1Class, btn2Class) {
        var container = document.createElement('div');
        container.id = 'jav-jumper-container';

        // 複製按鈕
        var copyBtn = mkActionBtn('複製', 'jj-copy');
        copyBtn.onclick = async function() {
            try {
                await navigator.clipboard.writeText(番號);
                var orig = copyBtn.textContent;
                copyBtn.textContent = '已複製!';
                copyBtn.classList.add('jj-copied');
                setTimeout(function() {
                    copyBtn.textContent = orig;
                    copyBtn.classList.remove('jj-copied');
                }, 1500);
            } catch (e) {
                copyBtn.textContent = '失敗';
                setTimeout(function() { copyBtn.textContent = '複製'; }, 1500);
            }
        };

        // 跳轉按鈕 1 (上方)
        var btn1Url = btn1Target === 'missav'
            ? 'https://missav.ws/' + 番號.toLowerCase()
            : btn1Target === 'supjav'
                ? 'https://supjav.com/?s=' + encodeURIComponent(getSupjavSearchQuery(番號))
                : 'https://javdb.com/search?f=all&q=' + encodeURIComponent(番號);
        var btn1 = mkLinkBtn(btn1Label, btn1Url, btn1Class);

        // 跳轉按鈕 2 (下方)
        var btn2Url = btn2Target === 'missav'
            ? 'https://missav.ws/' + 番號.toLowerCase()
            : btn2Target === 'supjav'
                ? 'https://supjav.com/?s=' + encodeURIComponent(getSupjavSearchQuery(番號))
                : 'https://javdb.com/search?f=all&q=' + encodeURIComponent(番號);
        var btn2 = mkLinkBtn(btn2Label, btn2Url, btn2Class);

        container.append(copyBtn, btn1, btn2);
        document.body.appendChild(container);
    }

    // ═══════════════════════════════════════════════════════════
    //  JavDB 磁力相關常數(必須在下面的分站分流之前完成初始化)
    //  舊版把它們宣告在 floatingInit 之後,導致浮動面板一被呼叫就因
    //  TDZ 拋出 ReferenceError,第 2 列以後的磁力列全部處理不到。
    // ═══════════════════════════════════════════════════════════
    const JJ_MAGNET_ATTR = 'data-jj-magnet';

    // ═══════════════════════════════════════════════════════════
    //  分站分流
    // ═══════════════════════════════════════════════════════════

    if (window.location.href.includes('onejav.com')) {
        oneJavInit();
    } else if (window.location.href.includes('u9a9.com')) {
        u9a9Init();
    } else {
        floatingInit();
    }

    // ═══════════════════════════════════════════════════════════
    //  OneJav:在每個標題前插入按鈕
    // ═══════════════════════════════════════════════════════════
    function oneJavInit() {
        function add() {
            const titles = document.querySelectorAll('.title.is-4.is-spaced');
            if (titles.length === 0) return;

            titles.forEach(function(title) {
                let avid = title.querySelector('a').textContent
                    ? title.querySelector('a').textContent.replace(/[ ]/g, "").replace(/[\r\n]/g, "")
                    : 'Unknown';
                if (!(/(-)/g).test(avid)) {
                    avid = getAvCode(avid);
                }

                const container = document.createElement('div');
                container.className = 'jj-onejav-container';

                const btnMiss = mkLinkBtn(avid,
                    'https://missav.ws/' + avid,
                    'jj-missav');
                const btnSup = mkLinkBtn(avid,
                    'https://supjav.com/?s=' + encodeURIComponent(getSupjavSearchQuery(avid)),
                    'jj-supjav');
                const btnJav = mkLinkBtn(avid,
                    'https://javdb.com/search?q=' + encodeURIComponent(avid),
                    'jj-javdb');

                container.appendChild(btnMiss);
                container.appendChild(btnSup);
                container.appendChild(btnJav);
                title.parentNode.insertBefore(container, title);
            });
        }

        window.addEventListener('load', add);
    }

    // ═══════════════════════════════════════════════════════════
    //  U9A9:浮動面板
    // ═══════════════════════════════════════════════════════════
    function u9a9Init() {
        function tryCreate() {
            if (document.getElementById('jav-jumper-container')) return;
            var raw = extractU9a9Code();
            if (!raw) return;
            var processedCode = getAvCode(raw);
            // 排列:複製 → SupJav → MissAV
            createFloatingPanel(processedCode,
                'supjav', 'missav',
                processedCode, processedCode,
                'jj-supjav', 'jj-missav');
        }

        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', function() {
                var check = setInterval(function() {
                    if (document.querySelector('.panel-heading .panel-title')) {
                        clearInterval(check);
                        tryCreate();
                    }
                }, 100);
                setTimeout(function() { clearInterval(check); }, 5000);
            });
        } else {
            var check = setInterval(function() {
                if (document.querySelector('.panel-heading .panel-title')) {
                    clearInterval(check);
                    tryCreate();
                }
            }, 100);
            setTimeout(function() { clearInterval(check); }, 5000);
        }

        new MutationObserver(function() {
            if (!document.getElementById('jav-jumper-container')) tryCreate();
        }).observe(document.body, { childList: true, subtree: true });
    }

    // ═══════════════════════════════════════════════════════════
    //  JavDB / MissAV / SupJav:浮動面板
    // ═══════════════════════════════════════════════════════════
    function floatingInit() {
        var 番號 = getCode();
        if (!番號) return;

        if (window.location.href.includes('javdb.com')) {
            // 複製 → SupJav → MissAV
            createFloatingPanel(番號,
                'supjav', 'missav',
                番號, 番號,
                'jj-supjav', 'jj-missav');
        } else if (window.location.href.includes('missav.ws') || window.location.href.includes('missav.ai')) {
            // 複製 → SupJav → JavDB
            createFloatingPanel(番號,
                'supjav', 'javdb',
                番號, 番號,
                'jj-supjav', 'jj-javdb');
        } else if (window.location.href.includes('supjav.com')) {
            // 複製 → MissAV → JavDB
            createFloatingPanel(番號,
                'missav', 'javdb',
                番號, 番號,
                'jj-missav', 'jj-javdb');
        }

        // ── JavDB 磁力連接下載按鈕修改 ──
        // 磁力清單是 SPA 非同步載入的,所以對整個 body 做觀察,
        // 並先用 timer 補做幾次,避免觀察器啟動前資料就已載入完成。
        updateDownloadButtons();
        [0, 300, 1000, 2500].forEach(function(ms) {
            setTimeout(updateDownloadButtons, ms);
        });

        var observer = new MutationObserver(function() { updateDownloadButtons(); });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    // ═══════════════════════════════════════════════════════════
    //  JavDB 磁力:「下載」按鈕 → 改為 magnet 連結(觸發系統預設 BT 程式)
    //  (JJ_MAGNET_ATTR 已於檔尾分流之前宣告)
    // ═══════════════════════════════════════════════════════════

    // 由單一磁力項目中取得 magnet 連結
    //   1) 「複製」按鈕的 data-clipboard-text(最穩定)
    //   2) 標題連結本身的 href(有些列沒有複製按鈕)
    //   3) 原始 href 內藏在 keepshare 網址裡的 URL-encoded magnet
    function resolveMagnet(item, downloadButton) {
        var copyBtn = item.querySelector('.copy-to-clipboard');
        var m = copyBtn && copyBtn.getAttribute('data-clipboard-text');
        if (m && /^magnet:/i.test(m.trim())) return m.trim();

        var titleLink = item.querySelector('a[href^="magnet:"]');
        if (titleLink) {
            var t = titleLink.getAttribute('href');
            if (t && /^magnet:/i.test(t.trim())) return t.trim();
        }

        var raw = downloadButton && downloadButton.getAttribute('href');
        if (raw) {
            var decoded = raw;
            try { decoded = decodeURIComponent(raw); } catch (e) { /* 保底用原字串 */ }
            var idx = decoded.indexOf('magnet:');
            if (idx !== -1) return decoded.slice(idx);
        }
        return null;
    }

    // 點擊行為:
    //   直接依 href="magnet:..." 交給瀏覽器 → 作業系統 → 系統預設 BT 程式。
    //   不做「沒反應就再送一次」的補送,因為點擊 <a href="magnet:"> 時頁面不會隱藏,
    //   補送會讓 BT 程式跳出兩次「新增種子」。
    //   只需要在沒有註冊處理程式時,給使用者一個看得懂的提示。
    function bindMagnetClick(link) {
        if (link.dataset.jjBound === '1') return;
        link.dataset.jjBound = '1';

        link.addEventListener('click', function() {
            // 移除 target 後仍可能被某些擴充攔成新分頁,這裡確保永遠是「就地」交給系統
            link.removeAttribute('target');

            // 若使用者系統沒有 BT 程式可接手,頁面不會有任何變化,
            // 3 秒後在按鈕上短暫提示(不改內容、不重送連結)。
            var originalTitle = link.title;
            setTimeout(function() {
                if (document.visibilityState === 'visible' && link.isConnected) {
                    link.title = '若沒有任何反應,請確認系統已安裝並註冊 BT 程式(magnet: 連結)';
                    setTimeout(function() {
                        if (link.isConnected) link.title = originalTitle;
                    }, 4000);
                }
            }, 3000);
        });
    }

    function updateDownloadButtons() {
        if (!window.location.href.includes('javdb.com')) return;

        // 不依賴 .item 的層級(JavDB 改版時層級會變),
        // 直接抓「所有下載按鈕」,再往上找它自己所屬的那一列。
        var buttons = [].slice.call(
            document.querySelectorAll(
                '#magnets-content a.button.is-info.is-small, #magnets-content a[href*="keepshare"]'
            )
        );

        buttons.forEach(function (btn) {
            // 只處理「下載」這種按鈕;標題連結本身也是 magnet,不去動它
            var text = (btn.textContent || '').replace(/[\s\u00a0]+/g, '');
            if (text !== '下載' && text !== 'BT下載') return;

            // 往上找它所屬的列。JavDB 每列的 class 是 "item" / "item odd"
            // (注意:沒有 .columns),所以只有 .item 能當依據。
            var item = btn.closest('.item') || btn.closest('#magnets-content');
            if (!item) return;

            var magnet = resolveMagnet(item, btn);
            if (!magnet) return;

            // 只要把 href 指向 magnet 就好,外觀、文字、class 一律保持 JavDB 原樣,
            // 不改變網站的下載按鍵樣式。
            if (btn.getAttribute('href') !== magnet) {
                btn.setAttribute('href', magnet);
            }

            // 移除 target:交給作業系統的通訊協定處理常式接手,
            // 避免瀏覽器開新分頁去 fetch 這個磁力連結。
            btn.removeAttribute('target');
            btn.removeAttribute('rel');
            btn.setAttribute(JJ_MAGNET_ATTR, magnet);

            bindMagnetClick(btn);
        });
    }
})();