您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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.
// ==UserScript== // @name JAVDB/JAVBUS/JAVLIBRARY to Jable.tv Linker (Class Refactor) // @namespace https://tampermonkey.net/ // @version 2025.09.30 // @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} .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);}`, 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%;} .-mt-6 {width: 80%;height: 80%;margin: 0 auto !important;} .related-videos {display: grid;grid-template-columns: repeat(4, 1fr);gap: 10px;} .order-last,.order-last >div {max-width: 100% !important;min-width: 100% !important;} .order-last > div {display:grid; grid-template-columns: repeat(4,minmax(0,1fr));gap: 1.25rem;} .ad-banner,.advertisement,.ads,.list-none,.space-y-6{display:none !important;} .content-without-search > .flex {flex-direction: column;}.ml-6 {margin-left: 0 !important;}` }; 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 }); } }; // ==================== 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 (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专属逻辑 } if (config.isMagnetSite) { this.dom.injectStyleOnce(config.style, siteKey.replace(/\./g, "-")); this.dom.enhanceMagnetLinks(config); return; } if (config.style) this.dom.injectStyleOnce(config.style, siteKey.replace(/\./g, "-")); const isDetailPage = this.utils.isValidPath(config.pathCheck); 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(); })();