dmm预告片
// ==UserScript==
// @name JavBus Javdb library trailer (Dynamic)
// @name:zh-CN JavBus/Javdb 预告片
// @namespace https://greasyfork.org/zh-CN/scripts/441120
// @version 2026.09.20
// @description dmm预告片
// @description:zh-cn dmm预告片
// @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
// @connect *
// @connect dmm.co.jp
// @connect cc3001.dmm.co.jp
// @connect api.video.dmm.co.jp
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
var embyAPI = "";
var embyBaseUrl = "";
GM_addStyle(`
.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 {
width: 100% !important; max-width: 100% !important; height: auto !important;
aspect-ratio: 16 / 9; max-height: 80vh; min-height: 320px;
display: block !important; background: #000;
}
.trailer-toolbar {
display: flex; align-items: center; justify-content: space-between;
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-box.ready .trailer-status { display: none; }
.trailer-box:not(.ready) .trailer-quality-label { display: none !important; }
.trailer-quality-label { display: flex; align-items: center; gap: 4px; }
.trailer-quality {
background: #333; color: #ddd; border: 1px solid #555;
border-radius: 3px; padding: 3px 6px;
font-size: 12px; cursor: pointer;
}
.trailer-video { display: none; }
.trailer-box.ready .trailer-video { display: block !important; }
.trailer-box.error .trailer-toolbar { background: #4a1010; color: #f88; }
.trailer-quality-probing { font-size: 12px; color: #888; margin-left: 6px; }
.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] script loaded at', location.href);
const CACHE_PREFIX = 'jvl_trailer_v21_';
const CACHE_TTL = 5 * 24 * 60 * 60 * 1000;
// ==================== 音量记忆 ====================
function getSavedVolume () {
try {
const v = GM_getValue('jvl_trailer_volume', null);
if (v === null || v === undefined) 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 || m === undefined) return true;
return String(m) === 'true';
} catch (e) { return true; }
}
function saveVolumeState (videoEl) {
try {
GM_setValue('jvl_trailer_volume', String(videoEl.volume));
GM_setValue('jvl_trailer_muted', String(videoEl.muted));
} catch (e) {}
}
function applyVolumeState (videoEl) {
try {
videoEl.volume = getSavedVolume();
videoEl.muted = getSavedMuted();
let timer = null;
videoEl.addEventListener('volumechange', () => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => saveVolumeState(videoEl), 300);
});
} 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) && SUPPORTED_HEIGHTS.includes(n)) return String(n);
} catch (e) {}
return 'auto-high';
}
function setPreferredQuality (q) {
try { GM_setValue(QUALITY_KEY, String(q)); } catch (e) {}
}
// ==================== 下拉框选项 ====================
function buildQualityOptionsHtml () {
return `
<option value="auto-high">默认最高</option>
<option value="auto-low">默认最低</option>
<option value="1080">1080p</option>
<option value="720">720p</option>
<option value="576">576p</option>
<option value="432">432p</option>
<option value="288">288p</option>
<option value="144">144p</option>`;
}
// ==================== mp4 候选 URL 生成 ====================
// DMM 后缀 → 分辨率映射(根据实测样例):
// hhb → 1080 | hmb → 720 | mhb → 576
// mmb → 432 | dm → 288 | sm → 144
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' }
];
function buildCandidates (rawUrl) {
if (!rawUrl) return [];
const url = String(rawUrl).replace(/\?.*$/, '');
// 模式 1:xxx/1080p.mp4 (caribbean / 1pondo / 10musume / pacopacomama 等)
let m = url.match(/^(.+?)\/(\d{3,4})p\.mp4$/i);
if (m) {
const base = m[1];
return SUPPORTED_HEIGHTS.map(h => ({ height: h, url: `${base}/${h}p.mp4` }));
}
// 模式 2:xxx_{sfx}_{w}.mp4 (DMM litevideo freepv)
m = url.match(/^(.+?)_(hhb|hmb|mhb|mmb|dm|sm)_([ws])\.mp4$/i);
if (m) {
const base = m[1] + '_';
const w = m[3];
return DMM_SUFFIX_ORDER.map(e => ({ height: e.height, url: `${base}${e.sfx}_${w}.mp4` }));
}
// 模式 3:xxx_{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` }));
}
// 模式 4:xxx{sfx}.mp4 —— DMM pv 主模式(id 直接紧跟后缀)
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, timeout: true }); } }, 2500);
});
}
// ==================== 按偏好选择 mp4 ====================
async function pickForPref (originalUrl, pref) {
const candidates = buildCandidates(originalUrl);
if (candidates.length === 1 && candidates[0].height === 0) {
return candidates[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) || !SUPPORTED_HEIGHTS.includes(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 matches = candidates.filter(c => c.height === h);
if (!matches.length) continue;
for (const c of matches) {
const r = await probeQuick(c.url);
if (r.ok) {
const tag = (String(h) === String(pref)) ? 'exact' : 'fallback';
console.log('[Trailer] picked', h + 'p', '(' + tag + ', pref=' + pref + ')', c.url);
return { height: h, url: c.url };
}
}
}
console.log('[Trailer] pickForPref: all probes failed, fallback to original');
return { height: 0, url: originalUrl };
}
// ==================== 缓存 ====================
function getCached (code) {
try {
const key = CACHE_PREFIX + 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 setCache (code, url) {
try {
const key = CACHE_PREFIX + code.toUpperCase();
GM_setValue(key, JSON.stringify({ url, expire: Date.now() + CACHE_TTL }));
} catch (e) {}
}
// ==================== JavDB 原生 src ====================
function getNativePreviewSrc () {
try {
const nv = document.querySelector('#preview-video');
if (!nv) return '';
let src =
nv.querySelector('source')?.getAttribute('src') ||
nv.getAttribute('src') ||
nv.getAttribute('data-src') || '';
src = String(src).trim();
if (!src || src === 'about:blank') {
const wrap = nv.closest('.preview-video-wrapper, .video-meta-panel, .video-detail');
if (wrap) {
const innerSrc = wrap.querySelector('video source')?.getAttribute('src') ||
wrap.querySelector('video')?.getAttribute('src') || '';
if (innerSrc) src = innerSrc;
}
}
return src.startsWith('http') ? src : '';
} catch (e) { return ''; }
}
// ==================== 从 HTML 提取 PV (只保留 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 '';
}
// ==================== 提取 CID ====================
function extractCidsFromHtml (html) {
if (!html) return [];
const decoded = html.replace(/\\\//g, '/');
const cids = new Set();
let m;
const re1 = /[?&]cid=([a-z0-9_]+)/gi;
while ((m = re1.exec(decoded)) !== null) cids.add(m[1].toLowerCase());
const re2 = /data-cid="([a-z0-9_]+)"/gi;
while ((m = re2.exec(decoded)) !== null) cids.add(m[1].toLowerCase());
const re3 = /"cid"\s*:\s*"([a-z0-9_]+)"/gi;
while ((m = re3.exec(decoded)) !== null) cids.add(m[1].toLowerCase());
const re4 = /pics\.dmm\.co\.jp\/(?:digital|mono|rental)\/(?:video|videoa|movie|adult)\/(?:adult\/)?([a-z0-9_]+)\//gi;
while ((m = re4.exec(decoded)) !== null) cids.add(m[1].toLowerCase());
const re5 = /\/cid\/([a-z0-9_]+)\//gi;
while ((m = re5.exec(decoded)) !== null) cids.add(m[1].toLowerCase());
return Array.from(cids);
}
function filterMatchingCids (cids, series, num) {
const s = series.toLowerCase();
const n = String(num);
const n3 = n.padStart(3, '0');
const n5 = n.padStart(5, '0');
const stripped = String(parseInt(n, 10));
const matches = [];
for (const cid of cids) {
if (cid.includes(s) && (cid.includes(n) || cid.includes(n3) || cid.includes(n5) || cid.includes(stripped))) {
matches.push(cid);
}
}
matches.sort((a, b) => {
const score = (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 score(a) - score(b);
});
return matches;
}
function fetchCidsFromDmmSearch (code) {
return new Promise((resolve) => {
const parts = code.split(/-/);
if (parts.length < 2) return resolve([]);
const series = parts[0].toLowerCase();
const num = parts[1];
const searchWords = [`${series}${num}`, `${series}-${num}`, `${series}${num.padStart(5, '0')}`];
const searchUrls = [];
for (const kw of searchWords) {
const enc = encodeURIComponent(kw);
searchUrls.push(`https://www.dmm.co.jp/search/=/searchstr=${enc}/`);
searchUrls.push(`https://www.dmm.co.jp/digital/videoa/-/search/=/searchstr=${enc}/`);
searchUrls.push(`https://www.dmm.co.jp/mono/dvd/-/search/=/searchstr=${enc}/`);
}
let resolved = false;
let pending = searchUrls.length;
const allCids = new Set();
const finish = () => {
if (resolved) return;
resolved = true;
const filtered = filterMatchingCids(Array.from(allCids), series, num);
console.log('[Trailer] DMM search → cids(raw/filtered):', allCids.size, '/', filtered.length, filtered.slice(0, 5));
resolve(filtered);
};
searchUrls.forEach((url) => {
GM_xmlhttpRequest({
url, method: 'GET', timeout: 6000,
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/'
},
onload: (res) => {
if (resolved) return;
const cids = extractCidsFromHtml(res.responseText || '');
cids.forEach(c => allCids.add(c));
pending--;
const filtered = filterMatchingCids(Array.from(allCids), series, num);
if (filtered.length > 0) setTimeout(finish, 200);
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);
const stripped = String(parseInt(n, 10));
const n3 = n.padStart(3, '0');
const 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;
}
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 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 tryNext = () => {
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/'
},
onload: (res) => {
const pv = extractPvFromHtml(res.responseText || '');
if (pv) return resolve(pv);
tryNext();
},
onerror: () => tryNext(),
ontimeout: () => tryNext()
});
};
tryNext();
});
}
function fetchFanzaTrailerUrl (code) {
return new Promise((resolve) => {
const parts = code.split(/-/);
if (parts.length < 2) return resolve(null);
const series = parts[0].toLowerCase();
const num = parts[1];
const searchWords = [`${parts[0]}-${num}`, `${series}${num}`, `${series}${num.padStart(5, '0')}`];
let idx = 0;
const tryNext = () => {
if (idx >= searchWords.length) return resolve(null);
const sw = searchWords[idx++];
const query = `{ legacySearchPPV(limit: 10, searchWord: "${sw}") { items { cid title sampleUrl sampleMovieUrl } } }`;
console.log('[Trailer] FANZA search:', sw);
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 }),
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
const items = 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 filtered = filterMatchingCids(cids, series, num);
const target = filtered.length ? filtered : cids;
let pending = target.length;
target.forEach(cid => {
fetchPvFromCid(cid).then(pv => {
if (pv) return resolve(pv);
pending--;
if (pending === 0) tryNext();
});
});
return;
}
tryNext();
} catch (e) { tryNext(); }
},
onerror: () => tryNext(),
ontimeout: () => tryNext()
});
};
tryNext();
});
}
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 resolved = false;
const finish = (url, from) => {
if (resolved || !url) return;
resolved = true;
console.log('[Trailer] ✓ DMM resolved via', from, ':', url);
resolve(url);
};
const guessUrls = buildDmmUrlList(series, parts[1]);
const CONCURRENT = 4;
let nextIdx = 0;
const pickAndRun = () => {
if (resolved) return;
if (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/'
},
onload: (res) => {
if (resolved) return;
const pv = extractPvFromHtml(res.responseText || '');
if (pv) return finish(pv, 'guess');
pickAndRun();
},
onerror: () => { if (!resolved) pickAndRun(); },
ontimeout: () => { if (!resolved) pickAndRun(); }
});
};
for (let i = 0; i < Math.min(CONCURRENT, guessUrls.length); i++) pickAndRun();
setTimeout(() => {
if (resolved) return;
fetchCidsFromDmmSearch(code).then((cids) => {
if (resolved) return;
if (!cids.length) { console.log('[Trailer] DMM search: no matching cids'); return; }
const MAX = Math.min(5, cids.length);
for (let i = 0; i < MAX; i++) {
if (resolved) return;
fetchPvFromCid(cids[i]).then((pv) => { if (pv) finish(pv, 'dmm-search'); });
}
});
}, 100);
setTimeout(() => {
if (resolved) return;
fetchFanzaTrailerUrl(code).then((url) => { if (url) finish(url, 'fanza-api'); });
}, 500);
setTimeout(() => {
if (!resolved) { resolved = true; console.log('[Trailer] DMM all paths timeout'); 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, '-') : '';
const allTxt = (a.textContent || '').toUpperCase().replace(/_/g, '-');
if (uidText === norm || allTxt.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 patterns = [
/<video[^>]*id="preview-video"[^>]*src="([^"]+\.mp4[^"]*)"/i,
/<source[^>]*src="([^"]+\.mp4[^"]*)"/i,
];
for (const p of patterns) {
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)
});
});
}
class Request {
constructor () { this.lock = []; }
send (url, cb) {
let _this = this;
return new Promise((resolve, reject) => {
const idx = _this.lock.indexOf(url);
if (idx !== -1) return reject('发送请求ing');
_this.lock.push(url);
GM_xmlhttpRequest({
url, method: 'GET',
headers: { "Cache-Control": "no-cache" },
timeout: 30000,
onload: (r) => { _this.lock.splice(idx, 1); resolve(r); },
onabort: () => reject('wrong'),
onerror: () => reject('wrong'),
ontimeout: () => reject('wrong')
});
}).then(cb, (e) => console.log(e));
}
}
// ==================== Base 类 ====================
class Base {
constructor () { this._lastVideo = null; }
_injectPlaceholder (obj, code) {
const html = `
<div class="trailer-box" data-code="${code || ''}">
<div class="trailer-toolbar">
<span class="trailer-status">加载预告片中…</span>
<label class="trailer-quality-label" style="display:none;">
清晰度:
<select class="trailer-quality">
${buildQualityOptionsHtml()}
</select>
<span class="trailer-quality-probing" style="display:none;">探测中…</span>
</label>
</div>
<video class="trailer-video" controls playsinline muted loop preload="metadata"></video>
</div>`;
$(obj).before(html);
const boxes = document.querySelectorAll('.trailer-box');
const box = boxes[boxes.length - 1];
const videoEl = box.querySelector('video.trailer-video');
applyVolumeState(videoEl);
this._lastVideo = videoEl;
return { box, videoEl };
}
async _fillSource (box, videoEl, originalUrl) {
if (!box || !videoEl || !originalUrl) return;
console.log('[Trailer] Original source:', originalUrl);
box._originalUrl = originalUrl;
const label = box.querySelector('.trailer-quality-label');
const probing = box.querySelector('.trailer-quality-probing');
if (probing) probing.style.display = '';
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 (probing) probing.style.display = 'none';
if (label) label.style.display = '';
const useUrl = picked.url;
videoEl.querySelectorAll('source').forEach(s => s.remove());
const s = document.createElement('source');
s.src = useUrl;
s.type = 'video/mp4';
videoEl.appendChild(s);
box.classList.add('ready');
try { videoEl.load(); } catch (e) {}
videoEl.play().catch(() => {});
videoEl.addEventListener('playing', () => {
setCache((box.getAttribute('data-code') || '').toUpperCase(), originalUrl);
}, { once: true });
this._setupQualitySelect(box, videoEl, originalUrl);
}
_setupQualitySelect (box, videoEl, originalUrl) {
const select = box.querySelector('.trailer-quality');
const label = box.querySelector('.trailer-quality-label');
if (!select) return;
if (!box._qualityBound) {
box._qualityBound = true;
select.addEventListener('change', async () => {
const q = select.value;
setPreferredQuality(q);
const orig = box._originalUrl;
if (!orig) return;
const probing = box.querySelector('.trailer-quality-probing');
if (probing) probing.style.display = '';
const target = await pickForPref(orig, q);
if (probing) probing.style.display = 'none';
if (!target) return;
this._switchSource(videoEl, target.url);
});
}
const pref = getPreferredQuality();
if (Array.from(select.options).some(o => o.value === pref)) {
select.value = pref;
} else {
select.value = 'auto-high';
}
if (label) label.style.display = '';
}
_switchSource (videoEl, url) {
if (!videoEl || !url) return;
const t = videoEl.currentTime || 0;
const wasPlaying = !videoEl.paused;
videoEl.querySelectorAll('source').forEach(s => s.remove());
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);
}
async _fillSources (box, videoEl, urls) {
if (!box || !videoEl || !urls || !urls.length) return;
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);
}
_fillError (box, msg) {
if (!box) return;
box.classList.add('error');
const status = box.querySelector('.trailer-status');
if (status) status.textContent = msg || '未找到预告片';
}
_resolveAndFill (code, box, videoEl) {
const codeUpper = code.toUpperCase();
let resolved = false;
const tryResolve = (url, source) => {
if (resolved || !url) return false;
resolved = true;
console.log('[Trailer] ✓ Resolved via', source);
setCache(codeUpper, url);
this._fillSource(box, videoEl, url);
return true;
};
fetchDmmTrailerUrlParallel(code).then((url) => {
if (tryResolve(url, 'DMM')) return;
console.log('[Trailer] DMM failed, trying JavDB...');
fetchJavdbTrailerUrl(code).then((jdbUrl) => {
if (tryResolve(jdbUrl, 'JavDB')) return;
this._fillError(box, '未找到预告片');
});
});
}
addVideo (code, obj) {
const codeUpper = code.toUpperCase();
if (/^HEYZO-/i.test(code)) {
const n = code.split(/-/)[1];
const { box, videoEl } = this._injectPlaceholder(obj, code);
return this._fillSource(box, videoEl, `https://sample.heyzo.com/contents/3000/${n}/sample.mp4`);
}
if (/^HEYZ-/i.test(code)) {
const n = code.split(/-/)[1];
const { box, videoEl } = this._injectPlaceholder(obj, code);
return this._fillSource(box, videoEl, `https://www.heyzo.com/contents/3000/${n}/heyzo_hd_${n}_sample.mp4`);
}
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);
const cached = getCached(codeUpper);
if (cached) {
console.log('[Trailer] Cache hit:', cached);
return this._fillSource(box, videoEl, cached);
}
this._resolveAndFill(code, box, videoEl);
}
addVideoC (code, obj) {
const { box, videoEl } = this._injectPlaceholder(obj, code);
this._fillSource(box, videoEl, `https://smovie.caribbeancom.com/sample/movies/${code}/1080p.mp4`);
}
addVideoY (code, obj) {
const { box, videoEl } = this._injectPlaceholder(obj, code);
this._fillSource(box, videoEl, `https://smovie.1pondo.tv/sample/movies/${code}/1080p.mp4`);
}
addVideoH (code, obj) {
const { box, videoEl } = this._injectPlaceholder(obj, code);
this._fillSource(box, videoEl, `https://smovie.10musume.com/sample/movies/${code}/1080p.mp4`);
}
addVideoPM (code, obj) {
const { box, videoEl } = this._injectPlaceholder(obj, code);
this._fillSource(box, videoEl, `https://fms.pacopacomama.com/hls/sample/pacopacomama.com/${code}/1080p.mp4`);
}
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);
this._fillSources(box, videoEl, urls);
}
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>`);
}
}
// ==================== Emby 查询(需自行填 embyAPI / embyBaseUrl)====================
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 (req) { super(req); 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();
renderTrailer(this, code, yulan, title);
embyQuery(code, ".star-show");
}
}
class JavLibrary extends Base {
constructor (req) { super(req); 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();
renderTrailer(this, code, yulan, title);
info.find('a').attr('target', '_blank');
embyQuery(code, "#video_info");
}
}
class Javdb extends Base {
constructor (req) { super(req); 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();
let 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 codeUpper = code.toUpperCase();
const self = this;
const { box, videoEl } = this._injectPlaceholder(yulan, code);
let resolved = false;
const tryResolve = (url, source) => {
if (resolved || !url) return false;
resolved = true;
console.log('[Trailer] ✓ JavDB resolved via', source, ':', url);
setCache(codeUpper, url);
self._fillSource(box, videoEl, url);
return true;
};
const cached = getCached(codeUpper);
if (cached) return tryResolve(cached, 'cache');
const nativeSrc = getNativePreviewSrc();
if (nativeSrc) return tryResolve(nativeSrc, 'native-immediate');
let attempts = 0;
const pollNative = () => {
if (resolved) return;
const s = getNativePreviewSrc();
if (s) return tryResolve(s, 'native-poll');
attempts++;
if (attempts < 4) setTimeout(pollNative, 300);
};
setTimeout(pollNative, 300);
fetchDmmTrailerUrlParallel(code).then((url) => {
if (tryResolve(url, 'DMM')) return;
fetchJavdbTrailerUrl(code).then((jdbUrl) => {
if (tryResolve(jdbUrl, 'JavDB-fetch')) return;
self._fillError(box, '未找到预告片');
});
});
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 (req) { super(req); 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];
renderTrailer(this, code, yulan, title);
embyQuery(code, "#info");
}
}
class Avmoo extends Base {
constructor (req) { super(req); 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();
renderTrailer(this, code, yulan, title);
embyQuery(code, ".col-md-3.info");
}
}
class Sehuatang extends Base {
constructor (req) { super(req); 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;
if (!/(高清中文字幕|亚洲有码原创|亚洲无码原创|4K原版|素人有码系列|PPV)/i.test(title)) return;
renderTrailer(this, code, yulan, title);
embyQuery(code, "#pgt");
}
}
class Msin extends Base {
constructor (req) { super(req); 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 title = document.title;
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 ctx = new Base();
renderTrailer(ctx, code, info, document.title);
$('.p-workPage__table').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 () {
const req = new Request();
switch (this.site) {
case 'javBus': new JavBus(req); break;
case 'javLibrary': new JavLibrary(req); break;
case 'javdb': new Javdb(req); break;
case 'javbooks': new Javbooks(req); break;
case 'avmoo': new Avmoo(req); break;
case 'sehuatang': new Sehuatang(req); break;
case 'msin': new Msin(req); 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);
}
})();