Auto-play video thumbnails

Auto-play video thumbnails with playback speed control and pause functionality on adult sites

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 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.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

// ==UserScript==
// @name         Auto-play video thumbnails 
// @namespace    https://greasyfork.org/users/1168969
// @version      2.0
// @description  Auto-play video thumbnails with playback speed control and pause functionality on adult sites
// @author       6969RandomGuy6969
// @match        https://sxyprn.com/*
// @match        https://watchporn.to/*
// @match        https://yesporn.vip/*
// @match        https://www.theyarehuge.com/*
// @match        https://www.eporner.com/*
// @match        https://www.shyfap.net/*
// @match        https://www.wow.xxx/*
// @match        https://pornone.com/*
// @match        https://www.tnaflix.com/*
// @match        https://www.pornhits.com/*
// @match        https://hqporner.com/*
// @match        https://www.hqporner.com/*
// @match        https://m.hqporner.com/*
// @match        https://pornmz.com/*
// @grant        GM_registerMenuCommand
// @grant        GM_openInTab
// @grant        GM_setValue
// @grant        GM_getValue
// @icon         https://cdn-icons-png.flaticon.com/512/3998/3998861.png
// ==/UserScript==

(function () {
  'use strict';

  const SETTINGS = {
    playbackSpeed: GM_getValue('playbackSpeed', 1),
    isPaused: GM_getValue('isPaused', false),
    panelX: GM_getValue('panelX', null),
    panelY: GM_getValue('panelY', null)
  };

  const videoRegistry = new Set();
  let configPanel = null;

  function updateAllVideos() {
    videoRegistry.forEach(video => {
      if (video && video.parentNode) {
        video.playbackRate = SETTINGS.playbackSpeed;
        if (SETTINGS.isPaused) {
          video.pause();
        } else {
          video.play().catch(() => {});
        }
      }
    });
  }

  function setPlaybackSpeed(speed) {
    speed = Math.max(0.25, Math.min(2, speed));
    SETTINGS.playbackSpeed = speed;
    GM_setValue('playbackSpeed', speed);
    updateAllVideos();
    updatePanelDisplay();
  }

  function togglePause() {
    SETTINGS.isPaused = !SETTINGS.isPaused;
    GM_setValue('isPaused', SETTINGS.isPaused);
    updateAllVideos();
    updatePanelDisplay();
  }

  function updatePanelDisplay() {
    if (!configPanel) return;
    const speedDisplay = configPanel.querySelector('.speed-display');
    const pauseBtn = configPanel.querySelector('.pause-btn');
    if (speedDisplay) speedDisplay.textContent = `${SETTINGS.playbackSpeed.toFixed(2)}x`;
    if (pauseBtn) pauseBtn.textContent = SETTINGS.isPaused ? '▶️ Play' : '⏸️ Pause';
  }

  function createConfigPanel() {
    if (configPanel) {
      configPanel.style.display = 'block';
      return;
    }

    const panel = document.createElement('div');
    panel.id = 'video-config-panel';
    panel.innerHTML = `
      <div class="panel-header">
        <span class="panel-title">Thumbnail Control</span>
        <button class="close-btn">×</button>
      </div>
      <div class="panel-body">
        <div class="speed-control">
          <button class="speed-btn minus-btn">−</button>
          <span class="speed-display">${SETTINGS.playbackSpeed.toFixed(2)}x</span>
          <button class="speed-btn plus-btn">+</button>
        </div>
        <button class="pause-btn">${SETTINGS.isPaused ? '▶️ Play' : '⏸️ Pause'}</button>
      </div>
    `;

    const style = document.createElement('style');
    style.textContent = `
      #video-config-panel {
        position: fixed;
        z-index: 999999;
        background: rgba(20, 20, 20, 0.95);
        backdrop-filter: blur(10px);
        -webkit-backdrop-filter: blur(10px);
        border: 1px solid rgba(255, 255, 255, 0.1);
        border-radius: 12px;
        padding: 0;
        font-family: Arial, sans-serif;
        box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
        user-select: none;
        min-width: 160px;
        max-width: 200px;
      }
      #video-config-panel .panel-header {
        background: rgba(40, 40, 40, 0.8);
        padding: 8px 10px;
        border-bottom: 1px solid rgba(255, 255, 255, 0.1);
        display: flex;
        justify-content: space-between;
        align-items: center;
        cursor: move;
        border-radius: 11px 11px 0 0;
      }
      #video-config-panel .panel-title {
        color: #fff;
        font-size: 12px;
        font-weight: bold;
        text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
        white-space: nowrap;
      }
      #video-config-panel .close-btn {
        background: rgba(255, 255, 255, 0.1);
        border: none;
        color: #fff;
        font-size: 20px;
        font-weight: bold;
        cursor: pointer;
        padding: 0;
        width: 22px;
        height: 22px;
        line-height: 20px;
        border-radius: 4px;
        transition: all 0.2s;
        flex-shrink: 0;
      }
      #video-config-panel .close-btn:hover {
        background: rgba(255, 68, 68, 0.8);
        box-shadow: 0 0 10px rgba(255, 68, 68, 0.5);
      }
      #video-config-panel .panel-body {
        padding: 12px;
        background: rgba(30, 30, 30, 0.5);
        border-radius: 0 0 11px 11px;
      }
      #video-config-panel .speed-control {
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 8px;
        margin-bottom: 10px;
      }
      #video-config-panel .speed-btn {
        background: rgba(255, 255, 255, 0.15);
        backdrop-filter: blur(5px);
        -webkit-backdrop-filter: blur(5px);
        border: 1px solid rgba(255, 255, 255, 0.2);
        color: #fff;
        font-size: 18px;
        font-weight: bold;
        width: 32px;
        height: 32px;
        border-radius: 6px;
        cursor: pointer;
        transition: all 0.2s;
        padding: 0;
        text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
        display: flex;
        align-items: center;
        justify-content: center;
      }
      #video-config-panel .speed-btn:hover {
        background: rgba(255, 255, 255, 0.2);
        border-color: rgba(255, 255, 255, 0.3);
        box-shadow: 0 0 10px rgba(255, 255, 255, 0.2);
      }
      #video-config-panel .speed-btn:active { transform: scale(0.95); }
      #video-config-panel .speed-display {
        color: #fff;
        font-size: 14px;
        font-weight: bold;
        min-width: 50px;
        text-align: center;
        text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
      }
      #video-config-panel .pause-btn {
        width: 100%;
        background: rgba(255, 255, 255, 0.15);
        backdrop-filter: blur(5px);
        -webkit-backdrop-filter: blur(5px);
        border: 1px solid rgba(255, 255, 255, 0.2);
        color: #fff;
        padding: 8px;
        font-size: 13px;
        font-weight: 600;
        border-radius: 6px;
        cursor: pointer;
        transition: all 0.2s;
        text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
      }
      #video-config-panel .pause-btn:hover {
        background: rgba(255, 255, 255, 0.2);
        border-color: rgba(255, 255, 255, 0.3);
        box-shadow: 0 0 10px rgba(255, 255, 255, 0.2);
      }
      #video-config-panel .pause-btn:active { transform: scale(0.98); }
      @media (max-width: 600px) {
        #video-config-panel { min-width: 140px; max-width: 160px; }
        #video-config-panel .panel-header { padding: 6px 8px; }
        #video-config-panel .panel-title { font-size: 11px; }
        #video-config-panel .close-btn { width: 20px; height: 20px; font-size: 18px; }
        #video-config-panel .panel-body { padding: 8px; }
        #video-config-panel .speed-btn { width: 28px; height: 28px; font-size: 16px; }
        #video-config-panel .speed-display { font-size: 12px; min-width: 40px; }
        #video-config-panel .pause-btn { padding: 6px; font-size: 12px; }
      }
      @media (max-width: 400px) {
        #video-config-panel { min-width: 120px; max-width: 140px; }
        #video-config-panel .speed-control { gap: 4px; }
        #video-config-panel .speed-btn { width: 24px; height: 24px; font-size: 14px; }
        #video-config-panel .speed-display { font-size: 11px; min-width: 35px; }
      }
    `;
    document.head.appendChild(style);

    document.body.appendChild(panel);
    configPanel = panel;

    const panelWidth = 200;
    const panelHeight = 120;
    const margin = 10;

    if (SETTINGS.panelX !== null && SETTINGS.panelY !== null) {
      const maxX = window.innerWidth - panelWidth - margin;
      const maxY = window.innerHeight - panelHeight - margin;
      panel.style.left = Math.max(margin, Math.min(SETTINGS.panelX, maxX)) + 'px';
      panel.style.top  = Math.max(margin, Math.min(SETTINGS.panelY, maxY)) + 'px';
    } else {
      panel.style.right  = Math.min(20, window.innerWidth  - panelWidth  - margin) + 'px';
      panel.style.bottom = Math.min(20, window.innerHeight - panelHeight - margin) + 'px';
    }

    panel.querySelector('.close-btn').addEventListener('click', () => { panel.style.display = 'none'; });
    panel.querySelector('.minus-btn').addEventListener('click', () => { setPlaybackSpeed(SETTINGS.playbackSpeed - 0.25); });
    panel.querySelector('.plus-btn').addEventListener('click',  () => { setPlaybackSpeed(SETTINGS.playbackSpeed + 0.25); });
    panel.querySelector('.pause-btn').addEventListener('click', togglePause);

    // Draggable
    let isDragging = false, currentX, currentY, initialX, initialY;
    const header = panel.querySelector('.panel-header');

    header.addEventListener('mousedown', e => {
      if (e.target.classList.contains('close-btn')) return;
      isDragging = true;
      initialX = e.clientX - (SETTINGS.panelX || panel.offsetLeft);
      initialY = e.clientY - (SETTINGS.panelY || panel.offsetTop);
    });

    document.addEventListener('mousemove', e => {
      if (!isDragging) return;
      e.preventDefault();
      const rect = panel.getBoundingClientRect();
      const maxX = window.innerWidth  - rect.width  - margin;
      const maxY = window.innerHeight - rect.height - margin;
      currentX = Math.max(margin, Math.min(e.clientX - initialX, maxX));
      currentY = Math.max(margin, Math.min(e.clientY - initialY, maxY));
      panel.style.left   = currentX + 'px';
      panel.style.top    = currentY + 'px';
      panel.style.right  = 'auto';
      panel.style.bottom = 'auto';
    });

    document.addEventListener('mouseup', () => {
      if (isDragging) {
        SETTINGS.panelX = currentX || panel.offsetLeft;
        SETTINGS.panelY = currentY || panel.offsetTop;
        GM_setValue('panelX', SETTINGS.panelX);
        GM_setValue('panelY', SETTINGS.panelY);
      }
      isDragging = false;
    });

    window.addEventListener('resize', () => {
      if (!configPanel || configPanel.style.display === 'none') return;
      const rect = configPanel.getBoundingClientRect();
      let newX = rect.left, newY = rect.top, needs = false;
      if (rect.right  > window.innerWidth  - margin) { newX = window.innerWidth  - rect.width  - margin; needs = true; }
      if (rect.left   < margin)                       { newX = margin;                                     needs = true; }
      if (rect.bottom > window.innerHeight - margin)  { newY = window.innerHeight - rect.height - margin; needs = true; }
      if (rect.top    < margin)                       { newY = margin;                                     needs = true; }
      if (needs) {
        configPanel.style.left = newX + 'px'; configPanel.style.top = newY + 'px';
        configPanel.style.right = 'auto';     configPanel.style.bottom = 'auto';
        SETTINGS.panelX = newX; SETTINGS.panelY = newY;
        GM_setValue('panelX', newX); GM_setValue('panelY', newY);
      }
    });
  }

  // ==================== GM MENU ====================
  GM_registerMenuCommand('⚙️ Open Config UI', createConfigPanel);

  // ── Supported Sites ──
  GM_registerMenuCommand('🌐 Sxyprn', () => GM_openInTab('https://sxyprn.com', { active: true }));
  GM_registerMenuCommand('🌐 WatchPorn', () => GM_openInTab('https://watchporn.to', { active: true }));
  GM_registerMenuCommand('🌐 YesPorn', () => GM_openInTab('https://yesporn.vip', { active: true }));
  GM_registerMenuCommand('🌐 TheyAreHuge', () => GM_openInTab('https://www.theyarehuge.com', { active: true }));
  GM_registerMenuCommand('🌐 Eporner', () => GM_openInTab('https://www.eporner.com', { active: true }));
  GM_registerMenuCommand('🌐 ShyFap', () => GM_openInTab('https://www.shyfap.net', { active: true }));
  GM_registerMenuCommand('🌐 Wow.xxx', () => GM_openInTab('https://www.wow.xxx', { active: true }));
  GM_registerMenuCommand('🌐 Pornone', () => GM_openInTab('https://pornone.com', { active: true }));
  GM_registerMenuCommand('🌐 Tnaflix', () => GM_openInTab('https://www.tnaflix.com', { active: true }));
  GM_registerMenuCommand('🌐 PornHits', () => GM_openInTab('https://www.pornhits.com', { active: true }));
  GM_registerMenuCommand('🌐 HQPorner', () => GM_openInTab('https://hqporner.com', { active: true }));
  GM_registerMenuCommand('🌐 PornMZ', () => GM_openInTab('https://pornmz.com', { active: true }));

  // ── Script Links ──
  GM_registerMenuCommand('🔞 More Scripts (Sleazyfork)', () => {
    GM_openInTab('https://sleazyfork.org/en/users/1168969-6969randomguy6969', { active: true });
  });
  GM_registerMenuCommand('📜 More Scripts (Greasyfork)', () => {
    GM_openInTab('https://greasyfork.org/en/users/1168969-6969randomguy6969', { active: true });
  });

  // ==================== CORE FUNCTIONALITY ====================
  const hostname = window.location.hostname;

  // ---- FIX 1: Don't hide the image until the video is actually ready to play ----
  // ---- FIX 2: Keep the image visible as fallback if video fails ----
  function insertPreviewVideo(image, videoUrl) {
    if (!videoUrl) return;
    if (image.dataset.previewAttached) return;
    image.dataset.previewAttached = '1';

    const video = document.createElement('video');
    video.src = videoUrl;
    video.muted = true;
    video.loop = true;
    video.autoplay = !SETTINGS.isPaused;
    video.playsInline = true;
    video.preload = 'auto';
    video.playbackRate = SETTINGS.playbackSpeed;
    video.style.cssText = 'width:100%;height:100%;object-fit:cover;position:absolute;top:0;left:0;';

    video.setAttribute('importance', 'high');
    video.setAttribute('fetchpriority', 'high');

    videoRegistry.add(video);

    const parent = image.parentNode;
    parent.style.position = 'relative';
    parent.appendChild(video);

    const showVideo = () => {
      video.style.opacity = '1';
      image.style.opacity = '0';
      if (!SETTINGS.isPaused) video.play().catch(() => {});
    };

    // Try canplay first
    video.addEventListener('canplay', showVideo, { once: true });

    // Fallback: if canplay doesn't fire in 800ms, show anyway
    // (site JS may suppress the event by manipulating the element)
    const fallbackTimer = setTimeout(() => {
      video.removeEventListener('canplay', showVideo);
      showVideo();
    }, 800);

    video.addEventListener('canplay', () => clearTimeout(fallbackTimer), { once: true });

    video.load();
    if (!SETTINGS.isPaused) video.play().catch(() => {});

    video.onerror = () => {
      clearTimeout(fallbackTimer);
      videoRegistry.delete(video);
      video.remove();
      image.style.opacity = '1';
    };
  }

  // IntersectionObserver wrapper
  const activeObservers = new Set();

  function observeElements(selector, getUrl) {
    // Don't set up duplicate observers for the same selector
    if (activeObservers.has(selector)) return;
    activeObservers.add(selector);
    const observer = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const el = entry.target;
          const url = getUrl(el);
          if (url) insertPreviewVideo(el, url);
          observer.unobserve(el);
        }
      });
    }, { rootMargin: '500px', threshold: 0.01 });

    const observedElements = new WeakSet();

    function watch() {
      document.querySelectorAll(selector).forEach(el => {
        if (!observedElements.has(el)) {
          observer.observe(el);
          observedElements.add(el);
        }
      });
    }

    watch();

    let mutationTimeout;
    new MutationObserver(() => {
      clearTimeout(mutationTimeout);
      mutationTimeout = setTimeout(watch, 100);
    }).observe(document.body, { childList: true, subtree: true });

    setTimeout(watch, 500);
    setTimeout(watch, 1500);
  }

  // ==================== SITE HANDLERS ====================
  const siteHandlers = {

    "eporner.com": function () {
      function getPreviewUrl(id) {
        const s = id.toString();
        if (s.length < 5) return null;
        return `https://static-eu-cdn.eporner.com/thumbs/static4/${s[0]}/${s.slice(0,2)}/${s.slice(0,3)}/${s}/${s}-preview.webm`;
      }

      function initThumb(thumb) {
        if (thumb.dataset.epInit) return;
        thumb.dataset.epInit = '1';

        const url = getPreviewUrl(thumb.dataset.id);
        if (!url) return;

        let vid = thumb.querySelector('video');
        if (!vid) {
          vid = document.createElement('video');
          vid.muted = true;
          vid.loop = true;
          vid.playsInline = true;
          vid.preload = 'auto';
          vid.style.cssText = 'position:absolute !important;inset:0 !important;width:100% !important;height:100% !important;object-fit:cover !important;z-index:10 !important;background:#000;opacity:1 !important;';
          const cont = thumb.querySelector('.mbimg, .mbcontent') || thumb;
          cont.style.position = 'relative';
          cont.style.overflow = 'hidden';
          cont.appendChild(vid);
        }

        if (vid.src !== url) {
          vid.removeAttribute('src');
          vid.src = url;
          vid.load();
        }

        // Register for global pause/speed control
        videoRegistry.add(vid);
        vid.playbackRate = SETTINGS.playbackSpeed;

        // Play only when visible, pause when scrolled away (anti-throttle)
        const obs = new IntersectionObserver(([entry]) => {
          if (entry.isIntersecting) {
            if (vid.paused && !SETTINGS.isPaused) vid.play().catch(() => {});
          } else {
            if (!vid.paused) vid.pause();
          }
        }, { threshold: [0.01, 0.3] });
        obs.observe(thumb);
      }

      // Hide static img/badges so video shows through
      const style = document.createElement('style');
      style.textContent = `.mb img,.mvhdico,[style*="ajax_loader"]{opacity:0 !important;pointer-events:none !important}.mb .mbimg,.mb .mbcontent{background:#000 !important}`;
      document.head.appendChild(style);

      // Process existing cards
      document.querySelectorAll('div.mb[data-id]').forEach(initThumb);

      // Watch for new cards (infinite scroll / AJAX)
      new MutationObserver(() => {
        document.querySelectorAll('div.mb[data-id]:not([data-ep-init])').forEach(initThumb);
      }).observe(document.body, { childList: true, subtree: true });

      // Safety interval for any missed cards
      setInterval(() => {
        document.querySelectorAll('div.mb[data-id]').forEach(initThumb);
      }, 4000);
    },

    "sxyprn.com": function () {
      document.querySelectorAll('.mini_post_vid_thumb').forEach(img => {
        const video = img.nextElementSibling;
        if (video && (video.tagName === 'VIDEO' || video.tagName === 'IFRAME')) {
          video.playbackRate = SETTINGS.playbackSpeed;
          videoRegistry.add(video);
          if (SETTINGS.isPaused) { video.pause(); } else { video.play(); }
        }
      });
    },

    "watchporn.to": () =>
      observeElements('img.thumb.lazy-load[data-preview]', el => el.getAttribute('data-preview')),

    "yesporn.vip": () =>
      observeElements('img.lazy-load[data-preview]', el => el.getAttribute('data-preview')),

    "theyarehuge.com": function () {
      observeElements('img[data-preview]', el => el.getAttribute('data-preview'));

      const style = document.createElement('style');
      style.textContent = `
        body, html { background-color:#000 !important; color:#d1d1d1 !important; }
        * { background-color:transparent !important; border-color:#444 !important; color:inherit !important; }
        a, p, h1, h2, h3, h4, h5, h6, span, div { color:#d1d1d1 !important; }
        img, video { filter:brightness(0.95) contrast(1.1); }
        .header, .footer, .sidebar, .navbar, .top-menu, .main-header { background-color:#000 !important; }
      `;
      document.head.appendChild(style);

      const logo = document.querySelector('img[src*="tah-logo-m.png"]');
      if (logo) logo.style.filter = 'invert(1) hue-rotate(-180deg) brightness(1) saturate(10)';
    },

    "shyfap.net": function () {
      document.querySelectorAll('.media-card_preview').forEach(card => {
        const videoUrl = card.getAttribute('data-preview');
        const video = card.querySelector('video');
        const img   = card.querySelector('img');

        if (video) {
          video.muted = true;
          video.loop = true;
          video.autoplay = !SETTINGS.isPaused;
          video.playsInline = true;
          video.preload = 'auto';
          video.playbackRate = SETTINGS.playbackSpeed;
          video.style.width = '100%';
          video.style.height = '100%';
          video.style.objectFit = 'cover';
          videoRegistry.add(video);
          if (img) img.style.display = 'none';
          if (SETTINGS.isPaused) video.pause();
        } else if (img && videoUrl) {
          insertPreviewVideo(img, videoUrl);
        }
      });
    },

    "wow.xxx": function () {
      observeElements('.thumb__img[data-preview] img.thumb', img => {
        const container = img.closest('.thumb__img');
        return container ? container.getAttribute('data-preview') : null;
      });
    },

    "pornone.com": function () {
      // pornone.com has NO mp4 previews — it cycles JPG frames from data-thumbs.
      // Frame URL: {data-path}d{thumbNum}.jpg
      // The site's own showImage() cycles every 800ms; we replicate it autonomously,
      // respecting isPaused and mapping playbackSpeed -> interval delay.
      // Speed 1x=800ms, 2x=400ms, 0.5x=1600ms, etc.

      const pnCyclers = new Map(); // img el -> { timer }

      function getIntervalMs() {
        return Math.round(800 / Math.max(0.25, SETTINGS.playbackSpeed));
      }

      function startCycler(img) {
        if (img.dataset.pnInit) return;
        img.dataset.pnInit = '1';

        const path = img.getAttribute('data-path');
        const thumbs = JSON.parse(img.getAttribute('data-thumbs') || '[]');
        if (!path || thumbs.length < 2) return;

        // Preload first few frames
        thumbs.slice(0, 5).forEach(n => { (new Image()).src = path + 'd' + n + '.jpg'; });

        let idx = 1;
        img.dataset.pnIdx = '1';

        const state = { timer: null };
        pnCyclers.set(img, state);

        function tick() {
          if (!SETTINGS.isPaused) {
            img.src = path + 'd' + thumbs[idx] + '.jpg';
            // Preload next
            const next = thumbs[(idx + 1) % thumbs.length];
            (new Image()).src = path + 'd' + next + '.jpg';
            idx = (idx + 1) % thumbs.length;
            img.dataset.pnIdx = String(idx);
          }
          state.timer = setTimeout(tick, getIntervalMs());
        }

        if (!SETTINGS.isPaused) {
          state.timer = setTimeout(tick, getIntervalMs());
        } else {
          // Still schedule, but tick() will skip frame when paused
          state.timer = setTimeout(tick, getIntervalMs());
        }
      }

      // IntersectionObserver: only cycle visible thumbs
      const pnObserver = new IntersectionObserver(entries => {
        entries.forEach(entry => {
          const img = entry.target;
          const state = pnCyclers.get(img);
          if (entry.isIntersecting) {
            startCycler(img);
            // If previously stopped (scrolled away), restart
            if (state && !state.timer) {
              const path = img.getAttribute('data-path');
              const thumbs = JSON.parse(img.getAttribute('data-thumbs') || '[]');
              let idx = parseInt(img.dataset.pnIdx || '1', 10);
              function tick() {
                if (!SETTINGS.isPaused) {
                  img.src = path + 'd' + thumbs[idx] + '.jpg';
                  const next = thumbs[(idx + 1) % thumbs.length];
                  (new Image()).src = path + 'd' + next + '.jpg';
                  idx = (idx + 1) % thumbs.length;
                  img.dataset.pnIdx = String(idx);
                }
                state.timer = setTimeout(tick, getIntervalMs());
              }
              state.timer = setTimeout(tick, getIntervalMs());
            }
          } else {
            // Pause cycling off-screen to save resources
            if (state && state.timer) {
              clearTimeout(state.timer);
              state.timer = null;
            }
          }
        });
      }, { rootMargin: '300px', threshold: 0.01 });

      function watchThumbs() {
        document.querySelectorAll('img.thumbimg[data-thumbs][data-path]').forEach(img => {
          if (!img.dataset.pnObserved) {
            img.dataset.pnObserved = '1';
            pnObserver.observe(img);
          }
        });
      }

      watchThumbs();
      new MutationObserver(watchThumbs).observe(document.body, { childList: true, subtree: true });
      setTimeout(watchThumbs, 500);
      setTimeout(watchThumbs, 2000);
      setInterval(watchThumbs, 5000);
    },

    "tnaflix.com": function () {
      // tnaflix.com provides a direct `data-trailer` mp4 URL on the <a> tag.
      // Selector: a.thumb-chrome[data-trailer]  →  img inside it is the thumbnail.
      // Just grab data-trailer and hand it to insertPreviewVideo — cleanest possible.

      function initCard(anchor) {
        if (anchor.dataset.tnInit) return;
        anchor.dataset.tnInit = '1';

        const url = anchor.getAttribute('data-trailer');
        const img = anchor.querySelector('img');
        if (!url || !img) return;

        insertPreviewVideo(img, url);
      }

      // IntersectionObserver — only load trailers near the viewport
      const tnObserver = new IntersectionObserver(entries => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            initCard(entry.target);
            tnObserver.unobserve(entry.target);
          }
        });
      }, { rootMargin: '400px', threshold: 0.01 });

      function watchCards() {
        document.querySelectorAll('a.thumb-chrome[data-trailer]').forEach(anchor => {
          if (!anchor.dataset.tnObserved) {
            anchor.dataset.tnObserved = '1';
            tnObserver.observe(anchor);
          }
        });
      }

      watchCards();
      new MutationObserver(watchCards).observe(document.body, { childList: true, subtree: true });
      setTimeout(watchCards, 500);
      setTimeout(watchCards, 2000);
      setInterval(watchCards, 5000);
    },

    "pornhits.com": function () {
      // pornhits.com: img.thumb[data-preview] carries the preview URL directly.
      // URL scheme: //pv2.pornhits.com/v2/preview/ID.mp4  (protocol-relative)
      // Just prefix https: and feed to insertPreviewVideo — done.

      observeElements('img.thumb[data-preview]', img => {
        const raw = img.getAttribute('data-preview');
        if (!raw) return null;
        return raw.startsWith('//') ? 'https:' + raw : raw;
      });
    },

    "hqporner.com": function () {
      // hqporner.com has NO mp4 previews — it cycles 10 JPG frames per card.
      // Each img[id^="slide"] has src like: //cdn.../imgs/AA/BB/HASH_main.jpg
      // Frame URLs: //cdn.../imgs/AA/BB/HASH_1.jpg ... HASH_10.jpg  (10 frames)
      // Site cycles at 600ms; we replicate autonomously respecting isPaused/speed.
      // Speed 1x=600ms, 2x=300ms, 0.5x=1200ms etc.
      // The "play images" button just enables slideShowPlayer on click — we bypass
      // it entirely and start cycling immediately on IntersectionObserver trigger.

      const hqCyclers = new Map(); // img el -> { timer }

      function getIntervalMs() {
        return Math.round(600 / Math.max(0.25, SETTINGS.playbackSpeed));
      }

      function getFrameUrls(img) {
        // src: //fastporndelivery.hqporner.com/imgs/AA/BB/HASH_main.jpg
        // OR after "play images" click: HASH_10.jpg
        const src = img.getAttribute('src') || '';
        const base = src.replace(/https?:/, '').replace(/_(?:main|\d+)\.jpg$/, '');
        if (!base || !base.includes('/imgs/')) return null;
        const frames = [];
        for (let i = 1; i <= 10; i++) frames.push('https:' + base + '_' + i + '.jpg');
        return frames;
      }

      function startCycler(img) {
        if (img.dataset.hqInit) return;
        img.dataset.hqInit = '1';

        const frames = getFrameUrls(img);
        if (!frames) return;

        // Preload first 3 frames eagerly
        frames.slice(0, 3).forEach(u => { (new Image()).src = u; });

        let idx = 0;
        const state = { timer: null };
        hqCyclers.set(img, state);

        function tick() {
          if (!SETTINGS.isPaused) {
            img.setAttribute('src', frames[idx]);
            idx = (idx + 1) % frames.length;
            // Preload next
            (new Image()).src = frames[idx];
          }
          state.timer = setTimeout(tick, getIntervalMs());
        }
        state.timer = setTimeout(tick, getIntervalMs());
      }

      function stopCycler(img) {
        const state = hqCyclers.get(img);
        if (state && state.timer) {
          clearTimeout(state.timer);
          state.timer = null;
        }
      }

      function resumeCycler(img) {
        const state = hqCyclers.get(img);
        if (!state || state.timer) return;
        const frames = getFrameUrls(img);
        if (!frames) return;
        let idx = 0;
        function tick() {
          if (!SETTINGS.isPaused) {
            img.setAttribute('src', frames[idx]);
            idx = (idx + 1) % frames.length;
            (new Image()).src = frames[idx];
          }
          state.timer = setTimeout(tick, getIntervalMs());
        }
        state.timer = setTimeout(tick, getIntervalMs());
      }

      const hqObserver = new IntersectionObserver(entries => {
        entries.forEach(entry => {
          const img = entry.target;
          if (entry.isIntersecting) {
            startCycler(img);
            resumeCycler(img);
          } else {
            stopCycler(img);
          }
        });
      }, { rootMargin: '300px', threshold: 0.01 });

      function watchThumbs() {
        document.querySelectorAll('img[id^="slide"]').forEach(img => {
          if (!img.dataset.hqObserved) {
            img.dataset.hqObserved = '1';
            hqObserver.observe(img);
          }
        });
      }

      watchThumbs();
      new MutationObserver(watchThumbs).observe(document.body, { childList: true, subtree: true });
      setTimeout(watchThumbs, 500);
      setTimeout(watchThumbs, 2000);
      setInterval(watchThumbs, 5000);
    },

    "pornmz.com": function () {
      // pornmz.com: data-trailer on <article> carries a direct mp4 URL.
      // The thumbnail img.video-main-thumb sits inside .post-thumbnail-container
      // inside .post-thumbnail inside the <a> inside the <article>.
      // Strategy: find article[data-trailer], grab img.video-main-thumb inside it,
      // feed data-trailer to insertPreviewVideo — identical pattern to tnaflix.

      function initCard(article) {
        if (article.dataset.pmzInit) return;
        article.dataset.pmzInit = '1';

        const url = article.getAttribute('data-trailer');
        const img = article.querySelector('img.video-main-thumb');
        if (!url || !img) return;

        insertPreviewVideo(img, url);
      }

      const pmzObserver = new IntersectionObserver(entries => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            initCard(entry.target);
            pmzObserver.unobserve(entry.target);
          }
        });
      }, { rootMargin: '400px', threshold: 0.01 });

      function watchCards() {
        document.querySelectorAll('article[data-trailer]').forEach(article => {
          if (!article.dataset.pmzObserved) {
            article.dataset.pmzObserved = '1';
            pmzObserver.observe(article);
          }
        });
      }

      watchCards();
      new MutationObserver(watchCards).observe(document.body, { childList: true, subtree: true });
      setTimeout(watchCards, 500);
      setTimeout(watchCards, 2000);
      setInterval(watchCards, 5000);
    }
  };

  // Dispatcher — run immediately AND on DOMContentLoaded as fallback
  function dispatch() {
    for (const domain in siteHandlers) {
      if (hostname.includes(domain)) {
        siteHandlers[domain]();
        break;
      }
    }
  }

  // Run now (for scripts injected after DOM is ready)
  dispatch();

  // Also run on DOMContentLoaded in case we injected before DOM was ready
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', dispatch);
  }

})();