您需要先安装一个扩展,例如 篡改猴、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
// ==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(); })();