您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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== // @name Restore animated thumbnail previews - youtube.com // @namespace Violentmonkey Scripts seekhare // @match *://www.youtube.com/* // @run-at document-start // @grant GM_addStyle // @version 4.3 // @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 forceDisableCarouselFallback = false; // force disable the carousel fallback on new home/subscription pages where an_webp is unavailable. const forceEnableCarouselFallback = 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); //Below are some exceptions where we don't want to apply the carousel fallback and can't except these in the mutation observer as child elements are not present then. 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, as these have an_webp available. target.removeEventListener('mouseenter', animatedThumbsEventEnter); target.removeEventListener('mouseleave', animatedThumbsEventLeave); return false } else if (target.querySelector('badge-shape.badge-shape-wiz--thumbnail-live') != null) { // don't apply to video tiles that are live. 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. 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 by YT as these have the an_webp thumbs available, these videos have a crossed out play icon SVG but otherwise no other identifier hence the strange selector. 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 carouselImgNode = document.createElement("img"); carouselImgNode.setAttribute('videoId', atag.getAttribute('videoId')); carouselImgNode.setAttribute("carouselIndex", 0); carouselImgNode.setAttribute("id", "thumbnail"); carouselImgNode.setAttribute("class", "style-scope ytd-moving-thumbnail-renderer fade-in animatedThumbTarget"); //animatedThumbTarget is custom class, others are Youtube updateCarousel(carouselImgNode); 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(carouselImgNode); carouselImgNode.timer = setInterval(updateCarousel, carouselDelay, carouselImgNode); 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 enableCarouselFallbackCheck() { if (forceDisableCarouselFallback) { return false } else if (forceEnableCarouselFallback) { return true } //2025-08-02 - Check no longer reliable as YT can disable video inlines on some videos so may be one or two an_webp avaiable but most videos without, therefore have set forceEnableCarouselFallback to 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 (enableCarouselFallbackCheck() === false) { return console.log(logHeader, "Using an_webp old method only, disabling carousel fallback.") } console.log(logHeader, "Enabling carousel fallback where an_webp not available.") 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() });