Block YouTube videos, comments, notifications by keywords or channel/user blacklist. Hover to show X button for quick blocking.
// ==UserScript==
// @name YouTube Blocklist
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Block YouTube videos, comments, notifications by keywords or channel/user blacklist. Hover to show X button for quick blocking.
// @license MIT
// @author Henry Suen
// @match https://www.youtube.com/
// @match https://www.youtube.com/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @icon https://www.google.com/s2/favicons?sz=64&domain=YouTube.com
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
// ======================== Storage ========================
const STORAGE_KEYS = {
BLOCKED_CHANNELS: 'yt_blocked_channels',
BLOCKED_USERS: 'yt_blocked_users',
BLOCKED_KEYWORDS: 'yt_blocked_keywords',
};
function getBlockedChannels() {
return GM_getValue(STORAGE_KEYS.BLOCKED_CHANNELS, []);
}
function setBlockedChannels(list) {
GM_setValue(STORAGE_KEYS.BLOCKED_CHANNELS, list);
}
function getBlockedUsers() {
return GM_getValue(STORAGE_KEYS.BLOCKED_USERS, []);
}
function setBlockedUsers(list) {
GM_setValue(STORAGE_KEYS.BLOCKED_USERS, list);
}
function getBlockedKeywords() {
return GM_getValue(STORAGE_KEYS.BLOCKED_KEYWORDS, []);
}
function setBlockedKeywords(list) {
GM_setValue(STORAGE_KEYS.BLOCKED_KEYWORDS, list);
}
function addBlockedChannel(channel) {
const list = getBlockedChannels();
const normalized = channel.trim().toLowerCase();
if (normalized && !list.includes(normalized)) {
list.push(normalized);
setBlockedChannels(list);
}
}
// Block channel name + also block @handle in users (so comments are also hidden)
function addBlockedChannelWithHandle(displayName, handleHref) {
addBlockedChannel(displayName);
if (handleHref) {
// handleHref is like "/@LinusTechTips" or "/@handle"
const handle = handleHref.replace(/^\/@?/, '').trim().toLowerCase();
if (handle) {
// Add handle to channels list for video filtering
const chList = getBlockedChannels();
if (!chList.includes(handle)) {
chList.push(handle);
setBlockedChannels(chList);
}
// Also add @handle to users list so comments from this user are hidden
const userHandle = '@' + handle;
const uList = getBlockedUsers();
const normalizedUser = userHandle.toLowerCase();
if (!uList.includes(normalizedUser)) {
uList.push(normalizedUser);
setBlockedUsers(uList);
}
}
}
}
function addBlockedUser(user) {
const list = getBlockedUsers();
const normalized = user.trim().toLowerCase();
if (normalized && !list.includes(normalized)) {
list.push(normalized);
setBlockedUsers(list);
}
}
// ======================== Inject Styles ========================
function injectStyles() {
let style = document.getElementById('ytb-blocklist-styles');
if (style) return;
style = document.createElement('style');
style.id = 'ytb-blocklist-styles';
style.textContent = `
/* ===== Block button on video cards ===== */
.ytb-block-btn {
position: absolute;
bottom: 4px;
right: 4px;
width: 28px;
height: 28px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.75);
color: #fff;
font-size: 16px;
font-weight: bold;
line-height: 28px;
text-align: center;
cursor: pointer;
z-index: 9999;
opacity: 0;
transition: opacity 0.2s;
border: none;
user-select: none;
}
.ytb-block-btn:hover {
background: rgba(200, 0, 0, 0.9);
transform: scale(1.1);
}
/* Sidebar horizontal lockup: position button at top-right of whole card */
.ytLockupViewModelHorizontal .ytb-block-btn {
bottom: auto;
top: 4px;
right: 4px;
}
ytd-rich-item-renderer:hover .ytb-block-btn,
ytd-video-renderer:hover .ytb-block-btn,
ytd-compact-video-renderer:hover .ytb-block-btn,
ytd-grid-video-renderer:hover .ytb-block-btn,
ytd-reel-item-renderer:hover .ytb-block-btn,
yt-lockup-view-model:hover .ytb-block-btn,
.ytLockupViewModelHost:hover .ytb-block-btn,
.ytThumbnailViewModelHost:hover .ytb-block-btn,
yt-thumbnail-view-model:hover .ytb-block-btn,
ytd-notification-renderer:hover .ytb-block-btn {
opacity: 1;
}
/* ===== Block button on comments ===== */
.ytb-block-btn-comment {
position: absolute;
bottom: 8px;
right: 8px;
width: 24px;
height: 24px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.7);
color: #fff;
font-size: 14px;
font-weight: bold;
line-height: 24px;
text-align: center;
cursor: pointer;
z-index: 9999;
opacity: 0;
transition: opacity 0.2s;
border: none;
user-select: none;
}
.ytb-block-btn-comment:hover {
background: rgba(200, 0, 0, 0.9);
transform: scale(1.1);
}
ytd-comment-renderer:hover > .ytb-block-btn-comment,
ytd-comment-thread-renderer:hover > .ytb-block-btn-comment {
opacity: 1;
}
/* ===== Notification pre-hide 3rd span ===== */
ytd-notification-renderer:not(.ytb-filtered) yt-formatted-string.message:not(.cbCustomTitle) > span:nth-child(n+3) {
visibility: hidden !important;
}
ytd-notification-renderer.ytb-filtered yt-formatted-string.message > span {
visibility: visible !important;
}
/* ===== Notification bar ===== */
.ytb-notification-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-bottom: 1px solid #0f3460;
font-size: 13px;
color: #e0e0e0;
z-index: 10000;
}
.ytb-notification-bar .ytb-bar-info {
display: flex;
align-items: center;
gap: 8px;
}
.ytb-notification-bar .ytb-bar-count {
background: #e53935;
color: #fff;
border-radius: 10px;
padding: 2px 8px;
font-size: 12px;
font-weight: bold;
}
.ytb-notification-bar .ytb-bar-restore {
background: rgba(255,255,255,0.1);
color: #90caf9;
border: 1px solid #90caf9;
border-radius: 6px;
padding: 4px 12px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.ytb-notification-bar .ytb-bar-restore:hover {
background: rgba(144,202,249,0.2);
color: #fff;
}
/* ===== Panel overlay ===== */
.ytb-panel-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.7);
z-index: 99999;
display: flex;
align-items: center;
justify-content: center;
animation: ytb-fadein 0.2s ease;
}
@keyframes ytb-fadein {
from { opacity: 0; }
to { opacity: 1; }
}
.ytb-panel {
background: #212121;
color: #eee;
border-radius: 16px;
padding: 0;
width: 520px;
max-height: 80vh;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
box-shadow: 0 16px 48px rgba(0,0,0,0.6);
display: flex;
flex-direction: column;
}
.ytb-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px 16px;
border-bottom: 1px solid #333;
background: #1a1a1a;
border-radius: 16px 16px 0 0;
}
.ytb-panel-header h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #fff;
}
.ytb-panel-close {
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(255,255,255,0.1);
border: none;
color: #aaa;
font-size: 18px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.ytb-panel-close:hover {
background: rgba(255,255,255,0.2);
color: #fff;
}
.ytb-panel-body {
padding: 20px 24px;
overflow-y: auto;
flex: 1;
}
.ytb-section {
margin-bottom: 24px;
}
.ytb-section:last-child {
margin-bottom: 0;
}
.ytb-section-title {
font-size: 13px;
font-weight: 600;
color: #90caf9;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 6px;
}
.ytb-list {
max-height: 140px;
overflow-y: auto;
border: 1px solid #333;
border-radius: 8px;
padding: 4px;
margin-bottom: 10px;
background: #1a1a1a;
}
.ytb-list::-webkit-scrollbar { width: 6px; }
.ytb-list::-webkit-scrollbar-thumb { background: #444; border-radius: 3px; }
.ytb-list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 10px;
border-radius: 6px;
transition: background 0.15s;
}
.ytb-list-item:hover { background: #2a2a2a; }
.ytb-list-item span { font-size: 13px; color: #ddd; }
.ytb-remove-btn {
background: transparent;
color: #e53935;
border: 1px solid #e53935;
border-radius: 4px;
padding: 2px 8px;
cursor: pointer;
font-size: 11px;
transition: all 0.2s;
}
.ytb-remove-btn:hover { background: #e53935; color: #fff; }
.ytb-input-row { display: flex; gap: 8px; }
.ytb-input-row input {
flex: 1;
background: #1a1a1a;
border: 1px solid #444;
color: #eee;
padding: 8px 12px;
border-radius: 8px;
font-size: 13px;
outline: none;
transition: border-color 0.2s;
}
.ytb-input-row input:focus { border-color: #90caf9; }
.ytb-input-row input::placeholder { color: #666; }
.ytb-add-btn {
background: #1e88e5;
color: #fff;
border: none;
border-radius: 8px;
padding: 8px 16px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: background 0.2s;
white-space: nowrap;
}
.ytb-add-btn:hover { background: #42a5f5; }
.ytb-empty {
color: #555;
font-size: 12px;
padding: 8px 10px;
text-align: center;
font-style: italic;
}
.ytb-toast {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
background: #323232;
color: #fff;
padding: 12px 24px;
border-radius: 8px;
z-index: 999999;
font-size: 14px;
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
animation: ytb-fadein 0.2s ease;
transition: opacity 0.3s;
}
/* ===== Masthead button (top-left, near YT logo) ===== */
#ytb-blocklist-btn {
margin-left: 12px;
padding: 6px 14px;
background: transparent;
border: 1px solid var(--yt-spec-10-percent-layer, #ccc);
border-radius: 18px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
white-space: nowrap;
vertical-align: middle;
color: inherit;
}
#ytb-blocklist-btn:hover {
background: var(--yt-spec-10-percent-layer, rgba(255,255,255,0.1));
}
html:not([dark]) #ytb-blocklist-btn {
color: #0f0f0f !important;
border-color: rgba(0, 0, 0, 0.1) !important;
}
html[dark] #ytb-blocklist-btn {
color: #f1f1f1 !important;
border-color: rgba(255, 255, 255, 0.2) !important;
}
/* ===== Channel page block button ===== */
.ytb-channel-block-btn {
padding: 4px 12px;
background: rgba(200, 0, 0, 0.8);
color: #fff;
border: none;
border-radius: 16px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
white-space: nowrap;
vertical-align: middle;
}
.ytb-channel-block-btn:hover {
background: rgba(220, 0, 0, 1);
}
`;
document.head.appendChild(style);
}
// ======================== Toast ========================
function showToast(msg, duration = 2000) {
const toast = document.createElement('div');
toast.className = 'ytb-toast';
toast.textContent = msg;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
}, duration);
}
// ======================== Masthead Button (top-left, next to YT logo) ========================
function addBlocklistButton() {
if (document.getElementById('ytb-blocklist-btn')) return;
const btn = document.createElement('button');
btn.id = 'ytb-blocklist-btn';
btn.textContent = '🛡 Blocklist';
btn.addEventListener('click', openPanel);
// Insert into ytd-masthead #start (same as the reference filter script)
function insertBtn() {
const startContainer = document.querySelector('ytd-masthead #start');
if (startContainer) {
startContainer.appendChild(btn);
return true;
}
return false;
}
if (!insertBtn()) {
const retryInterval = setInterval(() => {
if (insertBtn()) clearInterval(retryInterval);
}, 500);
// Fallback after 10s: fixed position top-left
setTimeout(() => {
clearInterval(retryInterval);
if (!btn.parentElement) {
Object.assign(btn.style, {
position: 'fixed', top: '14px', left: '200px',
zIndex: '99998', marginLeft: '0',
});
document.body.appendChild(btn);
}
}, 10000);
}
}
// ======================== Management Panel ========================
// Helper: create element with properties
function el(tag, props, children) {
const e = document.createElement(tag);
if (props) {
for (const [k, v] of Object.entries(props)) {
if (k === 'className') e.className = v;
else if (k === 'textContent') e.textContent = v;
else if (k === 'id') e.id = v;
else if (k === 'placeholder') e.placeholder = v;
else if (k === 'type') e.type = v;
else if (k === 'title') e.title = v;
else if (k.startsWith('on')) e[k] = v;
else e.setAttribute(k, v);
}
}
if (children) {
for (const child of children) {
if (typeof child === 'string') e.appendChild(document.createTextNode(child));
else if (child) e.appendChild(child);
}
}
return e;
}
function openPanel() {
const existing = document.querySelector('.ytb-panel-overlay');
if (existing) existing.remove();
const overlay = el('div', { className: 'ytb-panel-overlay' });
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
const panel = el('div', { className: 'ytb-panel' });
// Header
const closeBtn = el('button', { className: 'ytb-panel-close', textContent: '\u00D7' });
closeBtn.onclick = () => overlay.remove();
const header = el('div', { className: 'ytb-panel-header' }, [
el('h2', { textContent: '\u{1F6E1}\uFE0F YouTube Blocklist' }),
closeBtn
]);
panel.appendChild(header);
// Body
const body = el('div', { className: 'ytb-panel-body' });
// Create sections
function createSection(emoji, title, listId, inputId, addBtnId, placeholder) {
const section = el('div', { className: 'ytb-section' });
const titleRow = el('div', { className: 'ytb-section-title' }, [
el('span', { textContent: emoji }),
document.createTextNode(' ' + title)
]);
const list = el('div', { className: 'ytb-list', id: listId });
const input = el('input', { type: 'text', id: inputId, placeholder: placeholder });
const addBtn = el('button', { className: 'ytb-add-btn', id: addBtnId, textContent: '+ Add' });
const inputRow = el('div', { className: 'ytb-input-row' }, [input, addBtn]);
section.appendChild(titleRow);
section.appendChild(list);
section.appendChild(inputRow);
return section;
}
body.appendChild(createSection('\u{1F6AB}', 'Blocked Channels', 'ytb-channel-list', 'ytb-channel-input', 'ytb-channel-add', 'Channel name...'));
body.appendChild(createSection('\u{1F648}', 'Blocked Users (Comments)', 'ytb-user-list', 'ytb-user-input', 'ytb-user-add', 'Username...'));
body.appendChild(createSection('\u{1F511}', 'Blocked Keywords', 'ytb-keyword-list', 'ytb-keyword-input', 'ytb-keyword-add', 'Keyword...'));
panel.appendChild(body);
overlay.appendChild(panel);
document.body.appendChild(overlay);
function renderList(containerId, items, removeCallback) {
const container = panel.querySelector('#' + containerId);
while (container.firstChild) container.removeChild(container.firstChild);
if (items.length === 0) {
container.appendChild(el('div', { className: 'ytb-empty', textContent: 'No items added yet' }));
return;
}
items.forEach((item, idx) => {
const span = el('span', { textContent: item });
const removeBtn = el('button', { className: 'ytb-remove-btn', textContent: 'Remove' });
removeBtn.onclick = () => { removeCallback(idx); renderAll(); runFilter(); };
const row = el('div', { className: 'ytb-list-item' }, [span, removeBtn]);
container.appendChild(row);
});
}
function renderAll() {
renderList('ytb-channel-list', getBlockedChannels(), (idx) => {
const list = getBlockedChannels(); list.splice(idx, 1); setBlockedChannels(list);
});
renderList('ytb-user-list', getBlockedUsers(), (idx) => {
const list = getBlockedUsers(); list.splice(idx, 1); setBlockedUsers(list);
});
renderList('ytb-keyword-list', getBlockedKeywords(), (idx) => {
const list = getBlockedKeywords(); list.splice(idx, 1); setBlockedKeywords(list);
});
}
renderAll();
panel.querySelector('#ytb-channel-add').onclick = () => {
const input = panel.querySelector('#ytb-channel-input');
const val = input.value.trim();
if (val) { addBlockedChannel(val); input.value = ''; renderAll(); runFilter(); }
};
panel.querySelector('#ytb-user-add').onclick = () => {
const input = panel.querySelector('#ytb-user-input');
const val = input.value.trim();
if (val) { addBlockedUser(val); input.value = ''; renderAll(); runFilter(); }
};
panel.querySelector('#ytb-keyword-add').onclick = () => {
const input = panel.querySelector('#ytb-keyword-input');
const val = input.value.trim();
if (val) {
const list = getBlockedKeywords();
const normalized = val.toLowerCase();
if (!list.includes(normalized)) { list.push(normalized); setBlockedKeywords(list); }
input.value = '';
renderAll();
runFilter();
}
};
// Enter key support
const inputs = ['ytb-channel-input', 'ytb-user-input', 'ytb-keyword-input'];
const btns = panel.querySelectorAll('.ytb-add-btn');
inputs.forEach((id, i) => {
panel.querySelector('#' + id).addEventListener('keydown', (e) => {
if (e.key === 'Enter') btns[i].click();
});
});
}
// Register Tampermonkey menu command as well
GM_registerMenuCommand('\u{1F4CB} Manage Blocklist', openPanel);
// ======================== Block Button Injection ========================
function getChannelNameFromVideoCard(card) {
// New YouTube layout: channel link href starting with /@
const channelLink = card.querySelector('a[href^="/@"]');
if (channelLink && channelLink.textContent.trim()) {
return channelLink.textContent.trim();
}
// Legacy layout selectors
const selectors = [
'ytd-channel-name yt-formatted-string a',
'ytd-channel-name #text a',
'ytd-channel-name a',
'#channel-name a',
'#text.ytd-channel-name a',
'#byline a',
];
for (const sel of selectors) {
const el = card.querySelector(sel);
if (el && el.textContent.trim()) return el.textContent.trim();
}
const channelEl = card.querySelector('ytd-channel-name #text, #channel-name #text');
if (channelEl && channelEl.textContent.trim()) return channelEl.textContent.trim();
return null;
}
function getAuthorFromComment(comment) {
const selectors = ['#author-text span', '#author-text', 'a#author-text span', 'a#author-text'];
for (const sel of selectors) {
const el = comment.querySelector(sel);
if (el && el.textContent.trim()) return el.textContent.trim();
}
return null;
}
function injectVideoBlockButton(card) {
if (card.querySelector('.ytb-block-btn')) return;
// Find a suitable container for the button
const thumbnail = card.querySelector('#thumbnail, ytd-thumbnail, .ytd-thumbnail, .ytThumbnailViewModelHost')
|| card.querySelector('yt-thumbnail-view-model');
const container = thumbnail || card;
if (container) container.style.position = 'relative';
const btn = document.createElement('div');
btn.className = 'ytb-block-btn';
btn.textContent = '\u2715';
btn.title = 'Block this channel';
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const { name, handle } = detectChannel(card);
if (name) {
if (confirm('Block channel: "' + name + '"?\n\nAll videos from this channel will be hidden.')) {
addBlockedChannelWithHandle(name, handle);
showToast('Blocked: ' + name);
runFilter();
}
} else {
showToast('Could not detect channel name');
}
return false;
});
(thumbnail || card).appendChild(btn);
}
// Unified channel detection for any video card/lockup
function detectChannel(card) {
// 1. Try a[href^="/@"] link (homepage new layout)
let channelLink = card.querySelector('a[href^="/@"]');
if (!channelLink && card.parentElement) channelLink = card.parentElement.querySelector('a[href^="/@"]');
if (!channelLink && card.parentElement?.parentElement) channelLink = card.parentElement.parentElement.querySelector('a[href^="/@"]');
if (channelLink && channelLink.textContent.trim()) {
return { name: channelLink.textContent.trim(), handle: channelLink.getAttribute('href') };
}
// 2. Sidebar / compact layout: first ytContentMetadataViewModelMetadataRow contains channel name as span text
const metaRows = card.querySelectorAll('.ytContentMetadataViewModelMetadataRow');
if (metaRows.length > 0) {
const firstRowSpan = metaRows[0].querySelector('span.ytAttributedStringHost');
if (firstRowSpan && firstRowSpan.textContent.trim()) {
return { name: firstRowSpan.textContent.trim(), handle: null };
}
}
// 3. Legacy selectors
const legacyName = getChannelNameFromVideoCard(card);
return { name: legacyName, handle: null };
}
function injectCommentBlockButton(comment) {
if (comment.querySelector('.ytb-block-btn-comment')) return;
comment.style.position = 'relative';
const btn = document.createElement('div');
btn.className = 'ytb-block-btn-comment';
btn.textContent = '\u2715';
btn.title = 'Block this user';
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const author = getAuthorFromComment(comment);
if (author) {
if (confirm('Block user: "' + author + '"?\n\nAll comments from this user will be hidden.')) {
addBlockedUser(author);
showToast('Blocked user: ' + author);
runFilter();
}
} else {
showToast('Could not detect username');
}
});
comment.appendChild(btn);
}
function injectNotificationBlockButton(notification) {
if (notification.querySelector('.ytb-block-btn')) return;
notification.style.position = 'relative';
const btn = document.createElement('div');
btn.className = 'ytb-block-btn';
btn.textContent = '\u2715';
btn.title = 'Block this channel';
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const msgEl = notification.querySelector('yt-formatted-string.message:not(.cbCustomTitle)');
const firstSpan = msgEl ? msgEl.querySelector(':scope > span:first-child') : null;
const channelName = firstSpan ? firstSpan.textContent.trim() : null;
// Try to get handle from avatar link
const avatarLink = notification.querySelector('a[href^="/@"]');
const handleHref = avatarLink ? avatarLink.getAttribute('href') : null;
if (channelName) {
if (confirm('Block channel: "' + channelName + '" from notifications?')) {
addBlockedChannelWithHandle(channelName, handleHref);
showToast('Blocked: ' + channelName);
runFilter();
}
} else {
showToast('Could not detect channel name');
}
});
notification.appendChild(btn);
}
// ======================== Filtering Logic ========================
function matchesKeyword(text) {
if (!text) return false;
const keywords = getBlockedKeywords();
const lowerText = text.toLowerCase();
return keywords.some(kw => lowerText.includes(kw));
}
function isChannelBlocked(channelName) {
if (!channelName) return false;
const blocked = getBlockedChannels();
const lower = channelName.trim().toLowerCase();
return blocked.some(ch => lower.includes(ch) || ch.includes(lower));
}
// Also check by @handle href
function isChannelBlockedByHandle(handleHref) {
if (!handleHref) return false;
const handle = handleHref.replace(/^\/@/, '').trim().toLowerCase();
if (!handle) return false;
const blocked = getBlockedChannels();
return blocked.some(ch => handle.includes(ch) || ch.includes(handle));
}
function isUserBlocked(userName) {
if (!userName) return false;
const blocked = getBlockedUsers();
const lower = userName.trim().toLowerCase().replace(/^@/, '');
return blocked.some(u => lower.includes(u) || u.includes(lower));
}
function filterVideoCards() {
const videoSelectors = [
'ytd-rich-item-renderer',
'ytd-video-renderer',
'ytd-compact-video-renderer',
'ytd-grid-video-renderer',
'ytd-reel-item-renderer',
];
videoSelectors.forEach(selector => {
document.querySelectorAll(selector).forEach(card => {
injectVideoBlockButton(card);
const { name: channelName, handle: handleHref } = detectChannel(card);
const titleEl = card.querySelector('#video-title, #title, .title');
const title = titleEl ? titleEl.textContent.trim() : '';
let shouldHide = false;
if (isChannelBlocked(channelName)) shouldHide = true;
if (isChannelBlockedByHandle(handleHref)) shouldHide = true;
if (matchesKeyword(title)) shouldHide = true;
if (channelName && matchesKeyword(channelName)) shouldHide = true;
card.style.display = shouldHide ? 'none' : '';
});
});
// yt-lockup-view-model elements (sidebar + other new layouts)
document.querySelectorAll('yt-lockup-view-model').forEach(lockup => {
// Skip if inside ytd-rich-item-renderer (already handled above)
if (lockup.closest('ytd-rich-item-renderer')) return;
injectVideoBlockButton(lockup);
const { name: channelName, handle: handleHref } = detectChannel(lockup);
// Title: from the heading link or span with role="text"
const titleEl = lockup.querySelector('h3 a span[role="text"], a.ytLockupMetadataViewModelTitle span[role="text"], h3 a');
const title = titleEl ? titleEl.textContent.trim() : '';
let shouldHide = false;
if (isChannelBlocked(channelName)) shouldHide = true;
if (isChannelBlockedByHandle(handleHref)) shouldHide = true;
if (matchesKeyword(title)) shouldHide = true;
if (channelName && matchesKeyword(channelName)) shouldHide = true;
lockup.style.display = shouldHide ? 'none' : '';
});
}
function filterComments() {
const commentSelectors = ['ytd-comment-thread-renderer', 'ytd-comment-renderer'];
commentSelectors.forEach(selector => {
document.querySelectorAll(selector).forEach(comment => {
if (selector === 'ytd-comment-thread-renderer' || !comment.closest('ytd-comment-thread-renderer')) {
injectCommentBlockButton(comment);
}
const author = getAuthorFromComment(comment);
const contentEl = comment.querySelector('#content-text, .content-text');
const content = contentEl ? contentEl.textContent.trim() : '';
let shouldHide = false;
if (isUserBlocked(author)) shouldHide = true;
if (matchesKeyword(content)) shouldHide = true;
if (author && matchesKeyword(author)) shouldHide = true;
comment.style.display = shouldHide ? 'none' : '';
});
});
}
// Track if user chose to show blocked notifications (prevent re-hiding by observer)
let notifShowingBlocked = false;
function filterNotifications() {
// If user is viewing blocked notifications, don't re-filter
if (notifShowingBlocked) return;
const notifications = document.querySelectorAll('ytd-notification-renderer');
if (notifications.length === 0) return;
let hiddenCount = 0;
const hiddenItems = [];
notifications.forEach(notification => {
injectNotificationBlockButton(notification);
const msgEl = notification.querySelector('yt-formatted-string.message:not(.cbCustomTitle)');
if (!msgEl) {
notification.classList.add('ytb-filtered');
return;
}
const spans = msgEl.querySelectorAll(':scope > span');
const channelName = spans[0] ? spans[0].textContent.trim() : '';
const contentSpan = spans[2] || null;
const fullText = msgEl.textContent.trim();
let shouldHide = false;
if (isChannelBlocked(channelName)) shouldHide = true;
if (matchesKeyword(fullText)) shouldHide = true;
if (shouldHide) {
notification.style.display = 'none';
notification.classList.add('ytb-filtered');
hiddenCount++;
hiddenItems.push(notification);
} else {
notification.style.display = '';
notification.classList.add('ytb-filtered');
}
});
updateNotificationBar(hiddenCount, hiddenItems);
}
function updateNotificationBar(hiddenCount, hiddenItems) {
const itemsContainer = document.querySelector('ytd-notification-renderer')?.closest('#items');
if (!itemsContainer) return;
let bar = itemsContainer.querySelector('.ytb-notification-bar');
if (hiddenCount === 0) {
if (bar) bar.remove();
return;
}
if (!bar) {
bar = document.createElement('div');
bar.className = 'ytb-notification-bar';
itemsContainer.insertBefore(bar, itemsContainer.firstChild);
}
function renderBarContent(showing) {
while (bar.firstChild) bar.removeChild(bar.firstChild);
const info = el('div', { className: 'ytb-bar-info' });
if (showing) {
info.appendChild(el('span', { textContent: '\u26A0\uFE0F Showing ' + hiddenCount + ' blocked' }));
} else {
info.appendChild(el('span', { textContent: '\u{1F6E1}\uFE0F Blocked:' }));
info.appendChild(el('span', { className: 'ytb-bar-count', textContent: String(hiddenCount) }));
}
bar.appendChild(info);
const btn = el('button', { className: 'ytb-bar-restore', textContent: showing ? 'Hide again' : 'Show hidden' });
btn.onclick = () => {
if (showing) {
hiddenItems.forEach(item => {
item.style.display = 'none';
item.style.opacity = '';
item.style.borderLeft = '';
});
notifShowingBlocked = false;
renderBarContent(false);
} else {
hiddenItems.forEach(item => {
item.style.display = '';
item.style.opacity = '0.6';
item.style.borderLeft = '3px solid #e53935';
});
notifShowingBlocked = true;
renderBarContent(true);
}
};
bar.appendChild(btn);
}
renderBarContent(false);
}
function runFilter() {
filterVideoCards();
filterComments();
filterNotifications();
injectChannelPageBlockButton();
}
// Separate run for non-notification content only
function runFilterVideosAndComments() {
filterVideoCards();
filterComments();
injectChannelPageBlockButton();
}
// ======================== Channel Page Block Button ========================
function injectChannelPageBlockButton() {
// Only on channel pages (/@handle or /c/... or /channel/...)
if (!location.pathname.match(/^\/@|^\/c\/|^\/channel\//)) return;
dbg('Channel page detected:', location.pathname);
// Find the channel name heading
const heading = document.querySelector('yt-dynamic-text-view-model.ytPageHeaderViewModelTitle h1');
if (!heading) { dbg('No heading found'); return; }
if (heading.querySelector('.ytb-channel-block-btn')) return;
// Get channel name from h1's first text span
const nameSpan = heading.querySelector('span[role="text"]');
const channelName = nameSpan ? nameSpan.childNodes[0]?.textContent?.trim() : null;
dbg('Channel name:', channelName);
// Get @handle
const handleEl = document.querySelector('.ytPageHeaderViewModelContentMetadata span[style*="font-weight: 500"]');
const handleText = handleEl ? handleEl.textContent.trim() : null;
const handleHref = handleText ? '/' + handleText : null;
dbg('Handle:', handleText);
if (!channelName) return;
const btn = document.createElement('button');
btn.className = 'ytb-channel-block-btn';
btn.textContent = '\u2715 Block';
btn.title = 'Block this channel';
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
if (confirm('Block channel: "' + channelName + '"?\n\nAll videos from this channel will be hidden.')) {
addBlockedChannelWithHandle(channelName, handleHref);
showToast('Blocked: ' + channelName);
}
});
heading.style.display = 'flex';
heading.style.alignItems = 'center';
heading.style.gap = '12px';
heading.appendChild(btn);
}
// ======================== Observer ========================
let filterTimeout = null;
function debouncedFilter() {
if (filterTimeout) clearTimeout(filterTimeout);
filterTimeout = setTimeout(() => {
// If showing blocked notifications, only filter videos/comments
if (notifShowingBlocked) {
runFilterVideosAndComments();
} else {
runFilter();
}
}, 300);
}
function setupObserver() {
const observer = new MutationObserver((mutations) => {
let shouldRun = false;
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0) {
shouldRun = true;
break;
}
}
if (shouldRun) debouncedFilter();
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
}
// ======================== URL change handler (YouTube SPA) ========================
let lastUrl = location.href;
function setupUrlObserver() {
const urlObserver = new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
// Reset notification showing state on navigation
notifShowingBlocked = false;
setTimeout(() => {
runFilter();
addBlocklistButton();
}, 1500);
}
});
urlObserver.observe(document.body, { childList: true, subtree: true });
}
// ======================== Initialization ========================
const DEBUG = true;
function dbg(...args) { if (DEBUG) console.log('[YTB]', ...args); }
function init() {
dbg('init() called, URL:', location.href);
injectStyles();
dbg('Styles injected');
addBlocklistButton();
dbg('Blocklist button added');
runFilter();
dbg('Initial filter run complete');
setupObserver();
setupUrlObserver();
dbg('Observers set up. Script ready.');
}
if (document.readyState === 'complete') {
init();
} else {
window.addEventListener('load', () => setTimeout(init, 1000));
}
})();