// ==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();
})();