您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Mutes, blurs and skips ads on YouTube. Speeds up ad playback. Clicks "yes" on "are you there?" on YouTube Music.
// ==UserScript== // @name YouTube Mute and Skip Ads // @namespace https://github.com/ion1/userscripts // @version 0.0.28 // @author ion // @description Mutes, blurs and skips ads on YouTube. Speeds up ad playback. Clicks "yes" on "are you there?" on YouTube Music. // @license MIT // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com // @homepage https://github.com/ion1/userscripts/tree/master/packages/youtube-mute-skip-ads // @homepageURL https://github.com/ion1/userscripts/tree/master/packages/youtube-mute-skip-ads // @match *://www.youtube.com/* // @match *://music.youtube.com/* // @grant GM_addStyle // @run-at document-body // ==/UserScript== (n=>{if(typeof GM_addStyle=="function"){GM_addStyle(n);return}const e=document.createElement("style");e.textContent=n,document.head.append(e)})(` /* Keep these in sync with the watchers. */ #movie_player :is(.ytp-ad-skip-button, .ytp-ad-skip-button-modern, .ytp-skip-ad-button) { anchor-name: --youtube-mute-skip-ads-unclickable-button; } body:has( #movie_player :is( .ytp-ad-skip-button, .ytp-ad-skip-button-modern, .ytp-skip-ad-button ):not([style*="display: none"], [aria-hidden="true"]) )::after { content: "\u{1D606}\u{1D5FC}\u{1D602}\u{1D601}\u{1D602}\u{1D5EF}\u{1D5F2}-\u{1D5FA}\u{1D602}\u{1D601}\u{1D5F2}-\u{1D600}\u{1D5F8}\u{1D5F6}\u{1D5FD}-\u{1D5EE}\u{1D5F1}\u{1D600}\\A\\A" "Unfortunately, YouTube has started to block automated clicks based on isTrusted being false.\\A\\A" "Please click on the skip button manually."; white-space: pre-line; pointer-events: none; z-index: 9999; position: fixed; position-anchor: --youtube-mute-skip-ads-unclickable-button; padding: 1.5em; border-radius: 1.5em; margin-bottom: 1em; bottom: anchor(--youtube-mute-skip-ads-unclickable-button top); right: anchor(--youtube-mute-skip-ads-unclickable-button right); max-width: 25em; font-size: 1.4rem; line-height: 2rem; font-weight: 400; color: rgb(240 240 240); background-color: rgb(0 0 0 / 0.7); backdrop-filter: blur(10px); animation: fade-in 3s linear; } @keyframes fade-in { 0% { opacity: 0; } 67% { opacity: 0; } 100% { opacity: 1; } } #movie_player.ad-showing video { filter: blur(100px) opacity(0.25) grayscale(0.5); } #movie_player.ad-showing .ytp-title, #movie_player.ad-showing .ytp-title-channel, .ytp-visit-advertiser-link, .ytp-ad-visit-advertiser-button, ytmusic-app:has(#movie_player.ad-showing) ytmusic-player-bar :is(.title, .subtitle) { filter: blur(4px) opacity(0.5) grayscale(0.5); transition: 0.05s filter linear; } :is(#movie_player.ad-showing .ytp-title,#movie_player.ad-showing .ytp-title-channel,.ytp-visit-advertiser-link,.ytp-ad-visit-advertiser-button,ytmusic-app:has(#movie_player.ad-showing) ytmusic-player-bar :is(.title,.subtitle)):is(:hover,:focus-within) { filter: none; } /* These popups are showing up on top of the video with a hidden dismiss button * since 2024-09-25. */ .ytp-suggested-action-badge { visibility: hidden !important; } #movie_player.ad-showing .caption-window, .ytp-ad-player-overlay-flyout-cta, .ytp-ad-player-overlay-layout__player-card-container, /* Seen since 2024-04-06. */ .ytp-ad-action-interstitial-slot, /* Added on 2024-08-25. */ ytd-action-companion-ad-renderer, ytd-display-ad-renderer, ytd-ad-slot-renderer, ytd-promoted-sparkles-web-renderer, ytd-player-legacy-desktop-watch-ads-renderer, ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"], ytd-merch-shelf-renderer { filter: blur(10px) opacity(0.25) grayscale(0.5); transition: 0.05s filter linear; } :is(#movie_player.ad-showing .caption-window,.ytp-ad-player-overlay-flyout-cta,.ytp-ad-player-overlay-layout__player-card-container,.ytp-ad-action-interstitial-slot,ytd-action-companion-ad-renderer,ytd-display-ad-renderer,ytd-ad-slot-renderer,ytd-promoted-sparkles-web-renderer,ytd-player-legacy-desktop-watch-ads-renderer,ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"],ytd-merch-shelf-renderer):is(:hover,:focus-within) { filter: none; } .ytp-ad-action-interstitial-background-container /* Added on 2024-08-25. */ { /* An image ad in place of the video. */ filter: blur(10px) opacity(0.25) grayscale(0.5); } `); (function () { 'use strict'; const logPrefix = "youtube-mute-skip-ads:"; class Watcher { name; element; #onCreatedCallbacks; #onRemovedCallbacks; #nodeObserver; #nodeWatchers; #textObserver; #onTextChangedCallbacks; #onAttrChangedCallbacks; visibilityAncestor; #visibilityObserver; #isVisible; #visibilityWatchers; constructor(name, elem) { this.name = name; this.element = null; this.#onCreatedCallbacks = []; this.#onRemovedCallbacks = []; this.#nodeObserver = null; this.#nodeWatchers = []; this.#textObserver = null; this.#onTextChangedCallbacks = []; this.#onAttrChangedCallbacks = []; this.visibilityAncestor = null; this.#visibilityObserver = null; this.#isVisible = null; this.#visibilityWatchers = []; if (elem != null) { this.#connect(elem); } } #assertElement() { if (this.element == null) { throw new Error(`Watcher not connected to an element`); } return this.element; } #assertVisibilityAncestor() { if (this.visibilityAncestor == null) { throw new Error(`Watcher is missing a visibilityAncestor`); } return this.visibilityAncestor; } #connect(element, visibilityAncestor) { if (this.element != null) { if (this.element !== element) { console.error( logPrefix, `Watcher already connected to`, this.element, `while trying to connect to`, element ); } return; } this.element = element; this.visibilityAncestor = visibilityAncestor ?? null; for (const onCreatedCb of this.#onCreatedCallbacks) { const onRemovedCb = onCreatedCb(this.element); if (onRemovedCb) { this.#onRemovedCallbacks.push(onRemovedCb); } } for (const { selector, name, watcher: watcher2 } of this.#nodeWatchers) { for (const descElem of getDescendantsBy(this.element, selector, name)) { watcher2.#connect(descElem, this.element); } } for (const callback of this.#onTextChangedCallbacks) { callback(this.element, this.element.textContent); } for (const { name, callback } of this.#onAttrChangedCallbacks) { callback(this.element, this.element.getAttribute(name)); } this.#registerNodeObserver(); this.#registerTextObserver(); this.#registerAttrObservers(); this.#registerVisibilityObserver(); } #disconnect() { if (this.element == null) { return; } for (const child of this.#nodeWatchers) { child.watcher.#disconnect(); } for (const callback of this.#onTextChangedCallbacks) { callback(this.element, void 0); } for (const { callback } of this.#onAttrChangedCallbacks) { callback(this.element, void 0); } for (const child of this.#visibilityWatchers) { child.#disconnect(); } this.#deregisterNodeObserver(); this.#deregisterTextObserver(); this.#deregisterAttrObservers(); this.#deregisterVisibilityObserver(); while (this.#onRemovedCallbacks.length > 0) { const onRemovedCb = this.#onRemovedCallbacks.shift(); onRemovedCb(); } this.element = null; } #registerNodeObserver() { if (this.#nodeObserver != null) { return; } if (this.#nodeWatchers.length === 0) { return; } const elem = this.#assertElement(); this.#nodeObserver = new MutationObserver((mutations) => { for (const mut of mutations) { for (const node of mut.addedNodes) { for (const { selector, name, watcher: watcher2 } of this.#nodeWatchers) { for (const descElem of getSelfOrDescendantsBy( node, selector, name )) { watcher2.#connect(descElem, elem); } } } for (const node of mut.removedNodes) { for (const { selector, name, watcher: watcher2 } of this.#nodeWatchers) { for (const _descElem of getSelfOrDescendantsBy( node, selector, name )) { watcher2.#disconnect(); } } } } }); this.#nodeObserver.observe(elem, { subtree: true, childList: true }); } #registerTextObserver() { if (this.#textObserver != null) { return; } if (this.#onTextChangedCallbacks.length === 0) { return; } const elem = this.#assertElement(); this.#textObserver = new MutationObserver((_mutations) => { for (const callback of this.#onTextChangedCallbacks) { callback(elem, elem.textContent); } }); this.#textObserver.observe(elem, { subtree: true, // This is needed when elements are replaced to update their text. childList: true, characterData: true }); } #registerAttrObservers() { const elem = this.#assertElement(); for (const handler of this.#onAttrChangedCallbacks) { if (handler.observer != null) { continue; } const { name, callback } = handler; handler.observer = new MutationObserver((_mutations) => { callback(elem, elem.getAttribute(name)); }); handler.observer.observe(elem, { attributes: true, attributeFilter: [name] }); } } #registerVisibilityObserver() { if (this.#visibilityObserver != null) { return; } if (this.#visibilityWatchers.length === 0) { return; } this.#isVisible = false; const elem = this.#assertElement(); const visibilityAncestor = this.#assertVisibilityAncestor(); this.#visibilityObserver = new IntersectionObserver( (entries) => { const oldVisible = this.#isVisible; for (const entry of entries) { this.#isVisible = entry.isIntersecting; } if (this.#isVisible !== oldVisible) { if (this.#isVisible) { for (const watcher2 of this.#visibilityWatchers) { watcher2.#connect(elem, visibilityAncestor); } } else { for (const watcher2 of this.#visibilityWatchers) { watcher2.#disconnect(); } } } }, { root: visibilityAncestor } ); this.#visibilityObserver.observe(elem); } #deregisterNodeObserver() { if (this.#nodeObserver == null) { return; } this.#nodeObserver.disconnect(); this.#nodeObserver = null; } #deregisterTextObserver() { if (this.#textObserver == null) { return; } this.#textObserver.disconnect(); this.#textObserver = null; } #deregisterAttrObservers() { for (const handler of this.#onAttrChangedCallbacks) { if (handler.observer == null) { continue; } handler.observer.disconnect(); handler.observer = null; } } #deregisterVisibilityObserver() { if (this.#visibilityObserver == null) { return; } this.#visibilityObserver.disconnect(); this.#visibilityObserver = null; this.#isVisible = null; } onCreated(onCreatedCb) { this.#onCreatedCallbacks.push(onCreatedCb); if (this.element != null) { const onRemovedCb = onCreatedCb(this.element); if (onRemovedCb) { this.#onRemovedCallbacks.push(onRemovedCb); } } return this; } descendant(selector, name) { const watcher2 = new Watcher(`${this.name} → ${name}`); this.#nodeWatchers.push({ selector, name, watcher: watcher2 }); if (this.element != null) { for (const descElem of getDescendantsBy(this.element, selector, name)) { watcher2.#connect(descElem, this.element); } this.#registerNodeObserver(); } return watcher2; } id(idName) { return this.descendant("id", idName); } klass(className) { return this.descendant("class", className); } tag(tagName) { return this.descendant("tag", tagName); } visible() { const watcher2 = new Watcher(`${this.name} (visible)`); this.#visibilityWatchers.push(watcher2); if (this.element != null) { const visibilityAncestor = this.#assertVisibilityAncestor(); if (this.#isVisible) { watcher2.#connect(this.element, visibilityAncestor); } this.#registerVisibilityObserver(); } return watcher2; } /// `null` implies null textContent. `undefined` implies that the watcher is /// being disconnected. text(callback) { this.#onTextChangedCallbacks.push(callback); if (this.element != null) { callback(this.element, this.element.textContent); this.#registerTextObserver(); } return this; } /// `null` implies no such attribute. `undefined` implies that the watcher is /// being disconnected. attr(name, callback) { this.#onAttrChangedCallbacks.push({ name, callback, observer: null }); if (this.element != null) { callback(this.element, this.element.getAttribute(name)); this.#registerAttrObservers(); } return this; } } function getSelfOrDescendantsBy(node, selector, name) { if (!(node instanceof HTMLElement)) { return []; } if (selector === "id" || selector === "class" || selector === "tag") { if (selector === "id" && node.id === name || selector === "class" && node.classList.contains(name) || selector === "tag" && node.tagName.toLowerCase() === name.toLowerCase()) { return [node]; } else { return getDescendantsBy(node, selector, name); } } else { const impossible = selector; throw new Error(`Impossible selector type: ${JSON.stringify(impossible)}`); } } function getDescendantsBy(node, selector, name) { if (!(node instanceof HTMLElement)) { return []; } let cssSelector = ""; if (selector === "id") { cssSelector += "#"; } else if (selector === "class") { cssSelector += "."; } else if (selector === "tag") ; else { const impossible = selector; throw new Error(`Impossible selector type: ${JSON.stringify(impossible)}`); } cssSelector += CSS.escape(name); return Array.from(node.querySelectorAll(cssSelector)); } const videoSelector = "#movie_player video"; function getVideoElement() { const videoElem = document.querySelector(videoSelector); if (!(videoElem instanceof HTMLVideoElement)) { console.error( logPrefix, "Expected", JSON.stringify(videoSelector), "to be a video element, got:", videoElem?.cloneNode(true) ); return null; } return videoElem; } function callMoviePlayerMethod(name, onSuccess, args) { try { const movieElem = document.getElementById("movie_player"); if (movieElem == null) { console.warn(logPrefix, "movie_player element not found"); return; } const method = Object.getOwnPropertyDescriptor( movieElem, name )?.value; if (method == null) { console.warn( logPrefix, `movie_player element has no ${JSON.stringify(name)} property` ); return; } if (!(typeof method === "function")) { console.warn( logPrefix, `movie_player element property ${JSON.stringify(name)} is not a function` ); return; } const result = method.apply(movieElem, args); if (onSuccess != null) { onSuccess(result); } return result; } catch (e) { console.warn( logPrefix, `movie_player method ${JSON.stringify(name)} failed:`, e ); return; } } function disableVisibilityChecks() { for (const eventName of ["visibilitychange", "blur", "focus"]) { document.addEventListener( eventName, (ev) => { ev.stopImmediatePropagation(); }, { capture: true } ); } document.hasFocus = () => true; Object.defineProperties(document, { visibilityState: { value: "visible" }, hidden: { value: false } }); } function adIsPlaying(_elem) { console.info(logPrefix, "An ad is playing, muting and speeding up"); const video = getVideoElement(); if (video == null) { return; } const onRemovedCallbacks = [ mute(video), speedup(video), cancelPlayback(video) ]; return function onRemoved() { for (const callback of onRemovedCallbacks) { callback(); } }; } function mute(video) { console.debug(logPrefix, "Muting"); video.muted = true; return unmute; } function unmute() { { return; } } function speedup(video) { for (let rate = 16; rate >= 2; rate /= 2) { try { video.playbackRate = rate; break; } catch (e) { console.debug(logPrefix, `Setting playback rate to`, rate, `failed:`, e); } } return function onRemoved() { const originalRate = callMoviePlayerMethod("getPlaybackRate"); if (originalRate == null || typeof originalRate !== "number" || isNaN(originalRate)) { console.warn( logPrefix, `Restoring playback rate failed:`, `unable to query the current playback rate, got: ${JSON.stringify(originalRate)}.`, `Falling back to 1.` ); restorePlaybackRate(video, 1); return; } restorePlaybackRate(video, originalRate); }; } function restorePlaybackRate(video, originalRate) { try { video.playbackRate = originalRate; } catch (e) { console.debug( logPrefix, `Restoring playback rate to`, originalRate, `failed:`, e ); } } function cancelPlayback(video) { let shouldResume = false; function doCancelPlayback() { console.info(logPrefix, "Attempting to cancel playback"); callMoviePlayerMethod("cancelPlayback", () => { shouldResume = true; }); } if (video.paused) { console.debug( logPrefix, "Ad paused, waiting for it to play before canceling playback" ); video.addEventListener("play", doCancelPlayback); } else { doCancelPlayback(); } return function onRemoved() { video.removeEventListener("play", doCancelPlayback); if (shouldResume) { resumePlaybackIfNotAtEnd(); } }; } function resumePlaybackIfNotAtEnd() { const currentTime = callMoviePlayerMethod("getCurrentTime"); const duration = callMoviePlayerMethod("getDuration"); const isAtLiveHead = callMoviePlayerMethod("isAtLiveHead"); if (currentTime == null || duration == null || typeof currentTime !== "number" || typeof duration !== "number" || isNaN(currentTime) || isNaN(duration)) { console.warn( logPrefix, `movie_player methods getCurrentTime/getDuration failed, got time: ${JSON.stringify(currentTime)}, duration: ${JSON.stringify(duration)}` ); return; } if (isAtLiveHead == null || typeof isAtLiveHead !== "boolean") { console.warn( logPrefix, `movie_player method isAtLiveHead failed, got: ${JSON.stringify(isAtLiveHead)}` ); return; } const atEnd = duration - currentTime < 1; if (atEnd && !isAtLiveHead) { console.info( logPrefix, `Video is at the end (${currentTime}/${duration}), not attempting to resume playback` ); return; } console.info(logPrefix, "Attempting to resume playback"); callMoviePlayerMethod("playVideo"); } function click(description) { return (elem) => { if (elem.getAttribute("aria-hidden")) { console.info(logPrefix, "Not clicking (aria-hidden):", description); } else { console.info(logPrefix, "Clicking:", description); elem.click(); } }; } disableVisibilityChecks(); const watcher = new Watcher("body", document.body); const adPlayerOverlayClasses = [ "ytp-ad-player-overlay", "ytp-ad-player-overlay-layout" // Seen since 2024-04-06. ]; for (const adPlayerOverlayClass of adPlayerOverlayClasses) { watcher.klass(adPlayerOverlayClass).onCreated(adIsPlaying); } const adSkipButtonClasses = [ "ytp-ad-skip-button", "ytp-ad-skip-button-modern", // Seen since 2023-11-10. "ytp-skip-ad-button" // Seen since 2024-04-06. ]; for (const adSkipButtonClass of adSkipButtonClasses) { watcher.id("movie_player").klass(adSkipButtonClass).visible().attr("aria-hidden", (elem, value) => { if (value === null) { click(`skip (${adSkipButtonClass})`)(elem); } }); } watcher.klass("ytp-ad-overlay-close-button").visible().onCreated(click("overlay close")); watcher.tag("ytmusic-you-there-renderer").tag("button").visible().onCreated(click("are-you-there")); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址