// ==UserScript==
// @name YouTube: Hide Watched Videos
// @namespace https://www.haus.gg/
// @version 6.9
// @license MIT
// @description Hides watched videos (and shorts) from your YouTube subscriptions page.
// @author Ev Haus
// @author netjeff
// @author actionless
// @match http://*.youtube.com/*
// @match http://youtube.com/*
// @match https://*.youtube.com/*
// @match https://youtube.com/*
// @noframes
// @require https://openuserjs.org/src/libs/sizzle/GM_config.js
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
// To submit bugs or submit revisions please see visit the repository at:
// https://github.com/EvHaus/youtube-hide-watched
// You can open new issues at:
// https://github.com/EvHaus/youtube-hide-watched/issues
((_undefined) => {
// Enable for debugging
const DEBUG = false;
// Needed to bypass YouTube's Trusted Types restrictions, ie.
// Uncaught TypeError: Failed to set the 'innerHTML' property on 'Element': This document requires 'TrustedHTML' assignment.
if (
typeof trustedTypes !== "undefined" &&
trustedTypes.defaultPolicy === null
) {
const s = (s) => s;
trustedTypes.createPolicy("default", {
createHTML: s,
createScriptURL: s,
createScript: s,
});
}
// GM_config setup
const title = document.createElement("a");
title.textContent = "YouTube: Hide Watched Videos Settings";
title.href = "https://github.com/EvHaus/youtube-hide-watched";
title.target = "_blank";
const gmc = new GM_config({
events: {
save() {
this.close();
},
},
fields: {
HIDDEN_THRESHOLD_PERCENT: {
default: 10,
label: "Hide/Dim Videos Above Percent",
max: 100,
min: 0,
type: "int",
},
},
id: "YouTubeHideWatchedVideos",
title,
});
// Set defaults
localStorage.YTHWV_WATCHED = localStorage.YTHWV_WATCHED || "false";
const logDebug = (...msgs) => {
if (DEBUG) console.debug("[YT-HWV]", msgs);
};
// GreaseMonkey no longer supports GM_addStyle. So we have to define
// our own polyfill here
const addStyle = (aCss) => {
const head = document.getElementsByTagName("head")[0];
if (head) {
const style = document.createElement("style");
style.setAttribute("type", "text/css");
style.textContent = aCss;
head.appendChild(style);
return style;
}
return null;
};
addStyle(`
.YT-HWV-WATCHED-HIDDEN { display: none !important }
.YT-HWV-WATCHED-DIMMED { opacity: 0.3 }
.YT-HWV-SHORTS-HIDDEN { display: none !important }
.YT-HWV-SHORTS-DIMMED { opacity: 0.3 }
.YT-HWV-HIDDEN-ROW-PARENT { padding-bottom: 10px }
.YT-HWV-BUTTONS {
background: transparent;
border: 1px solid var(--ytd-searchbox-legacy-border-color);
border-radius: 40px;
display: flex;
gap: 5px;
margin: 0 20px;
}
.YT-HWV-BUTTON {
align-items: center;
background: transparent;
border: 0;
border-radius: 40px;
color: var(--yt-spec-icon-inactive);
cursor: pointer;
display: flex;
height: 40px;
justify-content: center;
outline: 0;
width: 40px;
}
.YT-HWV-BUTTON:focus,
.YT-HWV-BUTTON:hover {
background: var(--yt-spec-badge-chip-background);
}
.YT-HWV-BUTTON-DISABLED { color: var(--yt-spec-icon-disabled) }
.YT-HWV-MENU {
background: #F8F8F8;
border: 1px solid #D3D3D3;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.05);
display: none;
font-size: 12px;
margin-top: -1px;
padding: 10px;
position: absolute;
right: 0;
text-align: center;
top: 100%;
white-space: normal;
z-index: 9999;
}
.YT-HWV-MENU-ON { display: block; }
.YT-HWV-MENUBUTTON-ON span { transform: rotate(180deg) }
`);
const BUTTONS = [
{
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 48 48"><path fill="currentColor" d="M24 9C14 9 5.46 15.22 2 24c3.46 8.78 12 15 22 15 10.01 0 18.54-6.22 22-15-3.46-8.78-11.99-15-22-15zm0 25c-5.52 0-10-4.48-10-10s4.48-10 10-10 10 4.48 10 10-4.48 10-10 10zm0-16c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6z"/></svg>',
iconHidden:
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 48 48"><path fill="currentColor" d="M24 14c5.52 0 10 4.48 10 10 0 1.29-.26 2.52-.71 3.65l5.85 5.85c3.02-2.52 5.4-5.78 6.87-9.5-3.47-8.78-12-15-22.01-15-2.8 0-5.48.5-7.97 1.4l4.32 4.31c1.13-.44 2.36-.71 3.65-.71zM4 8.55l4.56 4.56.91.91C6.17 16.6 3.56 20.03 2 24c3.46 8.78 12 15 22 15 3.1 0 6.06-.6 8.77-1.69l.85.85L39.45 44 42 41.46 6.55 6 4 8.55zM15.06 19.6l3.09 3.09c-.09.43-.15.86-.15 1.31 0 3.31 2.69 6 6 6 .45 0 .88-.06 1.3-.15l3.09 3.09C27.06 33.6 25.58 34 24 34c-5.52 0-10-4.48-10-10 0-1.58.4-3.06 1.06-4.4zm8.61-1.57 6.3 6.3L30 24c0-3.31-2.69-6-6-6l-.33.03z"/></svg>',
name: "Toggle Watched Videos",
stateKey: "YTHWV_STATE",
type: "toggle",
},
{
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 48 48"><path fill="currentColor" d="M31.95 3c-1.11 0-2.25.3-3.27.93l-15.93 9.45C10.32 14.79 8.88 17.67 9 20.7c.15 3 1.74 5.61 4.17 6.84.06.03 2.25 1.05 2.25 1.05l-2.7 1.59c-3.42 2.04-4.74 6.81-2.94 10.65C11.07 43.47 13.5 45 16.05 45c1.11 0 2.22-.3 3.27-.93l15.93-9.45c2.4-1.44 3.87-4.29 3.72-7.35-.12-2.97-1.74-5.61-4.17-6.81-.06-.03-2.25-1.05-2.25-1.05l2.7-1.59c3.42-2.04 4.74-6.81 2.91-10.65C36.93 4.53 34.47 3 31.95 3z"/></svg>',
iconHidden:
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 48 48"><g fill="currentColor"><g clip-path="url(#slashGap)"><path d="M31.97 3c-1.11 0-2.25.3-3.27.93l-15.93 9.45c-2.43 1.41-3.87 4.29-3.75 7.32.15 3 1.74 5.61 4.17 6.84.06.03 2.25 1.05 2.25 1.05l-2.7 1.59C9.32 32.22 8 36.99 9.8 40.83c1.29 2.64 3.72 4.17 6.27 4.17 1.11 0 2.22-.3 3.27-.93l15.93-9.45c2.4-1.44 3.87-4.29 3.72-7.35-.12-2.97-1.74-5.61-4.17-6.81-.06-.03-2.25-1.05-2.25-1.05l2.7-1.59c3.42-2.04 4.74-6.81 2.91-10.65C36.95 4.53 34.49 3 31.97 3z"/></g><path d="m7.501 5.55 4.066-2.42 24.26 40.78-4.065 2.418z"/></g></svg>',
name: "Toggle Shorts",
stateKey: "YTHWV_STATE_SHORTS",
type: "toggle",
},
{
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path fill="currentColor" d="M12 9.5a2.5 2.5 0 0 1 0 5 2.5 2.5 0 0 1 0-5m0-1c-1.93 0-3.5 1.57-3.5 3.5s1.57 3.5 3.5 3.5 3.5-1.57 3.5-3.5-1.57-3.5-3.5-3.5zM13.22 3l.55 2.2.13.51.5.18c.61.23 1.19.56 1.72.98l.4.32.5-.14 2.17-.62 1.22 2.11-1.63 1.59-.37.36.08.51c.05.32.08.64.08.98s-.03.66-.08.98l-.08.51.37.36 1.63 1.59-1.22 2.11-2.17-.62-.5-.14-.4.32c-.53.43-1.11.76-1.72.98l-.5.18-.13.51-.55 2.24h-2.44l-.55-2.2-.13-.51-.5-.18c-.6-.23-1.18-.56-1.72-.99l-.4-.32-.5.14-2.17.62-1.21-2.12 1.63-1.59.37-.36-.08-.51c-.05-.32-.08-.65-.08-.98s.03-.66.08-.98l.08-.51-.37-.36L3.6 8.56l1.22-2.11 2.17.62.5.14.4-.32c.53-.44 1.11-.77 1.72-.99l.5-.18.13-.51.54-2.21h2.44M14 2h-4l-.74 2.96c-.73.27-1.4.66-2 1.14l-2.92-.83-2 3.46 2.19 2.13c-.06.37-.09.75-.09 1.14s.03.77.09 1.14l-2.19 2.13 2 3.46 2.92-.83c.6.48 1.27.87 2 1.14L10 22h4l.74-2.96c.73-.27 1.4-.66 2-1.14l2.92.83 2-3.46-2.19-2.13c.06-.37.09-.75.09-1.14s-.03-.77-.09-1.14l2.19-2.13-2-3.46-2.92.83c-.6-.48-1.27-.87-2-1.14L14 2z"/></svg>',
name: "Settings",
type: "settings",
},
];
// ===========================================================
const debounce = function (func, wait, immediate) {
let timeout;
return (...args) => {
const later = () => {
timeout = null;
if (!immediate) func.apply(this, args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(this, args);
};
};
// ===========================================================
const findWatchedElements = () => {
const watched = document.querySelectorAll(
[
".ytd-thumbnail-overlay-resume-playback-renderer",
// 2025-02-01 Update
".ytThumbnailOverlayProgressBarHostWatchedProgressBarSegmentModern",
].join(","),
);
const withThreshold = Array.from(watched).filter((bar) => {
return (
bar.style.width &&
Number.parseInt(bar.style.width, 10) >=
gmc.get("HIDDEN_THRESHOLD_PERCENT")
);
});
logDebug(
`Found ${watched.length} watched elements ` +
`(${withThreshold.length} within threshold)`,
);
return withThreshold;
};
// ===========================================================
const findShortsContainers = () => {
const shortsContainers = [
// All pages (2024-09 update)
document.querySelectorAll("[is-shorts]"),
// Subscriptions Page (List View)
document.querySelectorAll(
"ytd-reel-shelf-renderer ytd-reel-item-renderer",
),
document.querySelectorAll(
"ytd-rich-shelf-renderer ytd-rich-grid-slim-media",
),
// Home Page & Subscriptions Page (Grid View)
document.querySelectorAll("ytd-reel-shelf-renderer ytd-thumbnail"),
// Search results page
document.querySelectorAll(
"ytd-reel-shelf-renderer .ytd-reel-shelf-renderer",
),
].reduce((acc, matches) => {
matches?.forEach((child) => {
const container =
child.closest("ytd-reel-shelf-renderer") ||
child.closest("ytd-rich-shelf-renderer");
if (container && !acc.includes(container)) acc.push(container);
});
return acc;
}, []);
// Search results sometimes also show Shorts as if they're regular videos with a little "Shorts" badge
document
.querySelectorAll(
'.ytd-thumbnail-overlay-time-status-renderer[aria-label="Shorts"]',
)
.forEach((child) => {
const container = child.closest("ytd-video-renderer");
shortsContainers.push(container);
});
logDebug(`Found ${shortsContainers.length} shorts container elements`);
return shortsContainers;
};
// ===========================================================
const findButtonAreaTarget = () => {
// Button will be injected into the main header menu
return document.querySelector("#container #end #buttons");
};
// ===========================================================
const determineYoutubeSection = () => {
const { href } = window.location;
let youtubeSection = "misc";
if (href.includes("/watch?")) {
youtubeSection = "watch";
} else if (
href.match(/.*\/(user|channel|c)\/.+\/videos/u) ||
href.match(/.*\/@.*/u)
) {
youtubeSection = "channel";
} else if (href.includes("/feed/subscriptions")) {
youtubeSection = "subscriptions";
} else if (href.includes("/feed/trending")) {
youtubeSection = "trending";
} else if (href.includes("/playlist?")) {
youtubeSection = "playlist";
}
return youtubeSection;
};
// ===========================================================
const updateClassOnWatchedItems = () => {
// Remove existing classes
document
.querySelectorAll(".YT-HWV-WATCHED-DIMMED")
.forEach((el) => el.classList.remove("YT-HWV-WATCHED-DIMMED"));
document
.querySelectorAll(".YT-HWV-WATCHED-HIDDEN")
.forEach((el) => el.classList.remove("YT-HWV-WATCHED-HIDDEN"));
// If we're on the History page -- do nothing. We don't want to hide
// watched videos here.
if (window.location.href.indexOf("/feed/history") >= 0) return;
const section = determineYoutubeSection();
const state = localStorage[`YTHWV_STATE_${section}`];
findWatchedElements().forEach((item, _i) => {
let watchedItem;
let dimmedItem;
// "Subscription" section needs us to hide the "#contents",
// but in the "Trending" section, that class will hide everything.
// So there, we need to hide the "ytd-video-renderer"
if (section === "subscriptions") {
// For rows, hide the row and the header too. We can't hide
// their entire parent because then we'll get the infinite
// page loader to load forever.
watchedItem =
// Grid item
item.closest(".ytd-grid-renderer") ||
item.closest(".ytd-item-section-renderer") ||
item.closest(".ytd-rich-grid-row") ||
item.closest(".ytd-rich-grid-renderer") ||
// List item
item.closest("#grid-container");
// If we're hiding the .ytd-item-section-renderer element, we need to give it
// some extra spacing otherwise we'll get stuck in infinite page loading
if (watchedItem?.classList.contains("ytd-item-section-renderer")) {
watchedItem
.closest("ytd-item-section-renderer")
.classList.add("YT-HWV-HIDDEN-ROW-PARENT");
}
} else if (section === "playlist") {
watchedItem = item.closest("ytd-playlist-video-renderer");
} else if (section === "watch") {
watchedItem = item.closest("ytd-compact-video-renderer");
// Don't hide video if it's going to play next.
//
// If there is no watchedItem - we probably got
// `ytd-playlist-panel-video-renderer`:
// let's also ignore it as in case of shuffle enabled
// we could accidentially hide the item which gonna play next.
if (watchedItem?.closest("ytd-compact-autoplay-renderer")) {
watchedItem = null;
}
// For playlist items, we never hide them, but we will dim
// them even if current mode is to hide rather than dim.
const watchedItemInPlaylist = item.closest(
"ytd-playlist-panel-video-renderer",
);
if (!watchedItem && watchedItemInPlaylist) {
dimmedItem = watchedItemInPlaylist;
}
} else {
// For home page and other areas
watchedItem =
item.closest("ytd-rich-item-renderer") ||
item.closest("ytd-video-renderer") ||
item.closest("ytd-grid-video-renderer");
}
if (watchedItem) {
// Add current class
if (state === "dimmed") {
watchedItem.classList.add("YT-HWV-WATCHED-DIMMED");
} else if (state === "hidden") {
watchedItem.classList.add("YT-HWV-WATCHED-HIDDEN");
}
}
if (dimmedItem && (state === "dimmed" || state === "hidden")) {
dimmedItem.classList.add("YT-HWV-WATCHED-DIMMED");
}
});
};
// ===========================================================
const updateClassOnShortsItems = () => {
const section = determineYoutubeSection();
document
.querySelectorAll(".YT-HWV-SHORTS-DIMMED")
.forEach((el) => el.classList.remove("YT-HWV-SHORTS-DIMMED"));
document
.querySelectorAll(".YT-HWV-SHORTS-HIDDEN")
.forEach((el) => el.classList.remove("YT-HWV-SHORTS-HIDDEN"));
const state = localStorage[`YTHWV_STATE_SHORTS_${section}`];
const shortsContainers = findShortsContainers();
shortsContainers.forEach((item) => {
// Add current class
if (state === "dimmed") {
item.classList.add("YT-HWV-SHORTS-DIMMED");
} else if (state === "hidden") {
item.classList.add("YT-HWV-SHORTS-HIDDEN");
}
});
};
// ===========================================================
const renderButtons = () => {
// Find button area target
const target = findButtonAreaTarget();
if (!target) return;
// Did we already render the buttons?
const existingButtons = document.querySelector(".YT-HWV-BUTTONS");
// Generate buttons area DOM
const buttonArea = document.createElement("div");
buttonArea.classList.add("YT-HWV-BUTTONS");
// Render buttons
BUTTONS.forEach(({ icon, iconHidden, name, stateKey, type }) => {
// For toggle buttons, determine where in localStorage they track state
const section = determineYoutubeSection();
const storageKey = [stateKey, section].join("_");
const toggleButtonState = localStorage.getItem(storageKey) || "normal";
// Generate button DOM
const button = document.createElement("button");
button.title =
type === "toggle"
? `${name} : currently "${toggleButtonState}" for section "${section}"`
: `${name}`;
button.classList.add("YT-HWV-BUTTON");
if (toggleButtonState !== "normal")
button.classList.add("YT-HWV-BUTTON-DISABLED");
button.innerHTML = toggleButtonState === "hidden" ? iconHidden : icon;
buttonArea.appendChild(button);
// Attach events for toggle buttons
switch (type) {
case "toggle":
button.addEventListener("click", () => {
logDebug(`Button ${name} clicked. State: ${toggleButtonState}`);
let newState = "dimmed";
if (toggleButtonState === "dimmed") {
newState = "hidden";
} else if (toggleButtonState === "hidden") {
newState = "normal";
}
localStorage.setItem(storageKey, newState);
updateClassOnWatchedItems();
updateClassOnShortsItems();
renderButtons();
});
break;
case "settings":
button.addEventListener("click", () => {
gmc.open();
renderButtons();
});
break;
}
});
// Insert buttons into DOM
if (existingButtons) {
target.parentNode.replaceChild(buttonArea, existingButtons);
logDebug("Re-rendered menu buttons");
} else {
target.parentNode.insertBefore(buttonArea, target);
logDebug("Rendered menu buttons");
}
};
const run = debounce((mutations) => {
// Don't react if only our own buttons changed state
// to avoid running an endless loop
if (
mutations &&
mutations.length === 1 &&
(mutations[0].target.classList.contains("YT-HWV-BUTTON") ||
mutations[0].target.classList.contains("YT-HWV-BUTTON-SHORTS"))
) {
return;
}
logDebug("Running check for watched videos, and shorts");
updateClassOnWatchedItems();
updateClassOnShortsItems();
renderButtons();
}, 250);
// ===========================================================
// Hijack all XHR calls
const send = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function (data) {
this.addEventListener(
"readystatechange",
function () {
if (
// Anytime more videos are fetched -- re-run script
this.responseURL.indexOf("browse_ajax?action_continuation") > 0
) {
setTimeout(() => {
run();
}, 0);
}
},
false,
);
send.call(this, data);
};
// ===========================================================
const observeDOM = (() => {
const MutationObserver =
window.MutationObserver || window.WebKitMutationObserver;
const eventListenerSupported = window.addEventListener;
return (obj, callback) => {
logDebug("Attaching DOM listener");
// Invalid `obj` given
if (!obj) return;
if (MutationObserver) {
const obs = new MutationObserver((mutations, _observer) => {
if (
mutations[0].addedNodes.length ||
mutations[0].removedNodes.length
) {
callback(mutations);
}
});
obs.observe(obj, { childList: true, subtree: true });
} else if (eventListenerSupported) {
obj.addEventListener("DOMNodeInserted", callback, false);
obj.addEventListener("DOMNodeRemoved", callback, false);
}
};
})();
// ===========================================================
logDebug("Starting Script");
// YouTube does navigation via history and also does a bunch
// of AJAX video loading. In order to ensure we're always up
// to date, we have to listen for ANY DOM change event, and
// re-run our script.
observeDOM(document.body, run);
run();
})();