// ==UserScript==
// @name JAVDB/JAVBUS/JAVLIBRARY to Jable.tv Linker (Class Refactor)
// @namespace https://tampermonkey.net/
// @version 2025.09.29
// @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;}`,
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: 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;} .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);}`,
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();
})();