Restore animated thumbnail previews - youtube.com

To restore animated thumbnail previews. Requires inline video previews to be disabled in your YouTube user settings (Go to https://www.youtube.com/account_playback and set "video previews" to disabled). Not Greasemonkey compatible. v4 Add new carousel fallback for Youtube's new homepage UI.

目前為 2025-08-02 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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     4.2
// @license     MIT
// @author      seekhare
// @description To restore animated thumbnail previews. Requires inline video previews to be disabled in your YouTube user settings (Go to https://www.youtube.com/account_playback and set "video previews" to disabled). Not Greasemonkey compatible. v4 Add new carousel fallback for Youtube's new homepage UI.
// ==/UserScript==
const logHeader = 'UserScript Restore YT Animated Thumbs:';
console.log(logHeader, "enabled.")
Object.defineProperties(Object.prototype,{isPreviewDisabled:{get:function(){return false}, set:function(){}}}); // original method

//2025-07-12 added animatedThumbnailEnabled & inlinePreviewEnabled for new sidebar UI on watch page.
Object.defineProperties(Object.prototype,{animatedThumbnailEnabled:{get:function(){return true}, set:function(){}}});
Object.defineProperties(Object.prototype,{inlinePreviewEnabled:{get:function(){return false}, set:function(){}}});

//2025-07-28 Don't enable the below as seems to break things but I'm leaving here in case of future Youtube change, for reference if needed in future fixes.
//Object.defineProperties(Object.prototype,{isInlinePreviewEnabled:{get:function(){return true}, set:function(){return true}}});
//Object.defineProperties(Object.prototype,{isInlinePreviewDisabled:{get:function(){return true}, set:function(){return true}}});
//Object.defineProperties(Object.prototype,{inlinePreviewIsActive:{get:function(){return false}, set:function(){}}});
//Object.defineProperties(Object.prototype,{inlinePreviewIsEnabled:{get:function(){return false}, set:function(){}}});


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 = true; // force disable takes priority over force enable.
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 = 500; //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('badge-shape.badge-shape-wiz--thumbnail-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 false
    }
    else if (target.querySelector('ytd-rich-grid-media') != null) { // don't apply to old media grid tiles for users with homepage or subscription page still on old version, can't do in observer as child element not present then.
        target.removeEventListener('mouseenter', animatedThumbsEventEnter);
        target.removeEventListener('mouseleave', animatedThumbsEventLeave);
        return false
    }
    else if (target.querySelector('ytm-shorts-lockup-view-model') != null) { // don't apply to shorts tiles, can't do in observer as child element not present then.
        target.removeEventListener('mouseenter', animatedThumbsEventEnter);
        target.removeEventListener('mouseleave', animatedThumbsEventLeave);
        return false
    }
    else if (target.querySelector('path[d="M2.81,2.81L1.39,4.22L8,10.83V19l4.99-3.18l6.78,6.78l1.41-1.41L2.81,2.81z M10,15.36v-2.53l1.55,1.55L10,15.36z"]') != null) { // don't apply to video tiles that have inline videos disabled as these have the an_webp thumbs available, can't do in observer as child element not present then.
        target.removeEventListener('mouseenter', animatedThumbsEventEnter);
        target.removeEventListener('mouseleave', animatedThumbsEventLeave);
        return false
    }

    var atag = target.querySelector('a');
    //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.yt-thumbnail-view-model__image');
    if (overlaytag == null) {
        target.removeEventListener('mouseenter', animatedThumbsEventEnter);
        target.removeEventListener('mouseleave', animatedThumbsEventLeave);
        return false
    }
    overlaytag.appendChild(animatedImgNode);
    animatedImgNode.timer = setInterval(updateCarousel, carouselDelay, animatedImgNode);
    return true
}
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()
});