JavBus Javdb library trailer (Dynamic)

dmm高清预告片

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         JavBus Javdb library trailer (Dynamic)
// @name:zh-CN   JavBus/Javdb/library/javbooks/avmoo/avsox/sehuatang/msin 图书馆预告片 mp4版
// @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(`
    .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 {
      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'  }
  ];

  // 返回 [{height, url}],按 SUPPORTED_HEIGHTS 顺序,或 [{height:0,url}] 表示未知
  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);

    // 未知 URL(无分辨率后缀)→ 直接使用
    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 };
        }
      }
    }

    // 所有档位探测都失败 → 退回原始 URL
    console.log('[Trailer] pickForPref: all probes failed, fallback to original');
    return { height: 0, url: originalUrl };
  }

  // ==================== 缓存 ====================
  // 缓存原始 URL(不是解析后的 URL),这样改偏好时不用重新抓页面
  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) ====================
  // 优先返回最高清 (hhb) 作为基准 URL
  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 };
    }

    // 主入口:给定原始 mp4 URL,按偏好选档后播放
    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 ====================
  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>&nbsp;&nbsp;跳转到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();
      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>&nbsp;
            <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 (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();
      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>&nbsp;
            <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 (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 parts = code.split(/-/);
      const videoSeries = parts[0].toLowerCase();
      const videoNo = parts[1] ? String(parts[1]).padStart(5, '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, '未找到预告片');
        });
      });

      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>&nbsp;
            <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 (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];
      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>&nbsp;
            <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 (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();
      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>&nbsp;
            <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 (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;
      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>&nbsp;
            <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 (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 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>&nbsp;
            <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>&nbsp;
          <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-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);
  }

})();