Sleazy Fork is available in English.
Обложки видео всегда активны на всех страницах
// ==UserScript==
// @name R34 Thumbnail always active
// @namespace http://tampermonkey.net/
// @version 2.7
// @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 = 16; // мс (синхронизация с частотой экрана ~1 кадр)
// =================
const ITEM_SELECTOR = '.item.thumb';
const WRAP_SELECTOR = '.img.wrap_image';
const videoPool = new Map();
const trackedVideos = new Set();
const visibilityState = new WeakMap();
const pendingRoots = new Set();
let started = false;
let fixInterval = null;
let processRafId = null;
// Стилизация через CSS предотвращает мигание картинки до прикрепления видео
const style = document.createElement('style');
style.textContent = `
.wrap_image {
position: relative;
}
.wrap_image[data-has-trailer="1"] > img.thumb {
display: none !important;
}
.wrap_image video[our-trailer] {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
pointer-events: none;
background: #000;
}
.wrap_image > :not(img):not(video) {
z-index: 2;
}
`;
document.head.appendChild(style);
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.15 }) : 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 registerVideo(item, video) {
trackedVideos.add(video);
video.lastTime = 0;
video.stuckCheckTime = Date.now();
video.addEventListener('canplay', () => {
if (shouldPlay(item)) video.play().catch(() => {});
}, { once: true });
video.addEventListener('timeupdate', () => {
if (video.currentTime !== video.lastTime) {
video.lastTime = video.currentTime;
video.stuckCheckTime = Date.now();
}
});
video.addEventListener('pause', () => {
if (shouldPlay(item) && video.isConnected) {
setTimeout(() => {
if (video.paused && video.isConnected) {
video.play().catch(() => {});
}
}, 10);
}
});
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;
item.dataset.trailerHoverBlocked = '1';
wrap.querySelectorAll('video').forEach(v => {
if (!v.hasAttribute('our-trailer')) v.remove();
});
let video = wrap.querySelector('video[our-trailer]');
if (video) {
wrap.dataset.hasTrailer = '1';
img.style.display = 'none';
video.style.display = 'block';
if (shouldPlay(item) && video.paused) {
video.play().catch(() => {});
}
return;
}
// Бесшовный перенос: сразу скрываем картинку и монтируем готовое видео
if (videoPool.has(previewUrl)) {
video = videoPool.get(previewUrl);
wrap.dataset.hasTrailer = '1';
img.style.display = 'none';
video.style.display = 'block';
wrap.appendChild(video);
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.autoplay = Boolean(shouldPlay(item));
video.preload = BACKGROUND_PLAY ? 'metadata' : 'none';
videoPool.set(previewUrl, video);
wrap.dataset.hasTrailer = '1';
img.style.display = 'none';
wrap.appendChild(video);
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 (videoPool.has(previewUrl)) {
wrap.dataset.hasTrailer = '1';
const img = wrap.querySelector('img.thumb');
if (img) img.style.display = 'none';
}
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) {
const items = root.querySelectorAll(ITEM_SELECTOR);
for (let i = 0; i < items.length; i++) {
processItem(items[i]);
}
}
}
function flushPendingRoots() {
const roots = Array.from(pendingRoots);
pendingRoots.clear();
for (let i = 0; i < roots.length; i++) {
processRoot(roots[i]);
}
}
function scheduleProcess(root = document) {
pendingRoots.add(root);
// Синхронная обработка для пересозданных контейнеров исключает межфреймовую задержку
if (root instanceof Element && (root.id === 'custom_list_videos_most_recent_videos_items' || root.querySelector?.(ITEM_SELECTOR))) {
flushPendingRoots();
return;
}
if (processRafId) return;
processRafId = requestAnimationFrame(() => {
processRafId = null;
flushPendingRoots();
});
}
function fixStuckVideos() {
if (!started) return;
const now = Date.now();
trackedVideos.forEach(video => {
if (!video.isConnected) return;
const item = video.closest(ITEM_SELECTOR);
if (!item || (!BACKGROUND_PLAY && !shouldPlay(item))) return;
if (video.paused) {
video.play().catch(() => {});
return;
}
if (!video.stuckCheckTime) {
video.stuckCheckTime = now;
return;
}
if (now - video.stuckCheckTime > STUCK_THRESHOLD * 1000 && video.currentTime === video.lastTime) {
video.currentTime = 0;
video.stuckCheckTime = now;
video.play().catch(() => {
video.load();
video.play().catch(() => {});
});
}
});
// Освобождение ресурсов видеокарты (NVDEC) для неиспользуемых потоков
videoPool.forEach((video, url) => {
if (!video.isConnected) {
if (!video._disconnectedAt) {
video._disconnectedAt = now;
} else if (now - video._disconnectedAt > 15000) {
videoPool.delete(url);
trackedVideos.delete(video);
video.pause();
video.removeAttribute('src');
video.load();
}
} else {
video._disconnectedAt = null;
}
});
}
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 target = event.target;
if (target && target !== document && target.closest?.(`${ITEM_SELECTOR}[data-trailer-hover-blocked="1"]`)) {
event.stopPropagation();
}
}, true);
});
new MutationObserver((mutations) => {
if (!started) return;
for (let i = 0; i < mutations.length; i++) {
const mutation = mutations[i];
for (let j = 0; j < mutation.addedNodes.length; j++) {
const node = mutation.addedNodes[j];
if (node.nodeType === 1) {
scheduleProcess(node);
}
}
}
}).observe(document.body, { childList: true, subtree: true });
document.addEventListener('lazyloaded', () => {
if (started) scheduleProcess(document);
}, true);
})();