91pinse-优化

91pinse 视频宽屏

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         91pinse-优化
// @namespace    https://sleazyfork.org/zh-CN/users/1461640-%E6%98%9F%E5%AE%BF%E8%80%81%E9%AD%94
// @version      0.1.1
// @description  91pinse 视频宽屏
// @match        https://91pinse.com/*
// @match        https://fplayer.cc/*
// @match        https://*.fplayer.cc/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=91pinse.com
// @license      MIT
// @grant        none
// @run-at      document-start
// ==/UserScript==

(function () {
  'use strict';

  // ======= 广告/弹窗拦截 =======
  const blockedAdHosts = ['tsyndicate.com', 'rmhfrtnd.com', 'pemsrv.com'];

  // 点击视频区域时短时间内屏蔽弹窗
  let suppressPopupUntil = 0;
  function armPopupShield(durationMs) {
    const duration = typeof durationMs === 'number' ? durationMs : 1500;
    const until = Date.now() + duration;
    suppressPopupUntil = Math.max(suppressPopupUntil, until);
  }
  function isShieldActive() {
    return Date.now() < suppressPopupUntil;
  }
  function isInsideVideoArea(target) {
    try {
      if (!target) return false;
      if (target.closest && target.closest('video, #videoIframe, .video-container')) return true;
      return false;
    } catch (_) {
      return false;
    }
  }

  // 在护盾生效期间,禁止脚本触发强制导航(assign/replace/reload)
  (function patchLocationNavigation() {
    try {
      const LocProto = window.Location && window.Location.prototype;
      if (!LocProto) return;
      const originalAssign = LocProto.assign;
      const originalReplace = LocProto.replace;
      const originalReload = LocProto.reload;
      if (originalAssign) {
        LocProto.assign = function (url) {
          if (isShieldActive()) {
            try { console.log('[91pinse-优化] 护盾生效,拦截 assign:', url); } catch (_) {}
            return; }
          return originalAssign.call(this, url);
        };
      }
      if (originalReplace) {
        LocProto.replace = function (url) {
          if (isShieldActive()) {
            try { console.log('[91pinse-优化] 护盾生效,拦截 replace:', url); } catch (_) {}
            return; }
          return originalReplace.call(this, url);
        };
      }
      if (originalReload) {
        LocProto.reload = function () {
          if (isShieldActive()) {
            try { console.log('[91pinse-优化] 护盾生效,拦截 reload'); } catch (_) {}
            return; }
          return originalReload.call(this);
        };
      }
    } catch (_) {}
  })();

  // 强制启用原生右键菜单(视频区域)
  function stripContextMenuAttribute(root) {
    try {
      const scope = (root || document).querySelectorAll('[oncontextmenu]');
      scope.forEach(function (el) {
        try { el.oncontextmenu = null; el.removeAttribute('oncontextmenu'); } catch (_) {}
      });
    } catch (_) {}
  }
  function enableContextMenuInDoc(doc) {
    try {
      doc.addEventListener('contextmenu', function (e) {
        try {
          // 仅在视频区域或包含视频的容器中放开右键
          const target = e.target;
          const inVideo = target && (target.tagName === 'VIDEO' || (target.closest && target.closest('video, .video-container, .plyr, .plyr__video-wrapper')));
          if (inVideo) {
            e.stopImmediatePropagation();
            e.stopPropagation();
          }
        } catch (_) {}
      }, true);
      const removeInline = function (root) {
        try {
          const scope = (root || doc).querySelectorAll('[oncontextmenu], .plyr__poster');
          scope.forEach(function (el) { try { el.oncontextmenu = null; el.removeAttribute('oncontextmenu'); } catch (_) {} });
        } catch (_) {}
      };
      removeInline(doc);
      // 观察 iframe 内部变化,持续移除 oncontextmenu
      const obs = new doc.defaultView.MutationObserver(function (mutations) {
        mutations.forEach(function (m) {
          if (m.type === 'childList') {
            m.addedNodes.forEach(function (n) { if (n.nodeType === 1) removeInline(n); });
          } else if (m.type === 'attributes' && (m.attributeName === 'oncontextmenu' || m.attributeName === 'style')) {
            try { m.target.oncontextmenu = null; m.target.removeAttribute('oncontextmenu'); } catch (_) {}
          }
        });
      });
      obs.observe(doc.documentElement || doc, { childList: true, subtree: true, attributes: true, attributeFilter: ['oncontextmenu','style'] });
    } catch (_) {}
  }
  function tryHookSameOriginIframe(iframe) {
    try {
      const cw = iframe && iframe.contentWindow;
      const cd = cw && cw.document;
      if (!cd) return;
      // 在同源或明确为 fplayer 域时尝试放开右键
      const origin = cw.location.origin;
      if (origin === window.location.origin || /(^https?:\/\/)?([\w-]+\.)?fplayer\.cc$/i.test(origin)) {
        enableContextMenuInDoc(cd);
      }
    } catch (_) {
      // 跨域无法处理
    }
  }
  // 在捕获阶段阻止站点脚本接管右键,但不阻止默认行为,从而保留浏览器菜单
  document.addEventListener('contextmenu', function (e) {
    try {
      // 全局右键:启动护盾,阻断冒泡,减少站点监听器触发的弹窗/刷新
      armPopupShield(1600);
      e.stopImmediatePropagation();
      e.stopPropagation();
      // 不调用 preventDefault,确保浏览器显示自带菜单
    } catch (_) {}
  }, true);

  function isBlockedAdUrl(url) {
    try {
      const u = new URL(url, window.location.href);
      return blockedAdHosts.some(function (host) {
        return u.hostname === host || u.hostname.endsWith('.' + host);
      });
    } catch (e) {
      return false;
    }
  }

  // ======= 91pinse: 优先使用 HD =======
  function isPinseDomain() {
    try { return /(^|\.)91pinse\.com$/i.test(window.location.hostname); } catch (_) { return false; }
  }
  function extractVideoIdFromPath(pathname) {
    try {
      const mHD = pathname.match(/^\/vhd\/(\d+)/);
      if (mHD) return { id: mHD[1], isHD: true };
      const mSD = pathname.match(/^\/v\/(\d+)/);
      if (mSD) return { id: mSD[1], isHD: false };
      return null;
    } catch (_) { return null; }
  }
  function buildPinseUrl(isHD, id) {
    return isHD ? `/vhd/${id}` : `/v/${id}`;
  }
  // 基于页面“是否存在 HD 标记/链接”来判断是否应走 HD,而不是发请求探测
  function elementHasHdText(el) {
    try {
      if (!el) return false;
      const text = (el.textContent || '').trim();
      return text === 'HD' || text === 'Hd' || text === 'hd' || /\bHD\b/i.test(text);
    } catch (_) { return false; }
  }
  function hasHdMarkerIn(container) {
    try {
      if (!container) return false;
      // 仅识别右上角的 HD 徽标,避免把时长等误判
      const hdBadges = container.querySelectorAll('.absolute.top-1.right-1, .absolute[class*="top-1"][class*="right-1"], .bg-primary-600');
      for (let i = 0; i < hdBadges.length; i++) { if (elementHasHdText(hdBadges[i])) return true; }
      // 也可能作为按钮或链接出现
      const anchors = container.querySelectorAll('a');
      for (let i = 0; i < anchors.length; i++) { if (elementHasHdText(anchors[i]) || /Switch\s+to\s+HD/i.test((anchors[i].textContent||''))) return true; }
      return false;
    } catch (_) { return false; }
  }
  function hasHdForId(id, scope) {
    try {
      const root = scope || document;
      if (root.querySelector(`a[href="/vhd/${id}"]`)) return true;
      return hasHdMarkerIn(root);
    } catch (_) { return false; }
  }
  async function preferHDOnPage() {
    if (!isPinseDomain()) return;
    const info = extractVideoIdFromPath(window.location.pathname);
    if (!info || info.isHD) return;
    // 仅在页面存在明确的 HD 入口/标记时才切换
    const pageHasHd = hasHdForId(info.id, document) || !!document.querySelector('.absolute.top-1.right-1, .absolute[class*="top-1"][class*="right-1"], a:contains("Switch to HD")');
    if (!pageHasHd) return;
    const hdUrl = buildPinseUrl(true, info.id);
    console.log('[91pinse-优化] 自动切换到 HD:', hdUrl);
    window.location.replace(hdUrl);
  }
  function upgradeLinksToHD(root) {
    if (!isPinseDomain()) return;
    const scope = (root || document).querySelectorAll('a[href^="/v/"]:not([data-hd-upgraded])');
    scope.forEach(function (a) {
      try {
        const m = a.getAttribute('href').match(/^\/v\/(\d+)/);
        if (!m) return;
        const id = m[1];
        const sdHref = a.getAttribute('href');
        const hdHref = `/vhd/${id}`;
        // 仅当“该卡片”内存在 HD 标记/链接时才升级;否则如已被误升级则回退
        const card = a.closest('.group, [id^="preview_"], .rounded, .shadow, .card, .overflow-hidden, .relative') || a.parentElement;
        const shouldUpgrade = hasHdForId(id, card);
        if (shouldUpgrade) {
          a.dataset.hdUpgraded = '1';
          a.dataset.sdHref = sdHref;
          a.setAttribute('href', hdHref);
        } else {
          // 若之前被误升级,恢复 sd
          const prevSd = a.dataset.sdHref;
          if (prevSd && a.getAttribute('href') === hdHref) {
            a.setAttribute('href', prevSd);
          }
          delete a.dataset.hdUpgraded;
        }
      } catch (_) {}
    });
  }
  function installPreferHdClickHandler() {
    if (!isPinseDomain()) return;
    document.addEventListener('click', async function (event) {
      try {
        if (event.defaultPrevented) return;
        if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
        let anchor = event.target && event.target.closest ? event.target.closest('a') : null;
        if (!anchor) return;
        const href = anchor.getAttribute('href') || '';
        const m = href.match(/^\/v\/(\d+)/);
        if (!m) return;
        const id = m[1];
        // 仅当附近确实有 HD 标记/链接时,才替换为 HD
        const card = anchor.closest('.group, [id^="preview_"], .rounded, .shadow, .card, .overflow-hidden, .relative') || anchor.parentElement;
        if (!hasHdForId(id, card)) return;
        const hdUrl = `/vhd/${id}`;
        if (href === hdUrl) return;
        event.preventDefault();
        window.location.href = hdUrl;
      } catch (_) {}
    }, true);
  }

  // 移除/禁用指向广告域名的 iframe,以防止其在自身上下文内弹窗
  function shouldRemoveIframe(iframe) {
    try {
      const src = iframe.getAttribute('src') || '';
      if (!src) return false;
      return isBlockedAdUrl(src);
    } catch (_) {
      return false;
    }
  }

  function sanitizeIframes(root) {
    const frames = (root || document).querySelectorAll('iframe');
    frames.forEach(function (frame) {
      try {
        if (shouldRemoveIframe(frame)) {
          console.log('[91pinse-优化] 已移除广告 iframe:', frame.src);
          frame.remove();
          return;
        }
        // 降权可能触发弹窗的 iframe:尽量禁止其响应点击
        if (!frame.__pinseLocked) {
          frame.style.pointerEvents = 'auto';
          // 对可疑域名关闭指针事件,避免在其内部触发“基于用户手势”的弹窗
          if (frame.src && isBlockedAdUrl(frame.src)) {
            frame.style.pointerEvents = 'none';
            frame.__pinseLocked = true;
          }
        }
      } catch (_) {}
    });
  }

  (function patchWindowOpen() {
    const originalOpen = window.open;
    window.open = function () {
      try {
        let url = arguments[0];
        if (url && typeof url !== 'string') {
          try { url = String(url); } catch (_) {}
        }
        // 直接拦截已知广告域名
        if (typeof url === 'string' && isBlockedAdUrl(url)) {
          console.log('[91pinse-优化] 已拦截弹窗:', url);
          return null;
        }
        // 某些站点先打开 about:blank 再在新窗口内跳转,这里在 91pinse 域名内一并拦截
        if ((url === '' || url === 'about:blank' || url === undefined) && window.location.hostname.endsWith('91pinse.com')) {
          const target = arguments[1];
          if (target === '_blank' || !target) {
            console.log('[91pinse-优化] 已拦截空白弹窗');
            return null;
          }
        }
        // 若当前处于“弹窗护盾”时间窗内,仅允许同源窗口
        if (isShieldActive()) {
          try {
            const u = new URL(typeof url === 'string' ? url : '', window.location.href);
            const sameOrigin = u.origin === window.location.origin;
            if (!sameOrigin) {
              console.log('[91pinse-优化] 护盾生效,已拦截跨域弹窗:', url || '(empty)');
              return null;
            }
          } catch (_) {
            console.log('[91pinse-优化] 护盾生效,已拦截无法解析的弹窗');
            return null;
          }
        }
      } catch (e) {}
      return originalOpen.apply(this, arguments);
    };
    try { if (window.top && window.top !== window) window.top.open = window.open; } catch (_) {}
    try { if (window.parent && window.parent !== window) window.parent.open = window.open; } catch (_) {}
  })();

  // 捕获阶段拦截到指向广告域名的 <a> 点击
  function interceptAnchorEvent(event) {
      try {
        let anchor = null;
        const path = event.composedPath ? event.composedPath() : [];
        for (let i = 0; i < path.length; i++) {
          const node = path[i];
          if (node && node.tagName === 'A') {
            anchor = node;
            break;
          }
        }
        if (!anchor && event.target && event.target.closest) {
          anchor = event.target.closest('a');
        }
        // 列表页点击/右键同源链接时,先开启护盾,防止随后触发的跨域弹窗
        if (anchor && anchor.href) {
          try {
            const u = new URL(anchor.href, window.location.href);
            if (u.origin === window.location.origin) {
              armPopupShield(1600);
            }
          } catch (_) {}
        }
        if (anchor && anchor.href && isBlockedAdUrl(anchor.href)) {
          event.preventDefault();
          event.stopImmediatePropagation();
          console.log('[91pinse-优化] 已阻止跳转到广告域名:', anchor.href);
        }
      } catch (e) {}
  }

  ['click', 'mousedown', 'mouseup', 'pointerdown', 'auxclick', 'contextmenu', 'touchstart'].forEach(function (evt) {
    document.addEventListener(evt, interceptAnchorEvent, true);
  });

  // 在视频区域的任意按键或点击都会激活护盾(包括右键)
  ['mousedown', 'pointerdown', 'click', 'auxclick', 'contextmenu'].forEach(function (evt) {
    document.addEventListener(
      evt,
      function (e) {
        try {
          // 在视频区域或链接点击前,启动护盾
          if (isInsideVideoArea(e.target)) armPopupShield(1600);
          // 右键/中键在任何位置也启动护盾
          if (evt === 'contextmenu' || (e.button && e.button !== 0)) armPopupShield(1600);
        } catch (_) {}
      },
      true
    );
  });

  const currentDomain = window.location.hostname;

  const config = {
    selectors: {
      videoContainer: '.video-container',
      videoIframe: '#videoIframe',
      mainGrid: '.grid.grid-cols-1.lg\\:grid-cols-3.gap-6',
      mainCol: '.lg\\:col-span-2'
    },
    applyStyles: function () {
      const mainGrid = document.querySelector(
        '.grid.grid-cols-1.lg\\:grid-cols-3.gap-6'
      );
      const mainCol = document.querySelector('.lg\\:col-span-2');

      if (mainGrid) {
        mainGrid.classList.remove('grid-cols-1', 'lg:grid-cols-3');
        mainGrid.classList.add('grid-cols-1');
        mainGrid.style.maxWidth = '100%';
      }

      if (mainCol) {
        mainCol.classList.remove('lg:col-span-2');
        mainCol.style.width = '100%';
        mainCol.style.maxWidth = '100%';
      }
    },
    cssRules: `
      .video-container {
        width: 100% !important;
        max-width: 100% !important;
        margin: 0 auto !important;
        background-color: #000 !important;
      }
      /* 去除 plyr 覆盖层影响右键 */
      .plyr__poster { pointer-events: none !important; }
      .plyr__controls[hidden], .plyr__controls[style*="display: none"] { display: none !important; }
      .pinse-open-player-btn {
        position: absolute;
        top: 10px;
        right: 10px;
        z-index: 9999;
        padding: 6px 10px;
        font-size: 12px;
        border-radius: 6px;
        background: rgba(0,0,0,0.6);
        color: #fff;
        border: 1px solid rgba(255,255,255,0.2);
        cursor: pointer;
        user-select: none;
        text-decoration: none;
      }
      .pinse-open-player-btn:hover { background: rgba(0,0,0,0.75); }
      #videoIframe, .aspect-video {
        display: block !important;
        width: 100% !important;
        height: auto !important;
      }
      .grid.grid-cols-1 {
        width: 100% !important;
        max-width: 100% !important;
      }
      .text-xl.md\\:text-2xl.font-bold.p-4 {
        text-align: center !important;
      }
      .grid.grid-cols-2.md\\:grid-cols-3.gap-4 {
        grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)) !important;
      }
      @media (min-width: 1400px) {
        .video-container {
          max-width: 85% !important;
        }
        .lg\\:col-span-2 {
          margin: 0 auto !important;
          max-width: 85% !important;
        }
      }
      body {
        background-color: #121212 !important;
      }
      .bg-base-100 {
        background-color: #1a1a1a !important;
      }
      .shadow-lg {
        box-shadow: 0 4px 6px rgba(0,0,0,0.2) !important;
      }
    `
  };

  function setWideScreenMode() {
    const videoContainer = document.querySelector(config.selectors.videoContainer);
    const videoElement =
      document.querySelector(config.selectors.videoElement) ||
      document.querySelector(config.selectors.videoIframe);

    if (!videoContainer && !videoElement) {
      console.log(`${currentDomain}宽屏模式: 未找到视频元素`);
      return;
    }

    if (config.applyStyles) {
      config.applyStyles();
    }

    if (videoContainer) {
      const viewportWidth = window.innerWidth;
      const viewportHeight = window.innerHeight;
      const targetAspect = 16 / 9;
      let containerWidth = viewportWidth;
      let containerMaxWidth = viewportWidth;
      let containerMaxHeight = Math.floor(viewportHeight * 0.9);

      if (viewportWidth >= 1400) {
        containerMaxWidth = Math.floor(viewportWidth * 0.9);
      }

      // 计算在可用高度内能容纳的最大宽度
      const widthByHeight = Math.floor(containerMaxHeight * targetAspect);
      containerWidth = Math.min(containerMaxWidth, widthByHeight);

      videoContainer.style.width = containerWidth + 'px';
      videoContainer.style.maxWidth = containerWidth + 'px';
      videoContainer.style.margin = '0 auto';
      videoContainer.style.position = 'relative';
    }

    if (videoElement) {
      const aspectRatio = 16 / 9;
      videoElement.style.width = '100%';
      videoElement.style.height = 'auto';
      videoElement.style.maxHeight = '90vh';
      videoElement.removeAttribute('style');
      // 重新设置关键样式,避免行内样式与 CSS 冲突
      videoElement.style.width = '100%';
      videoElement.style.height = 'auto';
      videoElement.style.maxHeight = '90vh';
    }

    ensureOpenPlayerButton();

    console.log(`${currentDomain}宽屏模式: 已启用宽屏模式`);
  }

  function setupResizeListener() {
    let resizeRaf = null;
    window.addEventListener('resize', function () {
      if (resizeRaf) cancelAnimationFrame(resizeRaf);
      resizeRaf = requestAnimationFrame(function () {
        setWideScreenMode();
      });
    });
  }

  function addStyles() {
    const styleSheet = document.createElement('style');
    styleSheet.textContent = config.cssRules;
    document.head.appendChild(styleSheet);
  }
  // 去除内联 style 中的强制宽高/比例,避免与适配冲突
  function normalizeInlineStyles() {
    try {
      const iframe = document.querySelector('#videoIframe');
      if (iframe) {
        iframe.style.removeProperty('max-height');
        iframe.style.removeProperty('aspect-ratio');
        iframe.style.removeProperty('height');
        iframe.style.removeProperty('width');
      }
      const container = document.querySelector('.video-container');
      if (container) {
        container.style.removeProperty('max-width');
        container.style.removeProperty('width');
      }
    } catch (_) {}
  }

  // 在同域播放器中提供“在新标签页打开播放器”按钮,方便用户用原生界面右键
  function ensureOpenPlayerButton() {
    try {
      const container = document.querySelector(config.selectors.videoContainer);
      const iframe = document.querySelector(config.selectors.videoIframe);
      if (!container || !iframe) return;
      if (iframe.src && new URL(iframe.src, location.href).origin !== location.origin) return; // 仅处理同源
      let btn = container.querySelector('.pinse-open-player-btn');
      if (!btn) {
        btn = document.createElement('a');
        btn.className = 'pinse-open-player-btn';
        btn.textContent = '在新标签页打开播放器';
        btn.target = '_blank';
        container.appendChild(btn);
      }
      btn.href = iframe.src || location.href;
    } catch (_) {}
  }

  function init() {
    addStyles();
    sanitizeIframes(document);
    stripContextMenuAttribute(document);
    // 尝试对同源 iframe 也启用右键
    try {
      document.querySelectorAll('iframe').forEach(function (f) { tryHookSameOriginIframe(f); });
    } catch (_) {}
    // 列表链接与详情页优先 HD
    upgradeLinksToHD(document);
    preferHDOnPage();
    installPreferHdClickHandler();

    const delay = 200;

    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', function () {
        setTimeout(function () {
          setWideScreenMode();
          setupResizeListener();
        }, delay);
      });
    } else {
      setTimeout(function () {
        setWideScreenMode();
        setupResizeListener();
      }, delay);
    }

    document.addEventListener(
      'load',
      function (e) {
        if (e.target.tagName === 'VIDEO' || e.target.tagName === 'IFRAME') {
          setWideScreenMode();
        }
      },
      true
    );

    window.addEventListener('load', function () {
      setWideScreenMode();
    });

    const observer = new MutationObserver(function (mutations) {
      let shouldUpdate = false;
      mutations.forEach(function (mutation) {
        if (mutation.addedNodes.length > 0) {
          shouldUpdate = true;
          mutation.addedNodes.forEach(function (node) {
            try {
              if (node.nodeType === 1) {
                if (node.tagName === 'IFRAME') {
                  sanitizeIframes(node.parentNode || document);
                  tryHookSameOriginIframe(node);
                } else if (node.querySelectorAll) {
                  sanitizeIframes(node);
                  stripContextMenuAttribute(node);
                  // 针对新增的同源 iframe
                  try { node.querySelectorAll('iframe').forEach(function (f) { tryHookSameOriginIframe(f); }); } catch (_) {}
                  // 新增节点里同样升级视频链接
                  upgradeLinksToHD(node);
                }
              }
            } catch (_) {}
          });
        }
      });
      if (shouldUpdate) {
        setWideScreenMode();
      }
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ['src','style']
    });

    console.log(`${currentDomain}宽屏模式: 已启用`);
  }

  init();
})();