R34 Thumbnail always active

Обложки видео всегда активны на всех страницах

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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);
})();