// ==UserScript==
// @name YouTube Mute and Skip Ads
// @namespace https://github.com/ion1/userscripts
// @version 0.0.9
// @author ion
// @description Mutes, blurs and skips ads on YouTube. Reloads the page (maintaining the position in the video) if unskippable ads are too long. 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/*
// @run-at document-body
// ==/UserScript==
(n=>{const e=document.createElement("style");e.dataset.source="vite-plugin-monkey",e.innerText=n,document.head.appendChild(e)})(` #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-ad-visit-advertiser-button {
filter: blur(4px) opacity(0.5) grayscale(0.5);
}
@media (prefers-reduced-motion: no-preference) {
#movie_player.ad-showing .ytp-title,
#movie_player.ad-showing .ytp-title-channel,
.ytp-ad-visit-advertiser-button {
transition: 0.05s filter linear;
}
}
:is(#movie_player.ad-showing .ytp-title,#movie_player.ad-showing .ytp-title-channel,.ytp-ad-visit-advertiser-button):hover,
:is(#movie_player.ad-showing .ytp-title,#movie_player.ad-showing .ytp-title-channel,.ytp-ad-visit-advertiser-button):focus-within {
filter: none;
}
#movie_player.ad-showing .caption-window,
.ytp-ad-player-overlay-flyout-cta,
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 {
filter: blur(10px) opacity(0.25) grayscale(0.5);
}
@media (prefers-reduced-motion: no-preference) {
#movie_player.ad-showing .caption-window,
.ytp-ad-player-overlay-flyout-cta,
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 {
transition: 0.05s filter linear;
}
}
:is(#movie_player.ad-showing .caption-window,.ytp-ad-player-overlay-flyout-cta,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):hover,
:is(#movie_player.ad-showing .caption-window,.ytp-ad-player-overlay-flyout-cta,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):focus-within {
filter: none;
}
.youtube-mute-skip-ads-notification {
pointer-events: none;
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
animation: none;
}
@media (prefers-reduced-motion: no-preference) {
.youtube-mute-skip-ads-notification.fade-out {
animation: youtube-mute-skip-ads-fade-out 3s;
animation-fill-mode: forwards;
}
}
.youtube-mute-skip-ads-notification > div {
font-size: 3rem;
line-height: 1.3;
text-align: center;
background-color: rgb(0 0 0 / 0.7);
color: white;
padding: 2rem;
border-radius: 1rem;
}
.youtube-mute-skip-ads-notification > div > :first-child {
font-size: 6rem;
}
@keyframes youtube-mute-skip-ads-fade-out {
0% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
opacity: 0;
}
}
`);
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => {
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
(function() {
"use strict";
const main = "";
class Watcher {
constructor(name, elem) {
__publicField(this, "name");
__publicField(this, "element");
__publicField(this, "onCreated");
__publicField(this, "onRemoved");
__publicField(this, "nodeObserver");
__publicField(this, "nodeWatchers");
__publicField(this, "textObserver");
__publicField(this, "onTextChanged");
__publicField(this, "visibilityAncestor");
__publicField(this, "visibilityObserver");
__publicField(this, "isVisible");
__publicField(this, "visibilityWatchers");
this.name = name;
this.element = null;
this.onCreated = [];
this.onRemoved = [];
this.nodeObserver = null;
this.nodeWatchers = [];
this.textObserver = null;
this.onTextChanged = [];
this.visibilityAncestor = null;
this.visibilityObserver = null;
this.isVisible = null;
this.visibilityWatchers = [];
if (!!elem) {
this.connect(elem);
}
}
assertElement() {
if (!this.element) {
throw new Error(`Watcher not connected to an element`);
}
return this.element;
}
assertVisibilityAncestor() {
if (!this.visibilityAncestor) {
throw new Error(`Watcher is missing a visibilityAncestor`);
}
return this.visibilityAncestor;
}
isConnected() {
return this.element != null;
}
connect(element, visibilityAncestor) {
if (!!this.element) {
if (this.element !== element) {
console.error(
`Watcher already connected to`,
this.element,
`while trying to connect to`,
element
);
}
return;
}
this.element = element;
this.visibilityAncestor = visibilityAncestor ?? null;
for (const f of this.onCreated) {
f(this.element);
}
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 f of this.onTextChanged) {
f(this.element.textContent);
}
this.registerNodeObserver();
this.registerTextObserver();
this.registerVisibilityObserver();
}
disconnect() {
if (!this.element) {
return;
}
for (const child of this.nodeWatchers) {
child.watcher.disconnect();
}
for (const f of this.onTextChanged) {
f(null);
}
for (const child of this.visibilityWatchers) {
child.disconnect();
}
this.deregisterNodeObserver();
this.deregisterTextObserver();
this.deregisterVisibilityObserver();
for (const f of this.onRemoved) {
f(this.element);
}
this.element = null;
}
registerNodeObserver() {
if (!!this.nodeObserver) {
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) {
return;
}
if (this.onTextChanged.length === 0) {
return;
}
const elem = this.assertElement();
this.textObserver = new MutationObserver((_mutations) => {
for (const f of this.onTextChanged) {
f(elem.textContent);
}
});
this.textObserver.observe(elem, {
subtree: true,
// This is needed when elements are replaced to update their text.
childList: true,
characterData: true
});
}
registerVisibilityObserver() {
if (!!this.visibilityObserver) {
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) {
return;
}
this.nodeObserver.disconnect();
this.nodeObserver = null;
}
deregisterTextObserver() {
if (!this.textObserver) {
return;
}
this.textObserver.disconnect();
this.textObserver = null;
}
deregisterVisibilityObserver() {
if (!this.visibilityObserver) {
return;
}
this.visibilityObserver.disconnect();
this.visibilityObserver = null;
this.isVisible = null;
}
lifecycle(onCreated, onRemoved) {
this.onCreated.push(onCreated);
if (!!onRemoved) {
this.onRemoved.push(onRemoved);
}
if (!!this.element) {
onCreated(this.element);
}
return this;
}
descendant(selector, name) {
const watcher2 = new Watcher(`${this.name} → ${name}`);
this.nodeWatchers.push({ selector, name, watcher: watcher2 });
if (!!this.element) {
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) {
const visibilityAncestor = this.assertVisibilityAncestor();
if (!!this.isVisible) {
watcher2.connect(this.element, visibilityAncestor);
}
this.registerVisibilityObserver();
}
return watcher2;
}
text(f) {
this.onTextChanged.push(f);
if (!!this.element) {
f(this.element.textContent);
this.registerTextObserver();
}
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 notification = "";
let visibleElem = null;
let hideTimerId = null;
function showNotification(params) {
const containerElem = document.createElement("div");
containerElem.setAttribute("class", "youtube-mute-skip-ads-notification");
const notifElem = document.createElement("div");
notifElem.setAttribute("aria-live", "assertive");
notifElem.setAttribute("aria-atomic", "true");
containerElem.appendChild(notifElem);
const headerElem = document.createElement("div");
headerElem.textContent = params.heading;
notifElem.appendChild(headerElem);
if (params.description != null) {
const descrElem = document.createElement("div");
descrElem.textContent = params.description;
notifElem.appendChild(descrElem);
}
const footerElem = document.createElement("div");
footerElem.textContent = "(Youtube Mute and Skip Ads)";
notifElem.appendChild(footerElem);
if (hideTimerId != null) {
clearTimeout(hideTimerId);
hideTimerId = null;
}
if (visibleElem != null) {
document.body.removeChild(visibleElem);
visibleElem = null;
}
document.body.append(containerElem);
visibleElem = containerElem;
if (params.fadeOut) {
containerElem.classList.add("fade-out");
hideTimerId = setTimeout(() => {
document.body.removeChild(containerElem);
visibleElem = null;
hideTimerId = null;
}, 3e3);
}
}
const logPrefix = "youtube-mute-skip-ads:";
const adMaxTime = 7;
const notificationKey = "youtube-mute-skip-ads-notification";
const restoreFocusKey = "youtube-mute-skip-ads-restore-focus";
const playerId = "movie_player";
const videoSelector = "#movie_player video";
const muteButtonClass = "ytp-mute-button";
const adState = {
reloading: false,
adCounter: null,
adDurationRemaining: null,
hasPreskip: false,
preskipRemaining: null
};
function reloadNotification(description) {
showNotification({ heading: "⟳ Reloading", description });
sessionStorage.setItem(notificationKey, description);
}
function reloadedNotification() {
const description = sessionStorage.getItem(notificationKey);
sessionStorage.removeItem(notificationKey);
if (description != null) {
showNotification({ heading: "✓ Reloaded", description, fadeOut: true });
}
}
reloadedNotification();
function storeFocusState() {
var _a;
const id = (_a = document.activeElement) == null ? void 0 : _a.id;
if (id != null && id !== "") {
sessionStorage.setItem(restoreFocusKey, id);
}
}
let focusElementId = sessionStorage.getItem(restoreFocusKey);
sessionStorage.removeItem(restoreFocusKey);
function restoreFocusState(elem) {
if (focusElementId == null) {
return;
}
console.info(logPrefix, "Restoring focus to", JSON.stringify(elem.id));
elem.focus();
focusElementId = null;
}
function setVideoProperties(props) {
const video = document.querySelector(videoSelector);
if (!(video instanceof HTMLVideoElement)) {
console.error(
logPrefix,
"Expected",
JSON.stringify(videoSelector),
"to be a video element, got:",
video == null ? void 0 : video.cloneNode(true)
);
return;
}
if (props.muted != null) {
video.muted = props.muted;
}
}
function toggleMuteTwice() {
for (const elem of document.getElementsByClassName(muteButtonClass)) {
if (!(elem instanceof HTMLElement)) {
console.error(
logPrefix,
"Expected",
JSON.stringify(muteButtonClass),
"to be an HTML element, got:",
elem.cloneNode(true)
);
continue;
}
elem.click();
elem.click();
return;
}
console.error(logPrefix, "Failed to find", JSON.stringify(muteButtonClass));
}
function adUIAdded(_elem) {
console.info(logPrefix, "An ad is playing, muting");
setVideoProperties({ muted: true });
}
function adUIRemoved(_elem) {
console.info(logPrefix, "An ad is no longer playing, unmuting");
toggleMuteTwice();
}
function reloadPage(description) {
const playerElem = document.getElementById(playerId);
if (playerElem == null) {
console.error(
logPrefix,
"Expected",
JSON.stringify(playerId),
"to be a player element, got:",
playerElem
);
return;
}
if (!("getCurrentTime" in playerElem)) {
console.error(
logPrefix,
"The player element doesn't have getCurrentTime:",
playerElem.cloneNode(true)
);
return;
}
if (typeof playerElem.getCurrentTime !== "function") {
console.error(
logPrefix,
"getCurrentTime is not a function:",
playerElem.getCurrentTime
);
return;
}
const currentTime = playerElem.getCurrentTime();
if (typeof currentTime !== "number") {
console.error(
logPrefix,
"Expected a number, getCurrentTime returned:",
currentTime
);
return;
}
reloadNotification(description);
storeFocusState();
adState.reloading = true;
var searchParams = new URLSearchParams(window.location.search);
searchParams.set("t", `${Math.floor(currentTime)}s`);
console.info(logPrefix, "Reloading with t =", searchParams.get("t"));
window.location.search = searchParams.toString();
}
function maybeReloadPage() {
if (adState.reloading) {
return;
}
if (!!adState.adCounter && !adState.adCounter.parsed.includes(1)) {
console.info(logPrefix, "Ad counter exceeds 1, reloading page");
reloadPage(`Reason: ad counter: ${adState.adCounter.text}`);
return;
}
if (!!adState.adDurationRemaining && adState.hasPreskip) {
let time = adState.adDurationRemaining.parsed;
if (!!adState.preskipRemaining) {
time = Math.min(time, adState.preskipRemaining.parsed);
}
if (time > adMaxTime) {
console.info(
logPrefix,
"Ad duration remaining exceeds maximum, reloading page:",
time,
">",
adMaxTime
);
reloadPage(`Reason: ad duration: ${time} s`);
return;
}
}
}
function adCounterUpdated(fullText) {
var _a;
const text = ((_a = fullText == null ? void 0 : fullText.match(/[0-9](?:.*[0-9])?/)) == null ? void 0 : _a[0]) ?? null;
if (text == null) {
adState.adCounter = null;
return;
}
const counter = (text.match(/[0-9]+/g) ?? []).map(Number);
if (counter.length === 0) {
adState.adCounter = null;
return;
}
adState.adCounter = { text, parsed: counter };
maybeReloadPage();
}
function durationRemainingUpdated(fullText) {
const match = fullText == null ? void 0 : fullText.match(/^(?:(?:([0-9]+):)?([0-9]+):)?([0-9]+)$/);
if (match == null) {
adState.adDurationRemaining = null;
return;
}
const text = match[0];
const h = Number(match[1] ?? 0);
const m = Number(match[2] ?? 0);
const s = Number(match[3] ?? 0);
const time = (h * 60 + m) * 60 + s + 1;
adState.adDurationRemaining = { text, parsed: time };
maybeReloadPage();
}
function preskipUpdated(fullText) {
var _a;
adState.hasPreskip = fullText != null;
const text = (_a = fullText == null ? void 0 : fullText.match(/^[^0-9]*([0-9]+)[^0-9]*$/)) == null ? void 0 : _a[1];
if (text == null) {
adState.preskipRemaining = null;
} else {
adState.preskipRemaining = { text, parsed: Number(text) };
}
maybeReloadPage();
}
function click(description) {
return (elem) => {
console.info(logPrefix, "Clicking:", description);
elem.click();
};
}
const watcher = new Watcher("body", document.body);
if (focusElementId != null) {
watcher.id(focusElementId).lifecycle(restoreFocusState);
}
const playerOverlay = watcher.klass("ytp-ad-player-overlay").lifecycle(adUIAdded, adUIRemoved);
playerOverlay.klass("ytp-ad-simple-ad-badge").text(adCounterUpdated);
playerOverlay.klass("ytp-ad-duration-remaining").text(durationRemainingUpdated);
playerOverlay.klass("ytp-ad-preview-text").text(preskipUpdated);
playerOverlay.klass("ytp-ad-skip-button").visible().lifecycle(click("skip"));
watcher.klass("ytp-ad-overlay-close-button").lifecycle(click("overlay close"));
watcher.tag("ytmusic-you-there-renderer").tag("button").lifecycle(click("are-you-there"));
})();