Auto-play video thumbnails with playback speed control and pause functionality on adult sites
// ==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);
}
})();