Обложки видео всегда активны на всех страницах
// ==UserScript==
// @name R34 Thumbnail always active
// @namespace http://tampermonkey.net/
// @version 2.4
// @description Обложки видео всегда активны на всех страницах
// @author Grok
// @match https://rule34video.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=rule34video.com
// @grant none
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// === НАСТРОЙКИ ===
const DELAY_START = 0; // мс
const BACKGROUND_PLAY = 1; // 1 = играть всегда, 0 = только в зоне видимости
const FIX_INTERVAL = 3000; // мс
const STUCK_THRESHOLD = 2; // сек без прогресса -> фикс
const PROCESS_DELAY = 150; // debounce для AJAX/ленивой подгрузки
// =================
const ITEM_SELECTOR = '.item.thumb';
const WRAP_SELECTOR = '.img.wrap_image';
const OVERLAY_SELECTOR = '.sound, .quality, .time, .custom-play';
let started = false;
let fixInterval = null;
let processTimer = null;
let pendingRoots = new Set();
const trackedVideos = new Set();
const visibilityState = new WeakMap();
const visibilityObserver = BACKGROUND_PLAY === 0 ? new IntersectionObserver((entries) => {
entries.forEach(entry => {
const item = entry.target;
visibilityState.set(item, entry.isIntersecting);
const video = item.querySelector('video[our-trailer]');
if (!video) return;
if (entry.isIntersecting) {
video.play().catch(() => {});
} else {
video.pause();
}
});
}, { threshold: 0.35 }) : null;
function isInViewport(el) {
if (!el) return false;
const rect = el.getBoundingClientRect();
return rect.top < window.innerHeight && rect.bottom > 0 &&
rect.left < window.innerWidth && rect.right > 0;
}
function shouldPlay(item) {
if (BACKGROUND_PLAY) return true;
return visibilityState.get(item) ?? isInViewport(item);
}
function blockHoverOnce(item) {
if (item.dataset.trailerHoverBlocked === '1') return;
item.dataset.trailerHoverBlocked = '1';
}
function ensureOverlayZIndex(item) {
if (item.dataset.trailerOverlayFixed === '1') return;
item.dataset.trailerOverlayFixed = '1';
item.querySelectorAll(OVERLAY_SELECTOR).forEach(el => {
el.style.zIndex = '2';
});
}
function registerVideo(item, video) {
trackedVideos.add(video);
video.lastTime = 0;
video.stuckCheckTime = Date.now();
video.addEventListener('canplay', () => {
if (shouldPlay(item)) video.play().catch(() => {});
});
video.addEventListener('timeupdate', () => {
if (video.currentTime > video.lastTime + 0.1) {
video.lastTime = video.currentTime;
video.stuckCheckTime = Date.now();
}
});
video.addEventListener('error', () => {
setTimeout(() => {
if (!video.isConnected) return;
video.load();
if (shouldPlay(item)) video.play().catch(() => {});
}, 1000);
});
}
function activateTrailer(item) {
const wrap = item.querySelector(WRAP_SELECTOR);
const img = wrap?.querySelector('img.thumb');
const previewUrl = wrap?.getAttribute('data-preview');
if (!wrap || !img || !previewUrl) return;
blockHoverOnce(item);
wrap.querySelectorAll('video').forEach(video => {
if (!video.hasAttribute('our-trailer')) {
video.remove();
}
});
let video = wrap.querySelector('video[our-trailer]');
if (video) {
if (video.dataset.previewUrl !== previewUrl) {
video.pause();
video.src = previewUrl;
video.dataset.previewUrl = previewUrl;
video.load();
}
img.style.display = 'none';
video.style.display = 'block';
if (shouldPlay(item) && video.paused) {
video.play().catch(() => {});
}
return;
}
video = document.createElement('video');
video.setAttribute('our-trailer', '1');
video.dataset.previewUrl = previewUrl;
video.src = previewUrl;
video.loop = true;
video.muted = true;
video.playsInline = true;
video.preload = BACKGROUND_PLAY ? 'metadata' : 'none';
video.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;object-fit:cover;z-index:1;display:none;background:#000;pointer-events:none;';
if (getComputedStyle(wrap).position === 'static') {
wrap.style.position = 'relative';
}
wrap.appendChild(video);
img.style.display = 'none';
video.style.display = 'block';
ensureOverlayZIndex(item);
registerVideo(item, video);
if (visibilityObserver) {
visibilityObserver.observe(item);
}
if (shouldPlay(item)) {
video.play().catch(() => {});
}
}
function processItem(item) {
if (!(item instanceof HTMLElement) || !item.matches(ITEM_SELECTOR)) return;
const wrap = item.querySelector(WRAP_SELECTOR);
const previewUrl = wrap?.getAttribute('data-preview');
if (!wrap || !previewUrl) return;
if (item.dataset.trailerPreviewUrl === previewUrl && wrap.querySelector('video[our-trailer]')) {
return;
}
activateTrailer(item);
item.dataset.trailerPreviewUrl = previewUrl;
}
function processRoot(root) {
if (!root) return;
if (root instanceof Element && root.matches(ITEM_SELECTOR)) {
processItem(root);
}
if (root.querySelectorAll) {
root.querySelectorAll(ITEM_SELECTOR).forEach(processItem);
}
}
function flushPendingRoots() {
const roots = Array.from(pendingRoots);
pendingRoots.clear();
roots.forEach(processRoot);
}
function scheduleProcess(root = document) {
pendingRoots.add(root);
if (processTimer) return;
processTimer = setTimeout(() => {
processTimer = null;
flushPendingRoots();
}, PROCESS_DELAY);
}
function fixStuckVideos() {
if (!started) return;
trackedVideos.forEach(video => {
if (!video.isConnected) {
trackedVideos.delete(video);
return;
}
const item = video.closest(ITEM_SELECTOR);
if (!item) return;
if (!BACKGROUND_PLAY && !shouldPlay(item)) {
return;
}
if (video.ended || video.paused) {
if (shouldPlay(item)) video.play().catch(() => {});
return;
}
const now = Date.now();
if (!video.stuckCheckTime) {
video.stuckCheckTime = now;
return;
}
if (now - video.stuckCheckTime > STUCK_THRESHOLD * 1000 && video.currentTime === video.lastTime) {
video.currentTime = 0;
video.load();
setTimeout(() => {
if (video.isConnected && shouldPlay(item)) {
video.play().catch(() => {});
}
}, 100);
}
});
}
function start() {
if (started) return;
started = true;
scheduleProcess(document);
fixInterval = setInterval(fixStuckVideos, FIX_INTERVAL);
}
setTimeout(start, DELAY_START);
['scroll', 'click', 'keydown'].forEach(eventName => {
document.addEventListener(eventName, start, { once: true, passive: true });
});
['mouseenter', 'mouseover', 'mouseleave', 'mouseout'].forEach(eventName => {
document.addEventListener(eventName, (event) => {
const item = event.target instanceof Element ? event.target.closest(`${ITEM_SELECTOR}[data-trailer-hover-blocked="1"]`) : null;
if (item) event.stopPropagation();
}, true);
});
new MutationObserver((mutations) => {
if (!started) return;
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType === 1) {
scheduleProcess(node);
}
}
}
}).observe(document.body, { childList: true, subtree: true });
document.addEventListener('lazyloaded', () => {
if (started) scheduleProcess(document);
}, true);
console.log('R34 Trailer Always Active v2.4 optimized');
})();