双源
// ==UserScript== // @name JavBus Javdb trailer // @name:zh-CN JavBus/Javdb 预告片 // @namespace https://greasyfork.org/zh-CN/scripts/596086 // @version 2026.09.23 // @description 双源 // @description:zh-cn 双源 // @author dynamic-rewrite // @license GPL // @match *://www.javbus.com/* // @include *://javdb*.com/* // @include *://javdb.com/* // @match *://*.javlib.com/* // @match *://*.javlibrary.com/* // @include *://avmoo.*/* // @include *://avsox.*/* // @match *://*.sehuatang.net/* // @match *://www.tanhuazu.com/* // @match *://db.msin.jp/* // @match *://*/works/detail/* // @match *://javbooks.com/* // @match *://jmvbt.com/* // @include *://*.com/content*censored/*.htm // @match *://xslist.org/* // @grant GM_download // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_notification // @grant GM_setClipboard // @grant GM_addStyle // @grant GM_deleteValue // @require https://code.jquery.com/jquery-3.6.3.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/hls.min.js // @connect * // @connect javtrailers.com // @connect media.javtrailers.com // @connect dmm.co.jp // @connect cc3001.dmm.co.jp // @connect pv3001.dmm.co.jp // @connect cc3001.dmm.com // @connect api.video.dmm.co.jp // @run-at document-end // ==/UserScript== (function () { 'use strict'; var embyAPI = ""; var embyBaseUrl = ""; GM_addStyle(` .header a.red {color:red;padding-left:2px;padding-right:2px;line-height:22px;} #tiaozhuan a.red {margin-right:15px;padding:3px 5px;background-color:rgb(255,215,0);font-size:large;color:rgb(255,0,0)!important;} .trailer-box { display: block; width: 100%; box-sizing: border-box; text-align: center; border-radius: 4px; border: 1px solid #ccc; margin: 8px 0; background: #000; position: relative; } .trailer-box video.trailer-video { display: block !important; width: 100% !important; max-width: 100% !important; height: auto !important; aspect-ratio: 16 / 9; max-height: 80vh; min-height: 320px; background: #000; } .trailer-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; padding: 6px 12px; background: #1a1a1a; color: #bbb; font-size: 13px; border-bottom: 1px solid #333; min-height: 32px; box-sizing: border-box; border-radius: 4px 4px 0 0; } .trailer-controls { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; } .trailer-source-label, .trailer-quality-label { display: flex; align-items: center; gap: 6px; } .trailer-box.ready .trailer-status { display: none; } .trailer-box.ready.error .trailer-status { display: block !important; color: #f88; } .trailer-box.ready.error .trailer-toolbar { background: #3a1010; } .trailer-box:not(.ready) .trailer-quality-label { display: none !important; } .trailer-quality, .trailer-source { background: #333; color: #ddd; border: 1px solid #555; border-radius: 3px; padding: 3px 6px; font-size: 12px; cursor: pointer; } .trailer-quality option, .trailer-source option { background: #222; color: #ddd; } .trailer-box.error .trailer-toolbar { background: #3a1010; color: #f88; } .trailer-quality-probing { font-size: 12px; color: #888; } .video-meta-panel { height: auto !important; max-height: none !important; overflow: visible !important; padding-bottom: 0 !important; } #video_jacket_info { width: 100% !important; } `); console.log('[Trailer] ====== VERSION 2026.09.38 (Normalize Code) ======'); // ==================== 缓存 ==================== const CACHE_TTL = 5 * 24 * 60 * 60 * 1000; const CID_CACHE_TTL = 30 * 24 * 60 * 60 * 1000; const CACHE_PREFIX_M3U8 = 'jvl_trailer_jt_m3u8_v12_'; const CACHE_PREFIX_MP4 = 'jvl_trailer_jt_mp4_v12_'; const CID_CACHE_PREFIX = 'jvl_trailer_jt_cid_v4_'; function _cacheGet (prefix, code) { try { const key = prefix + String(code || '').toUpperCase(); const raw = GM_getValue(key, null); if (!raw) return null; const data = typeof raw === 'string' ? JSON.parse(raw) : raw; if (data && data.expire > Date.now() && data.url) return data.url; try { GM_deleteValue(key); } catch (e) {} } catch (e) {} return null; } function _cacheSet (prefix, code, url) { try { const key = prefix + String(code || '').toUpperCase(); GM_setValue(key, JSON.stringify({ url, expire: Date.now() + CACHE_TTL })); } catch (e) {} } const getCachedM3u8 = (code) => _cacheGet(CACHE_PREFIX_M3U8, code); const setCacheM3u8 = (code, url) => _cacheSet(CACHE_PREFIX_M3U8, code, url); const getCachedMp4 = (code) => _cacheGet(CACHE_PREFIX_MP4, code); const setCacheMp4 = (code, url) => _cacheSet(CACHE_PREFIX_MP4, code, url); function getCachedJavTrailersCid (code) { try { const key = CID_CACHE_PREFIX + String(code || '').toUpperCase(); const raw = GM_getValue(key, null); if (!raw) return null; const data = typeof raw === 'string' ? JSON.parse(raw) : raw; if (data && data.expire > Date.now() && data.cid) return data.cid; try { GM_deleteValue(key); } catch (e) {} } catch (e) {} return null; } function setCachedJavTrailersCid (code, cid) { try { const key = CID_CACHE_PREFIX + String(code || '').toUpperCase(); GM_setValue(key, JSON.stringify({ cid, expire: Date.now() + CID_CACHE_TTL })); } catch (e) {} } // ==================== 音量记忆 ==================== function getSavedVolume () { try { const v = GM_getValue('jvl_trailer_volume', null); if (v == null) return 0.5; const n = parseFloat(v); return isNaN(n) ? 0.5 : Math.max(0, Math.min(1, n)); } catch (e) { return 0.5; } } function getSavedMuted () { try { const m = GM_getValue('jvl_trailer_muted', null); if (m == null) return true; return String(m) === 'true'; } catch (e) { return true; } } function saveVolumeState (v) { try { GM_setValue('jvl_trailer_volume', String(v.volume)); GM_setValue('jvl_trailer_muted', String(v.muted)); } catch (e) {} } function applyVolumeState (v) { try { v.volume = getSavedVolume(); v.muted = getSavedMuted(); let t = null; v.addEventListener('volumechange', () => { if (t) clearTimeout(t); t = setTimeout(() => saveVolumeState(v), 300); }); } catch (e) {} } // ==================== 播放源记忆 ==================== const SOURCE_KEY = 'jvl_trailer_source'; const VALID_SOURCES = ['m3u8', 'mp4']; function getPreferredSource () { try { const s = GM_getValue(SOURCE_KEY, 'm3u8'); if (typeof s === 'string' && VALID_SOURCES.includes(s)) return s; } catch (e) {} return 'm3u8'; } function setPreferredSource (s) { if (!VALID_SOURCES.includes(s)) return; try { GM_setValue(SOURCE_KEY, String(s)); } catch (e) {} } // ==================== 清晰度偏好 ==================== const QUALITY_KEY = 'jvl_trailer_quality'; const VALID_PRESETS = ['auto-high', 'auto-low']; const SUPPORTED_HEIGHTS = [1080, 720, 576, 432, 288, 144]; function getPreferredQuality () { try { let q = GM_getValue(QUALITY_KEY, 'auto-high'); if (q === 'auto') q = 'auto-high'; if (typeof q !== 'string' || !q.length) return 'auto-high'; if (VALID_PRESETS.includes(q)) return q; const n = parseInt(q, 10); if (!isNaN(n) && n > 0) return String(n); } catch (e) {} return 'auto-high'; } function setPreferredQuality (q) { try { GM_setValue(QUALITY_KEY, String(q)); } catch (e) {} } function resolvePreference (variants, pref) { if (!variants || !variants.length) return null; if (pref === 'auto-high') return variants[0]; if (pref === 'auto-low') return variants[variants.length - 1]; const nPref = parseInt(pref, 10); if (isNaN(nPref)) return variants[0]; const exact = variants.find(v => v.height === nPref); if (exact) return exact; const higher = variants.filter(v => v.height > nPref).sort((a, b) => a.height - b.height); if (higher.length) return higher[0]; return variants[0]; } function firstTruthy (promises) { return new Promise((resolve) => { if (!promises.length) return resolve(null); let remaining = promises.length; let done = false; const finish = (v) => { if (done) return; done = true; resolve(v); }; promises.forEach(p => { Promise.resolve(p).then(v => { if (v) return finish(v); if (--remaining === 0) finish(null); }, () => { if (--remaining === 0) finish(null); }); }); }); } // ============================================================ // ★ 规范化番号:字母 + 数字(去掉前导零) // HRSM-159 / hrsm159 / hrsm00159 / HRSM_159 → "HRSM:159" // ============================================================ function normalizeCode (code) { if (!code) return ''; const s = String(code).toUpperCase().replace(/[^A-Z0-9]/g, ''); const m = s.match(/^([A-Z]+)(\d+)$/); if (!m) return s; const series = m[1]; const num = String(parseInt(m[2], 10)); return series + ':' + num; } // 检查一个文本是否包含目标番号 function containsCode (text, code) { const target = normalizeCode(code); if (!target) return false; const norm = String(text).toUpperCase().replace(/[^A-Z0-9]/g, ''); const re = /([A-Z]+)(\d+)/g; let m; while ((m = re.exec(norm)) !== null) { const series = m[1]; const num = String(parseInt(m[2], 10)); if (series + ':' + num === target) return true; } return false; } // ============================================================ // GM_xmlhttpRequest 版 hls.js Loader // ============================================================ function createGmLoader () { return class GmLoader { constructor (config) { this.config = config || {}; this.stats = { aborted: false, loaded: 0, retry: 0, total: 0, chunkCount: 0, bwEstimate: 0, loading: { start: 0, first: 0, end: 0 }, parsing: { start: 0, end: 0 }, buffering: { start: 0, first: 0, end: 0 } }; this.callbacks = null; this.context = null; } destroy () { this.abort(); } abort () { this.stats.aborted = true; } load (context, config, callbacks) { this.context = context; if (config) this.config = config; this.callbacks = callbacks; const url = context.url; const wantBinary = context.responseType === 'arraybuffer'; const timeoutMs = (config && config.timeout) || (this.config && this.config.timeout) || 20000; this.stats.loading.start = performance.now(); GM_xmlhttpRequest({ url, method: 'GET', timeout: timeoutMs, responseType: wantBinary ? 'arraybuffer' : 'text', headers: { 'Referer': 'https://javtrailers.com/', 'Origin': 'https://javtrailers.com', 'Accept': '*/*' }, onload: (res) => { if (this.stats.aborted) return; const status = Number(res.status || 0); const now = performance.now(); this.stats.loading.first = now; this.stats.loading.end = now; if (status >= 200 && status < 400) { let data; if (wantBinary) { data = res.response instanceof ArrayBuffer ? res.response : new ArrayBuffer(0); this.stats.loaded = data.byteLength || 0; } else { data = String(res.responseText || ''); this.stats.loaded = data.length; } this.stats.total = this.stats.loaded; this.stats.chunkCount = 1; callbacks.onSuccess({ url: res.finalUrl || url, data }, this.stats, context, res); } else { callbacks.onError({ code: status, text: 'HTTP ' + status }, context, res, this.stats); } }, onerror: () => { if (this.stats.aborted) return; callbacks.onError({ code: 0, text: 'network' }, context, null, this.stats); }, ontimeout: () => { if (this.stats.aborted) return; callbacks.onError({ code: 0, text: 'timeout' }, context, null, this.stats); } }); } }; } // ==================== M3U8 探测 ==================== function probeM3u8 (url) { return new Promise((resolve) => { if (!url) return resolve(false); GM_xmlhttpRequest({ url, method: 'GET', timeout: 3500, headers: { 'User-Agent': navigator.userAgent, 'Accept': 'application/vnd.apple.mpegurl, application/x-mpegURL, */*', 'Referer': 'https://javtrailers.com/' }, onload: (res) => { const status = Number(res.status || 0); const text = String(res.responseText || ''); const ok = status >= 200 && status < 400 && /#EXTM3U/i.test(text); console.log('[Trailer] M3U8 probe:', ok ? 'OK' : 'FAIL', status, url); resolve(ok); }, onerror: () => resolve(false), ontimeout: () => resolve(false) }); }); } function buildJavTrailersCids (code) { if (!code) return []; const parts = String(code).split(/-/); if (parts.length < 2) return []; const series = parts[0].toLowerCase().replace(/[^a-z0-9_]/g, ''); const numRaw = parts[1].replace(/[^0-9]/g, ''); if (!series || !numRaw) return []; const out = new Set(); out.add(series + numRaw); out.add(series + numRaw.padStart(3, '0')); out.add(series + numRaw.padStart(5, '0')); const stripped = String(parseInt(numRaw, 10)); if (stripped !== numRaw) out.add(series + stripped); return Array.from(out); } function buildJavTrailersM3u8Candidates (cid) { cid = String(cid || '').trim().toLowerCase(); if (!cid || cid.length < 3) return []; const first = cid[0]; const first3 = cid.substring(0, 3); return [ `https://media.javtrailers.com/hlsvideo/freepv/${first}/${first3}/${cid}/playlist.m3u8`, `https://media.javtrailers.com/hlsvideo/freepv/${first}/${first3}/${cid}/${cid}.m3u8`, `https://media.javtrailers.com/hlsvideo/freepv/${first}/${first3}/${cid}/master.m3u8` ]; } function fetchText (url, timeoutMs) { return new Promise((resolve) => { GM_xmlhttpRequest({ url, method: 'GET', timeout: timeoutMs || 6000, headers: { 'User-Agent': navigator.userAgent, 'Accept': 'text/html,application/xhtml+xml,application/json,*/*', 'Referer': 'https://javtrailers.com/' }, onload: (res) => { const s = Number(res.status || 0); if (s >= 200 && s < 400) resolve(String(res.responseText || '')); else { console.log('[Trailer][JT] 非2xx:', s, url); resolve(null); } }, onerror: () => resolve(null), ontimeout: () => resolve(null) }); }); } // ============================================================ // ★ 验证 CID 是否匹配番号 // ============================================================ async function verifyJavTrailersCid (cid, code) { if (!cid || !code) return false; try { const html = await fetchText(`https://javtrailers.com/video/${cid}`, 5000); if (!html) { console.log('[Trailer][JT-Verify] 页面加载失败:', cid); return false; } const sources = []; let m = html.match(/<title[^>]*>([^<]+)<\/title>/i); if (m) sources.push(m[1]); m = html.match(/<meta[^>]+property=["']og:title["'][^>]+content=["']([^"]+)["']/i); if (m) sources.push(m[1]); m = html.match(/<h1[^>]*>([^<]+)<\/h1>/i); if (m) sources.push(m[1]); m = html.match(/<meta[^>]+name=["']description["'][^>]+content=["']([^"]+)["']/i); if (m) sources.push(m[1]); for (const s of sources) { if (containsCode(s, code)) { console.log('[Trailer][JT-Verify] ✓ 匹配:', cid, '←', s.slice(0, 60)); return true; } } console.log('[Trailer][JT-Verify] ✗ 不匹配:', cid, '| 期望:', normalizeCode(code), '| 页面:', (sources[0] || '').slice(0, 60)); return false; } catch (e) { return false; } } // ============================================================ // ★ 从搜索页 HTML 提取 CID(必须匹配番号) // ============================================================ function extractJavTrailersCid (html, code) { if (!html) return null; if (html.indexOf('/video/') === -1) return null; const linkRe = /<a[^>]+href=["'](?:\/ja)?\/video\/([a-z0-9_]+)["'][^>]*>([\s\S]{0,500}?)<\/a>/gi; let m; while ((m = linkRe.exec(html)) !== null) { const cid = m[1]; const innerText = m[2].replace(/<[^>]+>/g, ' '); if (containsCode(innerText, code)) { console.log('[Trailer][JT-Search] ✓ 链接文本匹配:', cid, '←', innerText.slice(0, 60).replace(/\s+/g, ' ')); return cid; } if (containsCode(m[2], code) || containsCode(cid, code)) { console.log('[Trailer][JT-Search] ✓ 链接属性匹配:', cid); return cid; } } const allVideos = [...html.matchAll(/\/video\/([a-z0-9_]+)/gi)].map(x => x[1]); const unique = [...new Set(allVideos)]; for (const cid of unique) { if (containsCode(cid, code)) { console.log('[Trailer][JT-Search] ✓ CID 本身匹配:', cid); return cid; } } console.log('[Trailer][JT-Search] 找到', unique.length, '个 /video/ 链接但都不匹配:', unique.slice(0, 5)); return null; } // ============================================================ // 搜索 JavTrailers 拿真实 CID(带验证) // ============================================================ async function fetchJavTrailersCidFromSearch (code) { const cached = getCachedJavTrailersCid(code); if (cached) { console.log('[Trailer][JT-Search] ✓ 从缓存拿到 CID:', cached); return cached; } const clean = String(code).trim(); const searchUrls = [ `https://javtrailers.com/ja/search/${clean}`, `https://javtrailers.com/search/${clean}` ]; console.log('[Trailer][JT-Search] 并行搜索:', searchUrls, '| 期望:', normalizeCode(code)); const results = await Promise.all(searchUrls.map(async (url) => { try { const html = await fetchText(url, 6000); if (!html) return null; const cid = extractJavTrailersCid(html, code); if (!cid) { console.log('[Trailer][JT-Search] HTML', html.length, 'bytes 但无匹配链接:', url); return null; } const valid = await verifyJavTrailersCid(cid, code); if (!valid) { console.log('[Trailer][JT-Search] ✗ 验证失败,跳过:', cid); return null; } console.log('[Trailer][JT-Search] ✓', url, '→', cid); return cid; } catch (e) {} return null; })); const cid = results.find(r => r); if (cid) { setCachedJavTrailersCid(code, cid); console.log('[Trailer][JT-Search] ✓ 已缓存 CID'); } return cid; } async function probeCidsAll (cids) { if (!cids || !cids.length) return null; const allUrls = []; for (const cid of cids) { for (const u of buildJavTrailersM3u8Candidates(cid)) allUrls.push(u); } console.log('[Trailer][JT-M3U8] 候选 URL 数:', allUrls.length); const results = await Promise.all(allUrls.map(async (url) => ({ url, ok: await probeM3u8(url) }))); const hit = results.find(r => r.ok); return hit ? hit.url : null; } // ============================================================ // 主入口 // ============================================================ async function resolveJavTrailersM3u8 (code) { if (!code) return null; let finished = false; const finish = (url, from) => { if (finished) return null; if (url) { finished = true; console.log('[Trailer][JT-M3U8] ✓ 命中 via', from, ':', url); } return url; }; const cachedCid = getCachedJavTrailersCid(code); if (cachedCid) { console.log('[Trailer][JT-M3U8] 使用缓存的 CID:', cachedCid); const url = await probeCidsAll([cachedCid]); if (url) return finish(url, 'CID缓存'); console.log('[Trailer][JT-M3U8] 缓存 CID 探测失败,继续'); } const directPromise = (async () => { const cids = buildJavTrailersCids(code); if (!cids.length) return null; console.log('[Trailer][JT-M3U8] 直接构造 CID:', cids); const allCandidates = []; for (const cid of cids) { for (const u of buildJavTrailersM3u8Candidates(cid)) { allCandidates.push({ url: u, cid: cid }); } } const probeResults = await Promise.all(allCandidates.map(async (c) => { const ok = await probeM3u8(c.url); return ok ? c : null; })); const hits = probeResults.filter(r => r); if (!hits.length) return null; console.log('[Trailer][JT-M3U8] 探测命中', hits.length, '个,逐个验证'); for (const hit of hits) { if (finished) return null; const valid = await verifyJavTrailersCid(hit.cid, code); if (valid) return finish(hit.url, '直接构造+验证'); } console.log('[Trailer][JT-M3U8] 所有命中均验证失败'); return null; })(); const searchPromise = new Promise((resolve) => { setTimeout(async () => { if (finished) return resolve(null); console.log('[Trailer][JT-M3U8] 搜索页兜底启动…'); try { const realCid = await fetchJavTrailersCidFromSearch(code); if (finished || !realCid) return resolve(null); const url = await probeCidsAll([realCid]); resolve(finish(url, '搜索页')); } catch (e) { resolve(null); } }, 300); }); return await firstTruthy([directPromise, searchPromise]); } // ==================== DMM MP4 相关 ==================== function extractPvFromHtml (html) { if (!html) return ''; const decoded = html.replace(/\\\//g, '/'); const patterns = [ /https?:\/\/cc3001\.dmm\.co\.jp\/pv\/[^"'\s<>\\]+\.mp4/gi, /https?:\/\/cc3001\.dmm\.com\/pv\/[^"'\s<>\\]+\.mp4/gi, /https?:\/\/cc3001\.dmm\.co\.jp\/litevideo\/freepv\/[^"'\s<>\\]+\.mp4/gi, /https?:\/\/cc3001\.dmm\.com\/litevideo\/freepv\/[^"'\s<>\\]+\.mp4/gi, ]; for (const p of patterns) { const m = decoded.match(p); if (m && m.length) { const best = m.find(u => /hhb\.mp4$/i.test(u)) || m.find(u => /hmb\.mp4$/i.test(u)) || m.find(u => /mhb\.mp4$/i.test(u)) || m.find(u => /mmb\.mp4$/i.test(u)) || m.find(u => /dm\.mp4$/i.test(u)) || m.find(u => /sm\.mp4$/i.test(u)); return best || m[0]; } } return ''; } function extractCidsFromHtml (html) { if (!html) return []; const decoded = html.replace(/\\\//g, '/'); const cids = new Set(); let m; const patterns = [ /[?&]cid=([a-z0-9_]+)/gi, /data-cid="([a-z0-9_]+)"/gi, /"cid"\s*:\s*"([a-z0-9_]+)"/gi, /pics\.dmm\.co\.jp\/(?:digital|mono|rental)\/(?:video|videoa|movie|adult)\/(?:adult\/)?([a-z0-9_]+)\//gi, /\/cid\/([a-z0-9_]+)\//gi ]; for (const re of patterns) while ((m = re.exec(decoded)) !== null) cids.add(m[1].toLowerCase()); return Array.from(cids); } function filterMatchingCids (cids, series, num) { const s = series.toLowerCase(), n = String(num); const n3 = n.padStart(3, '0'), n5 = n.padStart(5, '0'); const stripped = String(parseInt(n, 10)); const matches = cids.filter(c => c.includes(s) && (c.includes(n) || c.includes(n3) || c.includes(n5) || c.includes(stripped))); matches.sort((a, b) => { const sc = (x) => { if (x === s + n3) return 0; if (x === s + n5) return 1; if (x === s + n) return 2; if (x.endsWith(s + n3)) return 3; if (x.endsWith(s + n5)) return 4; if (x.endsWith(s + n)) return 5; return 10; }; return sc(a) - sc(b); }); return matches; } const _cidCache = new Map(); function fetchCidsFromDmmSearch (code) { const c = _cidCache.get(code); if (c && Date.now() - c.t < 60000) return Promise.resolve(c.cids); return new Promise((resolve) => { const parts = code.split(/-/); if (parts.length < 2) return resolve([]); const series = parts[0].toLowerCase(), num = parts[1]; const urls = []; for (const kw of [`${series}${num}`, `${series}-${num}`, `${series}${num.padStart(5, '0')}`]) { const enc = encodeURIComponent(kw); urls.push(`https://www.dmm.co.jp/digital/videoa/-/search/=/searchstr=${enc}/`); urls.push(`https://www.dmm.co.jp/mono/dvd/-/search/=/searchstr=${enc}/`); } let done = false, pending = urls.length; const all = new Set(); const finish = () => { if (done) return; done = true; const f = filterMatchingCids(Array.from(all), series, num); _cidCache.set(code, { cids: f, t: Date.now() }); resolve(f); }; urls.forEach((url) => { GM_xmlhttpRequest({ url, method: 'GET', timeout: 5000, headers: { 'User-Agent': navigator.userAgent, 'Accept': 'text/html,application/xhtml+xml', 'Accept-Language': 'ja,en-US;q=0.7,en;q=0.3', 'Referer': 'https://www.dmm.co.jp/', 'Cookie': 'age_check_done=1;' }, onload: (res) => { if (done) return; extractCidsFromHtml(res.responseText || '').forEach(c => all.add(c)); pending--; const f = filterMatchingCids(Array.from(all), series, num); if (f.length > 0) setTimeout(finish, 100); else if (pending === 0) finish(); }, onerror: () => { pending--; if (pending === 0) finish(); }, ontimeout: () => { pending--; if (pending === 0) finish(); } }); }); }); } function buildCidCandidates (series, num) { const n = String(num), stripped = String(parseInt(n, 10)); const n3 = n.padStart(3, '0'), n5 = n.padStart(5, '0'); const out = []; const push = (c) => { if (c && !out.includes(c)) out.push(c); }; push(series + n); push(series + n3); push(series + n5); if (stripped !== n) push(series + stripped); return out; } const DMM_SUFFIX_ORDER = [ { height: 1080, sfx: 'hhb' }, { height: 720, sfx: 'hmb' }, { height: 576, sfx: 'mhb' }, { height: 432, sfx: 'mmb' }, { height: 288, sfx: 'dm' }, { height: 144, sfx: 'sm' } ]; const SUFFIX_TO_HEIGHT = { hhb: 1080, hmb: 720, mhb: 576, mmb: 432, dm: 288, sm: 144 }; function buildCandidates (rawUrl) { if (!rawUrl) return []; const url = String(rawUrl).replace(/\?.*$/, ''); let m = url.match(/^(.+?)\/(\d{3,4})p\.mp4$/i); if (m) return SUPPORTED_HEIGHTS.map(h => ({ height: h, url: `${m[1]}/${h}p.mp4` })); m = url.match(/^(.+?)_(hhb|hmb|mhb|mmb|dm|sm)_([ws])\.mp4$/i); if (m) { const base = m[1] + '_', w = m[3]; return DMM_SUFFIX_ORDER.map(e => ({ height: e.height, url: `${base}${e.sfx}_${w}.mp4` })); } m = url.match(/^(.+?)_(hhb|hmb|mhb|mmb|dm|sm)\.mp4$/i); if (m) { const base = m[1] + '_'; return DMM_SUFFIX_ORDER.map(e => ({ height: e.height, url: `${base}${e.sfx}.mp4` })); } m = url.match(/^(.+?)(hhb|hmb|mhb|mmb|dm|sm)\.mp4$/i); if (m) { const base = m[1]; return DMM_SUFFIX_ORDER.map(e => ({ height: e.height, url: `${base}${e.sfx}.mp4` })); } return [{ height: 0, url }]; } function probeQuick (url) { return new Promise((resolve) => { const v = document.createElement('video'); v.muted = true; v.preload = 'metadata'; v.style.cssText = 'position:absolute;left:-99999px;top:-99999px;width:1px;height:1px;opacity:0;pointer-events:none;'; let done = false; const cleanup = () => { if (done) return; done = true; try { v.pause(); } catch (e) {} try { v.removeAttribute('src'); v.load(); } catch (e) {} try { v.remove(); } catch (e) {} }; v.addEventListener('loadedmetadata', () => { const h = v.videoHeight || 0; cleanup(); resolve({ ok: h > 0, height: h, url }); }, { once: true }); v.addEventListener('error', () => { cleanup(); resolve({ ok: false, url }); }, { once: true }); v.src = url; document.body.appendChild(v); setTimeout(() => { if (!done) { cleanup(); resolve({ ok: false, url }); } }, 2500); }); } async function pickForPref (url, pref) { const cs = buildCandidates(url); if (cs.length === 1 && cs[0].height === 0) return cs[0]; let order; if (pref === 'auto-high') order = [1080, 720, 576, 432, 288, 144]; else if (pref === 'auto-low') order = [144, 288, 432, 576, 720, 1080]; else { const n = parseInt(pref, 10); if (isNaN(n)) order = [1080, 720, 576, 432, 288, 144]; else { const rest = SUPPORTED_HEIGHTS.filter(h => h !== n).sort((a, b) => Math.abs(a - n) - Math.abs(b - n)); order = [n, ...rest]; } } for (const h of order) { const ms = cs.filter(c => c.height === h); for (const c of ms) { const r = await probeQuick(c.url); if (r.ok) return { height: h, url: c.url }; } } return { height: 0, url }; } function fetchPvFromCid (cid) { return new Promise((resolve) => { const urls = [ `https://www.dmm.co.jp/mono/dvd/-/detail/=/cid=${cid}/`, `https://www.dmm.co.jp/digital/videoa/-/detail/=/cid=${cid}/`, `https://www.dmm.co.jp/litevideo/-/part/=/cid=${cid}/size=720_480/affi_id=ProgramDMM-001/` ]; let idx = 0; const next = () => { if (idx >= urls.length) return resolve(null); const url = urls[idx++]; GM_xmlhttpRequest({ url, method: 'GET', timeout: 5000, headers: { 'User-Agent': navigator.userAgent, 'Accept': 'text/html,application/xhtml+xml', 'Accept-Language': 'ja,en-US;q=0.7,en;q=0.3', 'Referer': 'https://www.dmm.co.jp/', 'Cookie': 'age_check_done=1;' }, onload: (res) => { const pv = extractPvFromHtml(res.responseText || ''); if (pv) resolve(pv); else next(); }, onerror: next, ontimeout: next }); }; next(); }); } function fetchFanzaTrailerUrl (code) { return new Promise((resolve) => { const parts = code.split(/-/); if (parts.length < 2) return resolve(null); const series = parts[0].toLowerCase(), num = parts[1]; const words = [`${parts[0]}-${num}`, `${series}${num}`, `${series}${num.padStart(5, '0')}`]; let idx = 0; const next = () => { if (idx >= words.length) return resolve(null); const sw = words[idx++]; const q = `{ legacySearchPPV(limit: 10, searchWord: "${sw}") { items { cid title sampleUrl sampleMovieUrl } } }`; GM_xmlhttpRequest({ url: 'https://api.video.dmm.co.jp/graphql', method: 'POST', timeout: 8000, headers: { 'Content-Type': 'application/json', 'User-Agent': navigator.userAgent, 'Origin': 'https://www.dmm.co.jp', 'Referer': 'https://www.dmm.co.jp/' }, data: JSON.stringify({ query: q }), onload: (res) => { try { const data = JSON.parse(res.responseText); const items = (data && data.data && data.data.legacySearchPPV && data.data.legacySearchPPV.items) || []; for (const it of items) { if (it.sampleUrl && it.sampleUrl.startsWith('http')) return resolve(it.sampleUrl); if (it.sampleMovieUrl && it.sampleMovieUrl.startsWith('http')) return resolve(it.sampleMovieUrl); } const cids = items.map(it => it.cid).filter(Boolean); if (cids.length) { const f = filterMatchingCids(cids, series, num); const target = f.length ? f : cids; let pending = target.length; target.forEach(cid => fetchPvFromCid(cid).then(pv => { if (pv) return resolve(pv); pending--; if (pending === 0) next(); })); return; } next(); } catch (e) { next(); } }, onerror: next, ontimeout: next }); }; next(); }); } function buildDmmUrlList (series, num) { const cids = buildCidCandidates(series, num); const urls = []; const add = (c, path) => urls.push(`https://www.dmm.co.jp/${path}/-/detail/=/cid=${c}/`); const addLite = (c) => urls.push(`https://www.dmm.co.jp/litevideo/-/part/=/cid=${c}/size=720_480/affi_id=ProgramDMM-001/`); for (const c of cids) add(c, 'mono/dvd'); for (const c of cids) add(c, 'digital/videoa'); for (const c of cids) addLite(c); return urls; } function fetchDmmTrailerUrlParallel (code) { return new Promise((resolve) => { const parts = code.split(/-/); if (parts.length < 2 || !/^\d+$/.test(parts[1])) return resolve(null); const series = parts[0].toLowerCase(); let done = false; const finish = (url) => { if (done || !url) return; done = true; resolve(url); }; const guessUrls = buildDmmUrlList(series, parts[1]); const CONC = 4; let nextIdx = 0; const run = () => { if (done || nextIdx >= guessUrls.length) return; const url = guessUrls[nextIdx++]; GM_xmlhttpRequest({ url, method: 'GET', timeout: 5000, headers: { 'User-Agent': navigator.userAgent, 'Accept': 'text/html,application/xhtml+xml', 'Accept-Language': 'ja,en-US;q=0.7,en;q=0.3', 'Referer': 'https://www.dmm.co.jp/', 'Cookie': 'age_check_done=1;' }, onload: (res) => { if (done) return; const pv = extractPvFromHtml(res.responseText || ''); if (pv) finish(pv); else run(); }, onerror: () => { if (!done) run(); }, ontimeout: () => { if (!done) run(); } }); }; for (let i = 0; i < Math.min(CONC, guessUrls.length); i++) run(); setTimeout(() => { if (done) return; fetchCidsFromDmmSearch(code).then(cids => { if (done || !cids.length) return; cids.slice(0, 5).forEach(cid => fetchPvFromCid(cid).then(pv => { if (pv) finish(pv); })); }); }, 100); setTimeout(() => { if (done) return; fetchFanzaTrailerUrl(code).then(url => { if (url) finish(url); }); }, 500); setTimeout(() => { if (!done) { done = true; resolve(null); } }, 10000); }); } function fetchJavdbTrailerUrl (code) { return new Promise((resolve) => { const searchUrl = 'https://javdb.com/search?q=' + encodeURIComponent(code) + '&f=all'; GM_xmlhttpRequest({ url: searchUrl, method: 'GET', timeout: 15000, headers: { 'User-Agent': navigator.userAgent, 'Accept': 'text/html,application/xhtml+xml', 'Referer': 'https://javdb.com/' }, onload: (res) => { try { const doc = new DOMParser().parseFromString(res.responseText, 'text/html'); let links = doc.querySelectorAll('.movie-list .item a[href*="/v/"]'); if (!links.length) links = doc.querySelectorAll('a[href*="/v/"]'); if (!links.length) return resolve(null); const norm = code.toUpperCase().replace(/_/g, '-'); let href = null; for (const a of links) { const uid = a.querySelector('.uid'); const uidText = uid ? uid.textContent.trim().toUpperCase().replace(/_/g, '-') : ''; if (uidText === norm || (a.textContent || '').toUpperCase().replace(/_/g, '-').includes(norm)) { href = a.getAttribute('href'); break; } } if (!href) href = links[0].getAttribute('href'); if (!href) return resolve(null); const url = href.startsWith('http') ? href : 'https://javdb.com' + href; GM_xmlhttpRequest({ url, method: 'GET', timeout: 15000, headers: { 'User-Agent': navigator.userAgent, 'Accept': 'text/html,application/xhtml+xml', 'Referer': 'https://javdb.com/search' }, onload: (r2) => { const html = r2.responseText || ''; const pv = extractPvFromHtml(html); if (pv) return resolve(pv); const pats = [/<video[^>]*id="preview-video"[^>]*src="([^"]+\.mp4[^"]*)"/i, /<source[^>]*src="([^"]+\.mp4[^"]*)"/i]; for (const p of pats) { const m = html.match(p); if (m && m[1] && m[1].startsWith('http')) return resolve(m[1]); } resolve(null); }, onerror: () => resolve(null), ontimeout: () => resolve(null) }); } catch (e) { resolve(null); } }, onerror: () => resolve(null), ontimeout: () => resolve(null) }); }); } // ==================== Base ==================== class Base { constructor () { this._lastVideo = null; } _injectPlaceholder (obj, code, opts) { opts = opts || {}; const html = ` <div class="trailer-box" data-code="${code || ''}"> <div class="trailer-toolbar"> <span class="trailer-status">加载预告片中…</span> <span class="trailer-controls"> <label class="trailer-source-label">播放源: <select class="trailer-source"> <option value="m3u8">源一 (M3U8)</option> <option value="mp4">源二 (MP4)</option> </select> </label> <label class="trailer-quality-label" style="display:none;">清晰度: <select class="trailer-quality"></select> </label> <span class="trailer-quality-probing" style="display:none;">探测中…</span> </span> </div> <video class="trailer-video" controls playsinline muted preload="metadata"></video> </div>`; const $html = $(html); $(obj).before($html); const box = $html[0]; const videoEl = box.querySelector('video.trailer-video'); applyVolumeState(videoEl); this._lastVideo = videoEl; const srcSel = box.querySelector('.trailer-source'); const srcLabel = box.querySelector('.trailer-source-label'); if (opts.hideSource) { if (srcLabel) srcLabel.style.display = 'none'; } else { srcSel.value = getPreferredSource(); srcSel.addEventListener('change', () => { setPreferredSource(srcSel.value); this._startLoad(box, videoEl); }); } const qSel = box.querySelector('.trailer-quality'); qSel.addEventListener('change', () => { this._onQualityChange(box, videoEl, qSel.value); }); return { box, videoEl }; } addVideo (code, obj) { if (/^HEYZO-/i.test(code)) { const n = code.split(/-/)[1]; const { box, videoEl } = this._injectPlaceholder(obj, code, { hideSource: true }); return this._fillSource(box, videoEl, `https://sample.heyzo.com/contents/3000/${n}/sample.mp4`, code); } if (/^HEYZ-/i.test(code)) { const n = code.split(/-/)[1]; const { box, videoEl } = this._injectPlaceholder(obj, code, { hideSource: true }); return this._fillSource(box, videoEl, `https://www.heyzo.com/contents/3000/${n}/heyzo_hd_${n}_sample.mp4`, code); } if (/^FC2-/i.test(code)) { const n = code.replace(/FC2(-PPV)?-/, ''); $(obj).before(`<div class="trailer-box ready"><iframe src="https://contents.fc2.com/embed/${n}" style="width:100%;height:78vh;min-height:500px;border:none;"></iframe></div>`); return; } const { box, videoEl } = this._injectPlaceholder(obj, code); this._startLoad(box, videoEl); } _startLoad (box, videoEl) { const code = box.getAttribute('data-code') || ''; box._gen = (box._gen || 0) + 1; this._loadTrailer(box, videoEl, code, box._gen); } async _loadTrailer (box, videoEl, code, gen) { const status = box.querySelector('.trailer-status'); const probing = box.querySelector('.trailer-quality-probing'); const qLabel = box.querySelector('.trailer-quality-label'); const stale = () => box._gen !== gen; box.classList.remove('ready', 'error'); if (qLabel) qLabel.style.display = 'none'; if (probing) { probing.style.display = ''; probing.textContent = '探测中…'; } if (status) status.textContent = '加载预告片中…'; this._resetVideo(videoEl); const srcPref = getPreferredSource(); console.log('[Trailer] 使用播放源:', srcPref, '| 番号:', code); let ok = false; if (srcPref === 'm3u8') ok = await this._tryM3u8(box, videoEl, code, probing, stale, gen); else ok = await this._tryMp4(box, videoEl, code, probing, stale, gen); if (stale()) return; if (probing) probing.style.display = 'none'; if (!ok) { box.classList.remove('ready'); const msg = srcPref === 'm3u8' ? '未找到 m3u8,请手动切「源二 (MP4)」重试' : '未找到 MP4,请手动切「源一 (M3U8)」重试'; this._showPersistentNotice(box, msg); } } _resetVideo (v) { if (v._hls) { try { v._hls.destroy(); } catch (e) {} v._hls = null; } v._mode = null; v._variants = null; v._originalUrl = null; v.querySelectorAll('source').forEach(s => s.remove()); v.removeAttribute('src'); try { v.load(); } catch (e) {} } async _tryM3u8 (box, videoEl, code, probing, stale, gen) { if (!code) return false; let url = ''; const cached = getCachedM3u8(code); if (cached) { console.log('[Trailer] m3u8 缓存命中:', cached); if (probing) probing.textContent = '验证缓存…'; if (await probeM3u8(cached)) url = cached; else console.log('[Trailer] m3u8 缓存已失效'); } if (stale()) return false; if (!url) { if (probing) probing.textContent = 'JavTrailers M3U8 解析中…'; url = (await resolveJavTrailersM3u8(code)) || ''; } if (stale()) return false; if (!url) { console.log('[Trailer] JavTrailers 未找到 m3u8'); return false; } console.log('[Trailer] ✓ M3U8 found:', url); if (probing) probing.style.display = 'none'; box.classList.add('ready'); const ok = await this._playM3u8(box, videoEl, url, probing, stale, gen); if (ok && !stale()) setCacheM3u8(code, url); if (!ok && !stale()) box.classList.remove('ready'); return ok; } _guessHeightFromLevel (lv, idx) { if (lv.height && lv.height > 0) return lv.height; const url = String(lv.url || lv.uri || ''); const m = url.match(/(hhb|hmb|mhb|mmb|dm|sm)\.m3u8(?:\?|$)/i); if (m) { const h = SUFFIX_TO_HEIGHT[m[1].toLowerCase()]; if (h) return h; } const br = Number(lv.bitrate || (lv.attrs && lv.attrs.BANDWIDTH) || 0); if (br > 0) { if (br > 5e6) return 1080; if (br > 3e6) return 720; if (br > 1.5e6) return 432; if (br > 8e5) return 288; return 144; } return 720 + idx; } _playM3u8 (box, videoEl, m3u8Url, probing, stale, gen) { return new Promise((resolve) => { if (typeof Hls === 'undefined') return resolve(false); if (!Hls.isSupported()) { videoEl._mode = 'm3u8'; videoEl._variants = [{ height: 0, source: 'hls', levelIdx: 0 }]; videoEl.src = m3u8Url; videoEl.play().catch(() => {}); this._setQualityOptions(box, videoEl, [], 'm3u8'); return resolve(true); } const GmLoader = createGmLoader(); const hls = new Hls({ maxBufferLength: 30, maxMaxBufferLength: 60, enableWorker: false, lowLatencyMode: false, loader: GmLoader, pLoader: GmLoader, fLoader: GmLoader }); videoEl._hls = hls; let done = false; let uiApplied = false; const applyQualityUI = (from) => { if (stale()) return; const lv = hls.levels || []; let ui; if (lv.length > 0) ui = lv.map((l, i) => ({ height: this._guessHeightFromLevel(l, i), source: 'hls', levelIdx: i })); else ui = [{ height: 720, source: 'hls', levelIdx: 0 }]; ui.sort((a, b) => b.height - a.height); console.log('[Trailer] applyQualityUI(' + from + '):', ui); videoEl._mode = 'm3u8'; videoEl._variants = ui; this._setQualityOptions(box, videoEl, ui, 'm3u8'); const pref = getPreferredQuality(); const t = resolvePreference(ui, pref); if (t && typeof t.levelIdx === 'number' && lv.length > 0) { try { hls.currentLevel = t.levelIdx; } catch (e) {} try { hls.loadLevel = t.levelIdx; } catch (e) {} } }; const levelPoller = setInterval(() => { if (stale()) { clearInterval(levelPoller); return; } if (uiApplied) { clearInterval(levelPoller); return; } const lv = hls.levels || []; if (lv.length > 0) { applyQualityUI('poll'); uiApplied = true; clearInterval(levelPoller); } }, 200); const safetyTimer = setTimeout(() => { if (!uiApplied && !stale()) { applyQualityUI('safety'); uiApplied = true; clearInterval(levelPoller); } }, 30000); const finishOk = () => { if (done) return; done = true; clearInterval(levelPoller); clearTimeout(safetyTimer); if (stale()) { try { hls.destroy(); } catch (e) {} if (videoEl._hls === hls) videoEl._hls = null; return resolve(false); } if (!uiApplied) { applyQualityUI('manifest'); uiApplied = true; } videoEl.play().catch(() => {}); resolve(true); }; const onPlaying = () => { if (!uiApplied) { applyQualityUI('playing'); uiApplied = true; } }; videoEl.addEventListener('playing', onPlaying); const onLoadedData = () => { if (!uiApplied) { applyQualityUI('loadeddata'); uiApplied = true; } }; videoEl.addEventListener('loadeddata', onLoadedData); const finishFail = (reason) => { if (done) return; done = true; clearInterval(levelPoller); clearTimeout(safetyTimer); console.log('[Trailer] m3u8 播放失败(' + reason + ')'); try { hls.destroy(); } catch (e) {} if (videoEl._hls === hls) videoEl._hls = null; resolve(false); }; hls.on(Hls.Events.MANIFEST_PARSED, finishOk); hls.on(Hls.Events.LEVEL_LOADED, () => { if (!uiApplied && (hls.levels || []).length > 0) { applyQualityUI('level-loaded'); uiApplied = true; } }); hls.on(Hls.Events.ERROR, (e, d) => { console.log('[Trailer] hls error:', d.type, d.details, 'fatal:', d.fatal); if (d.fatal) finishFail(d.details); }); setTimeout(() => { if (!done) finishFail('timeout'); }, 15000); hls.loadSource(m3u8Url); hls.attachMedia(videoEl); }); } async _tryMp4 (box, videoEl, code, probing, stale, gen) { if (!code) return false; let url = getCachedMp4(code); if (url) console.log('[Trailer] mp4 缓存命中:', url); if (!url) { if (probing) probing.textContent = 'DMM MP4 解析中…'; url = await fetchDmmTrailerUrlParallel(code); } if (stale()) return false; if (!url) { if (probing) probing.textContent = 'JavDB 兜底中…'; url = await fetchJavdbTrailerUrl(code); } if (stale()) return false; if (!url) return false; console.log('[Trailer] ✓ MP4 found:', url); if (probing) probing.style.display = 'none'; box.classList.add('ready'); const ok = await this._fillSource(box, videoEl, url, code, probing, stale, gen); if (ok && !stale()) setCacheMp4(code, url); if (!ok && !stale()) box.classList.remove('ready'); return ok; } async _fillSource (box, videoEl, originalUrl, code, probing, stale, gen) { if (!box || !videoEl || !originalUrl) return false; console.log('[Trailer] Original mp4 source:', originalUrl); const pref = getPreferredQuality(); let picked; try { picked = await pickForPref(originalUrl, pref); } catch (e) { picked = { height: 0, url: originalUrl }; } if (!picked) picked = { height: 0, url: originalUrl }; if (stale && stale()) return false; videoEl._mode = 'mp4'; videoEl._originalUrl = originalUrl; videoEl._variants = buildCandidates(originalUrl).filter(c => c.height > 0); videoEl.querySelectorAll('source').forEach(s => s.remove()); videoEl.removeAttribute('src'); const s = document.createElement('source'); s.src = picked.url; s.type = 'video/mp4'; videoEl.appendChild(s); this._clearNotice(box); if (probing) probing.style.display = 'none'; try { videoEl.load(); } catch (e) {} videoEl.play().catch(() => {}); this._setQualityOptions(box, videoEl, videoEl._variants, 'mp4'); return true; } _switchSource (videoEl, url) { if (!videoEl || !url) return; const t = videoEl.currentTime || 0; const wasPlaying = !videoEl.paused; videoEl.querySelectorAll('source').forEach(s => s.remove()); videoEl.removeAttribute('src'); const s = document.createElement('source'); s.src = url; s.type = 'video/mp4'; videoEl.appendChild(s); try { videoEl.load(); } catch (e) {} const onMeta = () => { try { if (t > 0.5 && t < (videoEl.duration || 1e9) - 0.5) videoEl.currentTime = t; } catch (e) {} if (wasPlaying) videoEl.play().catch(() => {}); videoEl.removeEventListener('loadedmetadata', onMeta); }; videoEl.addEventListener('loadedmetadata', onMeta); } _setQualityOptions (box, videoEl, variants, mode) { const select = box.querySelector('.trailer-quality'); const label = box.querySelector('.trailer-quality-label'); if (!select) return; let html = '<option value="auto-high">默认最高</option><option value="auto-low">默认最低</option>'; if (mode === 'm3u8') { const list = (variants && variants.length) ? variants : []; for (const v of list) html += `<option value="${v.height}">${v.height}p</option>`; } else { for (const h of SUPPORTED_HEIGHTS) html += `<option value="${h}">${h}p</option>`; } select.innerHTML = html; const pref = getPreferredQuality(); if (Array.from(select.options).some(o => o.value === pref)) select.value = pref; else select.value = 'auto-high'; if (label) { box.classList.add('ready'); label.style.setProperty('display', 'flex', 'important'); } } async _onQualityChange (box, videoEl, q) { setPreferredQuality(q); const probing = box.querySelector('.trailer-quality-probing'); if (videoEl._mode === 'm3u8') { const target = resolvePreference(videoEl._variants || [], q); if (target && typeof target.levelIdx === 'number' && videoEl._hls) { try { videoEl._hls.currentLevel = target.levelIdx; } catch (e) {} try { videoEl._hls.loadLevel = target.levelIdx; } catch (e) {} } return; } if (videoEl._mode === 'mp4') { const orig = videoEl._originalUrl; if (!orig) return; if (probing) { probing.style.display = ''; probing.textContent = '切换清晰度…'; } const target = await pickForPref(orig, q); if (probing) probing.style.display = 'none'; if (target) this._switchSource(videoEl, target.url); } } _showPersistentNotice (box, msg) { const status = box.querySelector('.trailer-status'); if (!status) return; status.textContent = msg; box.classList.add('error'); } _clearNotice (box) { const status = box.querySelector('.trailer-status'); if (status) status.textContent = '加载预告片中…'; box.classList.remove('error'); } addVideoC (code, obj) { const { box, videoEl } = this._injectPlaceholder(obj, code, { hideSource: true }); this._fillSource(box, videoEl, `https://smovie.caribbeancom.com/sample/movies/${code}/1080p.mp4`, code); } addVideoY (code, obj) { const { box, videoEl } = this._injectPlaceholder(obj, code, { hideSource: true }); this._fillSource(box, videoEl, `https://smovie.1pondo.tv/sample/movies/${code}/1080p.mp4`, code); } addVideoH (code, obj) { const { box, videoEl } = this._injectPlaceholder(obj, code, { hideSource: true }); this._fillSource(box, videoEl, `https://smovie.10musume.com/sample/movies/${code}/1080p.mp4`, code); } addVideoPM (code, obj) { const { box, videoEl } = this._injectPlaceholder(obj, code, { hideSource: true }); this._fillSource(box, videoEl, `https://fms.pacopacomama.com/hls/sample/pacopacomama.com/${code}/1080p.mp4`, code); } addVideoN (code, obj) { const urls = [`https://my.cdn.tokyo-hot.com/media/samples/${code}.mp4`, `https://my.cdn.tokyo-hot.com/media/samples/${code.toLowerCase()}.mp4`]; if (/RED048/i.test(code)) urls.unshift('https://my.cdn.tokyo-hot.com/media/samples/5923.mp4'); if (/RED065/i.test(code)) urls.unshift('https://my.cdn.tokyo-hot.com/media/samples/5924.mp4'); const { box, videoEl } = this._injectPlaceholder(obj, code, { hideSource: true }); this._fillSources(box, videoEl, urls, code); } async _fillSources (box, videoEl, urls, code) { let picked = null; const probing = box.querySelector('.trailer-quality-probing'); if (probing) probing.style.display = ''; for (const u of urls) { const r = await probeQuick(u); if (r.ok) { picked = u; break; } } if (probing) probing.style.display = 'none'; if (!picked) picked = urls[0]; return this._fillSource(box, videoEl, picked, code); } addVideolegsjapan (code, obj) { this.addVideo(code, obj); } addVideoVR (code, obj) { this.addVideo(code, obj); } addVideoMGS (code, obj) { $(obj).before(`<div class="trailer-box ready"><iframe src="https://www.mgstage.com/api/affiliate_sample_movie.php?p=${code}&w=1060&h=630" style="width:100%;height:78vh;min-height:500px;border:none;"></iframe></div>`); } addVideoMSIN (code, obj) { const parts = code.split(/-/); const series = parts[0].toLowerCase(); const num = parts[1]; const num5 = num ? String(num).padStart(5, '0') : ''; $(obj).before(`<div class="trailer-box ready"><iframe src="https://db.msin.jp/.play/sample.fanza?id=${series}${num5}" style="width:100%;height:78vh;min-height:500px;border:none;"></iframe></div>`); } } function buildJumpLinks (code, videoSeries, videoNo) { return ` <a class='red' href='https://javtrailers.com/ja/search/${code}' target='_blank'>trai</a> <a class='red' href='http://www.javlibrary.com/cn/vl_searchbyid.php?keyword=${code}' target='_blank'>lib</a> <a class='red' href='https://www.javbus.com/${code}' target='_blank'>bus</a> <a class='red' href='https://javdb.com/search?q=${code.replace('-', '_')}' target='_blank'>db</a> <a class='red' href='https://javspyl.eu.org/${code}' target='_blank'>spyl</a> <a class='red' href='https://www.sehuatang.net/search.php?mod=forum&srchtype=title&srchtxt=${code}&searchsubmit=true' target='_blank'>98</a> <a class='red' href='https://btsow.motorcycles/search/${code}' target='_blank'>btsow</a> <a class='red' href='https://xslist.org/search?query=${code}&lg=tw' target='_blank'>xslist</a> <a class='red' href='https://db.msin.jp/jp.search/movie?str=${code}' target='_blank'>msin</a> <a class='red' href='https://www.dmm.co.jp/digital/videoa/-/detail/=/cid=${videoSeries}${videoNo}/' target='_blank'>dmm<sup>JP代</sup></a> <a class='red' href='https://www.mgstage.com/search/cSearch.php?search_word=${code}' target='_blank'>mgstage<sup>SG代</sup></a> <a class='red' href='https://missav.com/search/${code}' target='_blank'>missav</a>`; } function embyQuery (code, insertAfter) { if (!embyAPI || !embyBaseUrl) return; GM_xmlhttpRequest({ method: "GET", url: embyBaseUrl + "emby/Users/" + embyAPI + "/Items?api_key=" + embyAPI + "&Recursive=true&IncludeItemTypes=Movie&SearchTerm=" + code, headers: { accept: "application/json" }, onload: (res) => { try { const rr = JSON.parse(res.responseText); for (let i = 0; i < rr.Items.length; i++) { const url = embyBaseUrl + "web/index.html#!/item?id=" + rr.Items[i].Id + "&serverId=" + rr.Items[i].ServerId; $(insertAfter).after('<div style="border:3px solid HotPink;padding:20px;"><a href="' + url + '" target="_blank"><b><font size=6> 跳转到emby👉</font></b></a></div>'); } } catch (e) {} } }); } function renderTrailer (ctx, code, yulan, title) { if (/^[01]\d{5}[-_](?:1)?\d{2,3}$/i.test(code)) return ctx.addVideoC(code, yulan); if (/^[01]\d{5}_\d{3}$/.test(code)) return ctx.addVideoY(code, yulan); if (/^[01]\d{5}_0[12]$/.test(code)) return ctx.addVideoH(code, yulan); if (/^[01]\d{5}_\d{3}$/.test(code)) return ctx.addVideoPM(code, yulan); if (/legsjapan/i.test(code)) return ctx.addVideolegsjapan(code, yulan); if (/VR-/i.test(code) || /【VR/i.test(title)) return ctx.addVideoVR(code, yulan); if (/FC2-|FC2PPV-/i.test(code)) { const n = code.replace(/FC2(-PPV)?-/, ''); $(yulan).before(`<div class="trailer-box ready"><iframe src="https://contents.fc2.com/embed/${n}" style="width:100%;height:78vh;min-height:500px;border:none;"></iframe></div>`); return; } if (/^[a-zA-Z]{1,16}\d{4}$|^\d{5}$|^(RED-|NKD-|RHJ-)\d{3}$/i.test(code)) return ctx.addVideoN(code, yulan); ctx.addVideo(code, yulan); } class JavBus extends Base { constructor () { super(); if ($('.col-md-3.info').length > 0) this.detailPage(); } detailPage () { const info = $('.col-md-3.info'); const yulan = $('.row.movie'); const title = $('.container > h3').text(); const code = info.find('p').eq(0).find('span').eq(1).html(); const parts = code.split(/-/); const videoSeries = parts[0].toLowerCase(); const videoNo = parts[1] ? String(parts[1]).padStart(5, '0') : ''; document.querySelectorAll('h3').forEach(h => { h.insertAdjacentHTML('beforeend', ` <b><font color=blue>全片:</font></b> <a href="https://missav.com/search/${code}" target="_blank">missav</a> <a href="https://thisav.com/cn/${code}" target="_blank">thisav</a>`); }); renderTrailer(this, code, yulan, title); info.append("<p>" + buildJumpLinks(code, videoSeries, videoNo) + "</p>"); embyQuery(code, ".star-show"); } } class JavLibrary extends Base { constructor () { super(); if ($('#video_info').length > 0) this.detailPage(); } detailPage () { const info = $('#video_info'); const yulan = $('#video_jacket_info'); const title = $('.post-title').text(); const code = info.find('.item').eq(0).find('.text').html(); const parts = code.split(/-/); const videoSeries = parts[0].toLowerCase(); const videoNo = parts[1] ? String(parts[1]).padStart(5, '0') : ''; document.querySelectorAll('h3').forEach(h => { h.insertAdjacentHTML('beforeend', ` <b><font color=blue>全片:</font></b> <a href="https://missav.com/search/${code}" target="_blank">missav</a> <a href="https://thisav.com/cn/${code}" target="_blank">thisav</a>`); }); renderTrailer(this, code, yulan, title); info.find('a').attr('target', '_blank'); info.append("<div class='item'><table><tbody><tr><td class='header'>" + buildJumpLinks(code, videoSeries, videoNo) + "</td></tr></tbody></table></div>"); embyQuery(code, "#video_info"); } } class Javdb extends Base { constructor () { super(); if ($('.video-meta-panel').length > 0) this.detailPage(); } detailPage () { const info = $('.panel.movie-panel-info'); const yulan = $('.video-meta-panel'); const changyulan = $('#modal-comment-warning'); const title = $('.title.is-4').text().trim(); const code = $('body > section > div > div.video-detail > h2 > strong') .text().trim().replace("10musu_", "").replace("ALOVE", "LOVE").replace("AAQUA", "AQUA").replace("AMCMA", "MCMA").split(' ')[0]; const parts = code.split(/-/); const videoSeries = parts[0].toLowerCase(); const videoNo = parts[1] ? String(parts[1]).padStart(5, '0') : ''; this.addVideo(code, yulan); document.querySelectorAll('.current-title').forEach(h => { h.insertAdjacentHTML('beforeend', ` <b><font color=blue>全片:</font></b> <a href="https://missav.com/search/${code}" target="_blank">missav</a> <a href="https://thisav.com/cn/${code}" target="_blank">thisav</a>`); }); info.append("<div class='item'><table><tbody><tr><td class='header'>" + buildJumpLinks(code, videoSeries, videoNo) + "</td></tr></tbody></table></div>"); embyQuery(code, ".panel.movie-panel-info"); if (changyulan.length) { changyulan.before(`<div class='columns'><div class='column'><article class='message video-panel'><div class='message-header'><p>长缩略图 ${code} 在<a href='https://img.javstore.net/search/images/?q=%22${code}%22' target='_blank'>javstore</a>搜索</p></div><div class='message-body'><div class="trailer-box" style="border:1px solid #ccc;"><a href='https://image.memojav.com/image/screenshot/${code}.jpg' target='_blank'><img src='https://image.memojav.com/image/screenshot/${code}.jpg' style='max-width:100%;'></a></div></div></article></div></div>`); } } } class Javbooks extends Base { constructor () { super(); if ($('#info').length > 0) this.detailPage(); } detailPage () { const info = $('#info'); const yulan = $('#info'); $('#Preview_vedio_area > a > img').remove(); $('body > p > a > img').remove(); const title = $('#title').text().trim(); const code = $('#info > div:nth-child(2) > font').text().trim().replace("10musu_", "").split(' ')[0]; const parts = code.split(/-/); const videoSeries = parts[0].toLowerCase(); const videoNo = parts[1] ? String(parts[1]).padStart(5, '0') : ''; document.querySelectorAll('#title').forEach(h => { h.insertAdjacentHTML('beforeend', ` <b><font color=blue>全片:</font></b> <a href="https://missav.com/search/${code}" target="_blank">missav</a> <a href="https://thisav.com/cn/${code}" target="_blank">thisav</a>`); }); renderTrailer(this, code, yulan, title); info.append("<div class='infobox'><b>跳转:</b>" + buildJumpLinks(code, videoSeries, videoNo) + "</div>"); embyQuery(code, "#info"); } } class Avmoo extends Base { constructor () { super(); if ($('.col-md-3.info').length > 0) this.detailPage(); } detailPage () { const info = $('.col-md-3.info'); const yulan = $('.row.movie'); const title = $('.container > h3').text(); const code = info.find('p').eq(0).find('span').eq(1).html(); const parts = code.split(/-/); const videoSeries = parts[0].toLowerCase(); const videoNo = parts[1] ? String(parts[1]).padStart(5, '0') : ''; document.querySelectorAll('h3').forEach(h => { h.insertAdjacentHTML('beforeend', ` <b><font color=blue>全片:</font></b> <a href="https://missav.com/search/${code}" target="_blank">missav</a> <a href="https://thisav.com/cn/${code}" target="_blank">thisav</a>`); }); renderTrailer(this, code, yulan, title); info.append("<p>" + buildJumpLinks(code, videoSeries, videoNo) + "</p>"); embyQuery(code, ".col-md-3.info"); } } class Sehuatang extends Base { constructor () { super(); if ($('#pgt').length > 0) this.detailPage(); } detailPage () { const yulan = $('#pgt'); const reg = /([a-zA-Z]{2,15}[-\s]?\d{2,15}|FC2PPV-[^\d]{0,5}\d{6,7})/i; const str = document.title.split(" ")[0].split(" ")[0].split("【")[0].split("[")[0].split("-carib")[0].split("-10mu-")[0].split("-paco-")[0].split("-1pon-")[0].replace("SSSIS-", "SSIS-").replace("BBOBB-", "BOBB-").replace("SET-628", "FSET-628"); const m = str.match(reg); if (!m) return; const code = m[0]; const title = document.title; const parts = code.split(/-/); const videoSeries = parts[0].toLowerCase(); const videoNo = parts[1] ? String(parts[1]).padStart(5, '0') : ''; if (!/(高清中文字幕|亚洲有码原创|亚洲无码原创|4K原版|素人有码系列|PPV)/i.test(title)) return; document.querySelectorAll('#thread_subject').forEach(h => { h.insertAdjacentHTML('beforeend', ` <b><font color=blue>全片:</font></b> <a href="https://missav.com/search/${code}" target="_blank">missav</a> <a href="https://thisav.com/cn/${code}" target="_blank">thisav</a>`); }); yulan.before(`<div id='tiaozhuan'>${buildJumpLinks(code, videoSeries, videoNo)}</div>`); renderTrailer(this, code, yulan, title); embyQuery(code, "#pgt"); } } class Msin extends Base { constructor () { super(); this.detailPage(); } detailPage () { const info = $('#top_content'); const yulan = $('#breadcrumb'); let code; if (/db\.msin\.jp\/jp\.page\/movie/.test(location.href)) code = $('div.mv_pn').text().trim().split(' ')[0]; else code = $('div.mv_fileName').text().trim().split(' ')[0].replace("fc2-ppv-", "fc2-"); const parts = code.split(/-/); const videoSeries = parts[0].toLowerCase(); const videoNo = parts[1] ? String(parts[1]).padStart(5, '0') : ''; const title = document.title; document.querySelectorAll('.mv_title').forEach(h => { h.insertAdjacentHTML('beforeend', ` <b><font color=blue>全片:</font></b> <a href="https://missav.com/search/${code}" target="_blank">missav</a> <a href="https://thisav.com/cn/${code}" target="_blank">thisav</a>`); }); info.append("<div id='tiaozhuan'>" + buildJumpLinks(code, videoSeries, videoNo) + "</div>"); renderTrailer(this, code, yulan, title); embyQuery(code, "#top_content"); } } function handleMakerDetail () { const info = $('body > main > section:nth-child(3) > div > p'); let code = location.pathname.slice(location.pathname.lastIndexOf('/') + 1).toUpperCase(); if (/^[a-z|A-Z]{2,8}\d{2,5}$/i.test(code)) { const n = code.search(/\d/); if (n > 0) code = code.slice(0, n) + "-" + code.slice(n); } const parts = code.split(/-/); const videoSeries = parts[0].toLowerCase(); const videoNo = parts[1] ? String(parts[1]).padStart(5, '0') : ''; document.querySelectorAll('.p-workPage__title').forEach(h => { h.insertAdjacentHTML('beforeend', ` <b><font color=blue>全片:</font></b> <a href="https://missav.com/search/${code}" target="_blank">missav</a> <a href="https://thisav.com/cn/${code}" target="_blank">thisav</a>`); }); info.append("<div id='tiaozhuan'>" + buildJumpLinks(code, videoSeries, videoNo) + "</div>"); const ctx = new Base(); renderTrailer(ctx, code, info, document.title); $('.p-workTable').append(`<div class='item'><div class='th'>识别码</div><div class='td'>${code}</div></div>`); embyQuery(code, ".p-workPage__text"); } function autoJumpHandler () { if (/javdb/i.test(location.hostname)) { const a = document.querySelectorAll('.item a[href*="/v/"]'); const b = document.querySelectorAll('.box.actor-box a[href*="/actors/"]'); if (a.length === 1) { location.href = a[0].href; return; } if (b.length === 1) { location.href = b[0].href; return; } } if (/\/search\//.test(location.href)) { const boxes = $('.movie-box'); if (boxes.length === 1) { location.href = boxes[0].href; return; } } if (/xslist\.org\/search/.test(location.href)) { const rs = document.querySelectorAll('.clearfix'); if (rs.length === 1) { const a = rs[0].querySelector('a'); if (a) a.click(); } } } class Main { constructor () { if ($("footer:contains('JavBus')").length) this.site = 'javBus'; else if ($("#bottomcopyright:contains('JAVLibrary')").length) this.site = 'javLibrary'; else if (/javdb/i.test(location.hostname)) this.site = 'javdb'; else if ($("#footer:contains('javdb')").length) this.site = 'javdb'; else if ($("#Declare_box:contains('javbooks')").length) this.site = 'javbooks'; else if ($("footer:contains('AVMOO')").length) this.site = 'avmoo'; else if ($("#flk:contains('色花堂')").length) this.site = 'sehuatang'; else if ($("#footer:contains('db.msin.jp')").length) this.site = 'msin'; } make () { switch (this.site) { case 'javBus': new JavBus(); break; case 'javLibrary': new JavLibrary(); break; case 'javdb': new Javdb(); break; case 'javbooks': new Javbooks(); break; case 'avmoo': new Avmoo(); break; case 'sehuatang': new Sehuatang(); break; case 'msin': new Msin(); break; } } } try { console.log('[Trailer] start, site detection...'); autoJumpHandler(); if (/\/works\/detail/i.test(location.pathname)) handleMakerDetail(); else new Main().make(); console.log('[Trailer] done.'); } catch (e) { console.error('[Trailer] init error:', e); } })();