Restore animated thumbnail previews - youtube.com

To restore animated thumbnail previews, requires inline previews to be disabled in your YouTube settings. Note: not Greasemonkey compatible. v2.0 adds fallback option of a still image carousel for the homepage where YouTube has changed their data structure to no longer provide an_webp animated thumbnail urls, which is affecting some users; for such users non-homepage pages are unaffected and continue to use the an_webp animated thumbs.

当前为 2025-02-12 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Restore animated thumbnail previews - youtube.com
// @namespace   Violentmonkey Scripts seekhare
// @match       http*://www.youtube.com/*
// @run-at      document-start
// @grant       GM_addStyle
// @version     2.0
// @license     MIT
// @author      seekhare
// @description To restore animated thumbnail previews, requires inline previews to be disabled in your YouTube settings. Note: not Greasemonkey compatible. v2.0 adds fallback option of a still image carousel for the homepage where YouTube has changed their data structure to no longer provide an_webp animated thumbnail urls, which is affecting some users; for such users non-homepage pages are unaffected and continue to use the an_webp animated thumbs.
// ==/UserScript==

Object.defineProperties(Object.prototype,{isPreviewDisabled:{get:function(){return false}, set:function(){}}}); // old way preferred and none-the-less still valid for non-homepage pages for affected users.

fadeInCSS = `img.animatedThumbTarget { animation: fadeIn 0.5s; object-fit: cover;}
@keyframes fadeIn {
  0% { opacity: 0; }
  100% { opacity: 1; }
}
`;
GM_addStyle(fadeInCSS);

const forceDisableNewHomepageMethod = false; // force disable the new homepage method.
const forceEnableNewHomepageMethod = false; // force disable takes priority over force enable.
const logHeader = 'UserScript Restore YT Animated Thumbs:';
const homeUrl = 'https://www.youtube.com/';
const ytImageBaseUrl = 'https://i.ytimg.com/vi/';
const ytImageNames = ['hq1.jpg', 'hq2.jpg', 'hq3.jpg']; // e.g. https://i.ytimg.com/vi/UujGYE5mOnI/0.jpg
const carouselDelay = 850; //milliseconds, how long to display each image.

function animatedThumbsEventEnter(event) {
    //console.debug(logHeader, 'enter', event);
    var target = event.target;
    //console.debug(logHeader, 'target', target);
    if (target.querySelector('div[aria-label="LIVE"]') != null) { // don't apply to video tiles that are live, can't do in observer as child element not present then.
        target.removeEventListener('mouseenter', animatedThumbsEventEnter);
        target.removeEventListener('mouseleave', animatedThumbsEventLeave);
        return
    }
    var atag = target.querySelector('a#thumbnail');
    //console.debug(logHeader, 'atag', atag);
    if (atag.hasAttribute('videoId') === false) {
        //extract videoId from href and store on an attribute
        var videoId = atag.getAttribute('href').match(/watch\?v=([^&]*)/)[1]; //the href is like "/watch?v=IDabc123&t=123" so regex.
        //console.debug(logHeader, 'videoId', videoId);
        atag.setAttribute('videoId', videoId);
    }

    var animatedImgNode = document.createElement("img");
    animatedImgNode.setAttribute('videoId', atag.getAttribute('videoId'));
    animatedImgNode.setAttribute("carouselIndex", 0);
    animatedImgNode.setAttribute("id", "thumbnail");
    animatedImgNode.setAttribute("class", "style-scope ytd-moving-thumbnail-renderer fade-in animatedThumbTarget"); //animatedThumbTarget is custom class, others are Youtube
    updateCarousel(animatedImgNode);
    var overlaytag = target.querySelector('div#mouseover-overlay');
    overlaytag.appendChild(animatedImgNode);
    animatedImgNode.timer = setInterval(updateCarousel, carouselDelay, animatedImgNode);
    return
}
function animatedThumbsEventLeave(event) {
    //console.debug(logHeader, 'leave', event);
    try {
        var animatedImgNode = event.target.querySelector('img.animatedThumbTarget');
        clearTimeout(animatedImgNode.timer);
        animatedImgNode.remove();
    } catch {}
    return
}
function updateCarousel(animatedImgNode) {
    var index = parseInt(animatedImgNode.getAttribute("carouselIndex"));
    //console.debug(logHeader, 'index', index);
    var imgURL = ytImageBaseUrl + animatedImgNode.getAttribute('videoId') + '/' + ytImageNames[index];
    animatedImgNode.setAttribute("src", imgURL);
    var nextIndex = (index+1) % ytImageNames.length;
    animatedImgNode.setAttribute("carouselIndex", nextIndex);
}

function useNewSearchMethod() {
    if (forceDisableNewHomepageMethod) {
        return false
    } else if (forceEnableNewHomepageMethod) {
        return true
    }
    if (window.location.pathname  === '/') {
        console.debug(logHeader, 'Pathname check method');
        if (document.head.innerHTML.indexOf('an_webp') != -1 || document.body.innerHTML.indexOf('an_webp') != -1) {
            return false
        } 
        else {
            return true
        }
    } else {
        // if not entered youtube via homepage then do a request here to determine if user's homepage is affected by YouTube's changes removing animated thumbs.
        console.debug(logHeader, 'XMLHttpRequest check method');
        const request = new XMLHttpRequest();
        request.open("GET", homeUrl, false); // `false` makes the request synchronous
        request.send(null);

        if (request.status === 200) {
            //console.debug('response', request.responseText);
            var trimmedResponseIndex = request.responseText.indexOf('an_webp/');
            if (trimmedResponseIndex != -1) {
                return false
            }
            else {
                return true
            }
        } else {
            console.error(logHeader, 'Could not GET "'+homeUrl+'". Response Status = '+request.status, request.statusText);
            return true
        }
    }
}
function runPageCheckForExistingElements() {
    //Can run this just incase some elements were already created before observer set up.
    var list = document.getElementsByTagName("ytd-rich-item-renderer");
    for (var element of list) {
        //console.debug(logHeader, element);
        element.addEventListener('mouseenter', animatedThumbsEventEnter);
        element.addEventListener('mouseleave', animatedThumbsEventLeave);
    }
}
function setupMutationObserverSingle() {
    if (useNewSearchMethod() === false) {
        return console.log(logHeader, "Using old method only (preferred), disabling new method.")
    } 
    console.log(logHeader, "Enabling new image method for homepage.")
    const targetNode = document;
    //console.debug('targetNodeInit',targetNode);
    const config = {attributes: false, childList: true, subtree: true};
    const callback = (mutationList, observer) => {
        for (const mutation of mutationList) {
            //console.debug(logHeader, "Mutation", mutation);
            for (const element of mutation.addedNodes) {
                if (element.nodeName === 'YTD-RICH-ITEM-RENDERER') {
                    //console.debug(logHeader, "Adding event listeners to element", element);
                    element.addEventListener('mouseenter', animatedThumbsEventEnter);
                    element.addEventListener('mouseleave', animatedThumbsEventLeave);
                }
            }
        }
    }
    const observer = new MutationObserver(callback);
    observer.observe(targetNode, config);
    runPageCheckForExistingElements();
}
document.addEventListener("DOMContentLoaded", function(){
    setupMutationObserverSingle()
});