Automatically hide YouTube comments based on username, specific keywords, and spam text patterns. Features Dual-Mode Observer, Live Sync, and Data Export/Import.
// ==UserScript==
// @name YouTube Comment Filter
// @namespace YT Comment Filter (Improved)
// @version 7.1
// @description Automatically hide YouTube comments based on username, specific keywords, and spam text patterns. Features Dual-Mode Observer, Live Sync, and Data Export/Import.
// @author MochAdiMR (Refactored)
// @match https://www.youtube.com/watch*
// @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant GM_registerMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addValueChangeListener
// @grant GM_addStyle
// @license MIT
// @supportURL https://buymeacoffee.com/mochadimr
// @homepageURL https://github.com/adimuhamad/
// @run-at document-end
// ==/UserScript==
(function () {
"use strict";
// --- Configuration & Selectors ---
const CONFIG = {
USERS: "yt_filter_custom_words", KEYWORDS: "yt_filter_custom_content_keywords", MODE: "yt_filter_observer_mode",
DEF_USERS: ["vip"], DEF_KEYS: ["pulauwin"], DEF_MODE: "efficient",
SEL: { CONTAINER: "ytd-comment-view-model", TEXT: "#content-text", AUTHOR: "#author-text span", HEADER: "ytd-comments-header-renderer #additional-section", THREAD: "ytd-comment-thread-renderer", REPLIES: "#replies", TARGET: "ytd-comments" },
REGEX_NON_LATIN: /[^\u0000-\u007F\u00A0-\u00FF\u0100-\u017F\u0180-\u024F\u0250-\u02AF\u02B0-\u02FF\u0370-\u03FF\u0400-\u04FF\u0500-\u052F\u0530-\u058F\u0590-\u05FF\u0600-\u06FF\u0700-\u074F\u0750-\u077F\u0780-\u07BF\u07C0-\u07FF\u0800-\u083F\u0840-\u085F\u0860-\u087F\u08A0-\u08FF\u0900-\u097F\u0980-\u09FF\u0A00-\u0A7F\u0A80-\u0AFF\u0B00-\u0B7F\u0B80-\u0BFF\u0C00-\u0C7F\u0C80-\u0CFF\u0D00-\u0D7F\u0D80-\u0DFF\u0E00-\u0E7F\u0E80-\u0EFF\u0F00-\u0FFF\u1000-\u109F\u10A0-\u10FF\u1100-\u11FF\u1200-\u125F\u1280-\u12BF\u13A0-\u13FF\u1400-\u167F\u1680-\u169F\u16A0-\u16FF\u1700-\u171F\u1720-\u173F\u1740-\u175F\u1760-\u177F\u1780-\u17FF\u1800-\u18AF\u1900-\u194F\u1950-\u197F\u1980-\u19DF\u19E0-\u19FF\u1A00-\u1A1F\u1A20-\u1A5F\u1A80-\u1AFF\u1B00-\u1B7F\u1B80-\u1BBF\u1BC0-\u1BFF\u1C00-\u1C4F\u1C50-\u1C7F\u1C90-\u1CBF\u1CC0-\u1CCF\u1CD0-\u1CFF\u1E00-\u1EFF\u1F00-\u1FFF\u2000-\u206F\u2070-\u20CF\u20D0-\u20FF\u2150-\u218F\u2C60-\u2C7F\u2C80-\u2CFF\u2D00-\u2D2F\u2D30-\u2D7F\u2D80-\u2DDF\u2DE0-\u2DFF\u2E00-\u2E7F\u2E80-\u2EFF\u2F00-\u2FDF\u2FF0-\u2FFF\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\u3100-\u312F\u3130-\u318F\u3190-\u319F\u31A0-\u31BF\u31C0-\u31EF\u31F0-\u31FF\u3200-\u32FF\u3300-\u33FF\u3400-\u4DBF\u4DC0-\u4DFF\u4E00-\u9FFF\uA000-\uA48F\uA490-\uA4CF\uA4D0-\uA4FF\uA500-\uA63F\uA640-\uA69F\uA6A0-\uA6FF\uA700-\uA71F\uA720-\uA7FF\uA800-\uA82F\uA830-\uA83F\uA840-\uA87F\uA880-\uA8DF\uA8E0-\uA8FF\uA900-\uA92F\uA930-\uA95F\uA960-\uA97F\uA980-\uA9DF\uA9E0-\uA9FF\uAA00-\uAA3F\uAA40-\uAA6F\uAA70-\uAAAB\uAAAC-\uAAAF\uAAB0-\uAABF\uAAC0-\uAADF\uAAE0-\uAAEF\uAAF0-\uAAFF\uAB00-\uAB2F\uAB30-\uAB6F\uAB70-\uABBF\uABC0-\uABFF\uAC00-\uD7AF\uD7B0-\uD7FF\uF900-\uFAFF\uFB00-\uFB4F\uFB50-\uFDFF\uFE00-\uFE0F\uFE10-\uFE1F\uFE20-\uFE2F\uFE30-\uFE4F\uFE50-\uFE6F\uFE70-\uFEFF]/
};
const State = { isShowingHidden: false, userRegex: null, contentRegex: null, mode: "efficient" };
const Utils = {
debounce: (f, w) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => f(...a), w); }; },
throttle: (f, l) => { let wait = false; return (...a) => { if (!wait) { f(...a); wait = true; setTimeout(() => wait = false, l); } }; },
leet: (w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/a/gi, "[a4@]").replace(/i/gi, "[i1l]").replace(/e/gi, "[e3]").replace(/o/gi, "[o0]").replace(/s/gi, "[s5$]").replace(/t/gi, "[t7+]").replace(/g/gi, "[g9]")
};
function loadSettings() {
State.mode = GM_getValue(CONFIG.MODE, CONFIG.DEF_MODE);
const loadRe = (k, def) => {
const all = [...new Set([...def, ...(GM_getValue(k, "").split(",").map(w => w.trim()).filter(Boolean))])];
// Match with word boundaries and evaluate leet-speak patterns
return all.length ? new RegExp("(?:^|\\s|\\W)(" + all.map(Utils.leet).join("|") + ")(?:\\s|\\W|$)", "i") : null;
};
State.userRegex = loadRe(CONFIG.USERS, CONFIG.DEF_USERS);
State.contentRegex = loadRe(CONFIG.KEYWORDS, CONFIG.DEF_KEYS);
}
const Filter = {
// Evaluate all comments against spam patterns and toggle visibility
process: () => {
const comments = document.querySelectorAll(CONFIG.SEL.CONTAINER);
let hidden = 0;
comments.forEach(c => {
const text = c.querySelector(CONFIG.SEL.TEXT)?.innerText || "";
const user = c.querySelector(CONFIG.SEL.AUTHOR)?.innerText || "";
const isSpam = CONFIG.REGEX_NON_LATIN.test(text) || State.userRegex?.test(user) || State.contentRegex?.test(text);
c.classList.toggle("yt-hidden", isSpam);
c.style.display = isSpam && !State.isShowingHidden ? "none" : "";
// Hide replies if parent comment is flagged
if (c.id === "comment") {
const replies = c.closest(CONFIG.SEL.THREAD)?.querySelector(CONFIG.SEL.REPLIES);
if (replies) replies.style.display = isSpam && !State.isShowingHidden ? "none" : "";
}
if (isSpam) hidden++;
});
UI.updateBtn(hidden, comments.length);
}
};
const UI = {
injectCSS: () => {
// Minified CSS Styles
GM_addStyle(`:root{--ytf-bg:#ffffff;--ytf-txt:#0f0f0f;--ytf-txt-sec:#606060;--ytf-border:#e5e5e5;--ytf-btn-bg:#f2f2f2;--ytf-input:#f9f9f9;--ytf-prim:#3ea6ff;--ytf-danger:#ff4e45}html[dark],html[dark="true"],ytd-app[dark]{--ytf-bg:#212121;--ytf-txt:#f1f1f1;--ytf-txt-sec:#aaaaaa;--ytf-border:#3e3e3e;--ytf-btn-bg:#303030;--ytf-input:#121212}@media (prefers-color-scheme: dark){:root:not([dark="false"]){--ytf-bg:#212121;--ytf-txt:#f1f1f1;--ytf-txt-sec:#aaaaaa;--ytf-border:#3e3e3e;--ytf-btn-bg:#303030;--ytf-input:#121212}}.ytf-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:9999;backdrop-filter:blur(3px);display:flex;align-items:center;justify-content:center;font-family:Roboto,Arial,sans-serif}.ytf-modal{background:var(--ytf-bg);color:var(--ytf-txt);border-radius:12px;padding:24px;width:480px;max-width:90vw;max-height:85vh;overflow-y:auto;box-shadow:0 10px 25px rgba(0,0,0,0.5)}.ytf-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.ytf-header h2{margin:0;font-size:20px;font-weight:500}.ytf-close{background:none;border:none;cursor:pointer;padding:4px;fill:var(--ytf-txt)}.ytf-label{display:block;margin-bottom:8px;font-size:13px;font-weight:500}.ytf-textarea{width:100%;height:80px;background:var(--ytf-input);color:var(--ytf-txt);border:1px solid var(--ytf-border);border-radius:8px;padding:10px;margin-bottom:16px;font-family:monospace;font-size:13px;resize:vertical;box-sizing:border-box;outline:none}.ytf-textarea:focus{border-color:var(--ytf-prim)}.ytf-actions{display:flex;justify-content:space-between;align-items:center;margin-top:12px}.ytf-actions-group{display:flex;gap:8px}.ytf-btn{padding:8px 16px;border-radius:18px;cursor:pointer;font-weight:500;border:none;font-size:14px}.ytf-btn-small{font-size:12px;padding:6px 12px}.ytf-btn-out{background:transparent;color:var(--ytf-txt);border:1px solid var(--ytf-border)}.ytf-btn-out:hover{background:var(--ytf-btn-bg)}.ytf-btn-danger{background:transparent;color:var(--ytf-danger);border:1px solid var(--ytf-danger)}.ytf-btn-danger:hover{background:var(--ytf-danger);color:#fff}.ytf-btn-prim{background:var(--ytf-prim);color:#0f0f0f;font-weight:600}.yt-hidden{display:none}body.show-hidden .yt-hidden{display:block!important;opacity:0.6;border:1px dashed rgba(255,0,0,0.5);border-radius:8px;margin-bottom:8px!important}#ytf-toggle{display:inline-flex;align-items:center;cursor:pointer;font-family:Roboto,Arial,sans-serif;font-size:14px;font-weight:500;color:var(--ytf-txt);fill:var(--ytf-txt);margin-left:32px;position:relative;bottom:3px}`);
},
showModal: (html, bindEvents) => {
document.getElementById('ytf-wrap')?.remove();
const wrap = document.createElement("div");
wrap.id = "ytf-wrap"; wrap.className = "ytf-overlay";
// Minified UI Modal Template
wrap.innerHTML = `<div class="ytf-modal"><div class="ytf-header"><h2 id="ytf-title"></h2><button id="ytf-close" class="ytf-close"><svg viewBox="0 0 24 24" width="24" height="24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg></button></div><div id="ytf-body">${html}</div></div>`;
document.body.appendChild(wrap);
const close = () => wrap.remove();
document.getElementById('ytf-close').onclick = close;
document.addEventListener('keydown', function esc(e) { if(e.key === "Escape"){ close(); document.removeEventListener('keydown', esc); } });
bindEvents(close);
},
openSettings: () => {
// Minified Settings Interface
UI.showModal(`<label class="ytf-label">Blocked Usernames</label><textarea id="ytf-users" class="ytf-textarea" placeholder="Example: login, browser">${GM_getValue(CONFIG.USERS, "")}</textarea><label class="ytf-label">Blocked Keywords (Content)</label><textarea id="ytf-keys" class="ytf-textarea" placeholder="Example: pulauwin">${GM_getValue(CONFIG.KEYWORDS, "")}</textarea><div class="ytf-actions"><div class="ytf-actions-group"><button id="ytf-exp" class="ytf-btn ytf-btn-out ytf-btn-small">Export</button><button id="ytf-imp" class="ytf-btn ytf-btn-out ytf-btn-small">Import</button></div><div class="ytf-actions-group"><button id="ytf-rst" class="ytf-btn ytf-btn-danger">Reset Filters</button><button id="ytf-sv" class="ytf-btn ytf-btn-prim">Save Changes</button></div></div>`, (close) => {
document.getElementById('ytf-title').innerText = "Filter Configuration";
document.getElementById('ytf-users').focus();
// Button Events
document.getElementById('ytf-exp').onclick = () => { prompt("Please copy your configuration data:", JSON.stringify({ u: GM_getValue(CONFIG.USERS, ""), k: GM_getValue(CONFIG.KEYWORDS, "") })); };
document.getElementById('ytf-imp').onclick = () => { const data = prompt("Paste your JSON format data here:"); if(data) try { const p = JSON.parse(data); if(p.u !== undefined) document.getElementById('ytf-users').value = p.u; if(p.k !== undefined) document.getElementById('ytf-keys').value = p.k; alert("✅ Data successfully imported! Click 'Save Changes' to apply."); } catch(e) { alert("❌ Invalid JSON Data Format!"); } };
document.getElementById('ytf-rst').onclick = () => UI.openConfirm("Reset All Filters?", "This will delete all custom usernames and keywords.<br>Are you sure?", "Yes, Reset", () => { GM_setValue(CONFIG.USERS, ""); GM_setValue(CONFIG.KEYWORDS, ""); loadSettings(); Filter.process(); close(); });
document.getElementById('ytf-sv').onclick = () => { GM_setValue(CONFIG.USERS, document.getElementById('ytf-users').value); GM_setValue(CONFIG.KEYWORDS, document.getElementById('ytf-keys').value); loadSettings(); Filter.process(); close(); };
});
},
openConfirm: (title, msg, btnText, onConfirm) => {
// Minified Confirm Dialog Interface
UI.showModal(`<p style="color:var(--ytf-txt-sec);line-height:1.6;font-size:14px;margin:0 0 16px 0;">${msg}</p><div class="ytf-actions" style="justify-content:flex-end;"><div class="ytf-actions-group"><button id="ytf-c-no" class="ytf-btn ytf-btn-out">Cancel</button><button id="ytf-c-yes" class="ytf-btn ytf-btn-prim">${btnText}</button></div></div>`, (close) => {
document.getElementById('ytf-title').innerText = title;
document.getElementById('ytf-c-no').onclick = close;
document.getElementById('ytf-c-yes').onclick = () => { onConfirm(); close(); };
});
},
createBtn: () => {
const target = document.querySelector(CONFIG.SEL.HEADER);
if (document.getElementById("ytf-toggle") || !target) return;
// Minified Button SVG Injection
target.insertAdjacentHTML('afterend', `<span id="ytf-toggle"><svg id="ytf-on" style="width:24px;height:24px;fill:currentColor;margin-right:4px" viewBox="0 0 24 24"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5C21.27 7.61 17 4.5 12 4.5zm0 13c-3.04 0-5.5-2.46-5.5-5.5S8.96 6.5 12 6.5s5.5 2.46 5.5 5.5-2.46 5.5-5.5 5.5zm0-9c-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.5z"/></svg><svg id="ytf-off" style="width:24px;height:24px;fill:currentColor;margin-right:4px;display:none" viewBox="0 0 24 24"><path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L21.73 23 23 21.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.93 1.57 3.5 3.5 3.5.22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-3.04 0-5.5-2.46-5.5-5.5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.93-1.57-3.5-3.5-3.5l-.16.02z"/></svg><span>Hidden </span><strong id="ytf-status" style="margin:0 4px">ON</strong><span id="ytf-info">(0/0)</span></span>`);
document.getElementById("ytf-toggle").onclick = () => {
State.isShowingHidden = !State.isShowingHidden;
document.body.classList.toggle("show-hidden", State.isShowingHidden);
Filter.process();
};
},
updateBtn: (h, t) => {
if (!document.getElementById("ytf-toggle")) return;
document.getElementById("ytf-status").innerText = State.isShowingHidden ? "OFF" : "ON";
document.getElementById("ytf-info").style.display = State.isShowingHidden ? "none" : "inline";
document.getElementById("ytf-info").innerText = `(${h}/${t})`;
document.getElementById("ytf-on").style.display = State.isShowingHidden ? "none" : "block";
document.getElementById("ytf-off").style.display = State.isShowingHidden ? "block" : "none";
}
};
const Observer = {
obs: null,
start: () => {
// Targeted observer: Only monitor the comments section to save CPU resources
const target = document.querySelector(CONFIG.SEL.TARGET);
if (target) {
if (Observer.obs) Observer.obs.disconnect();
const fn = State.mode === "fast" ? Utils.throttle(Filter.process, 1000) : Utils.debounce(Filter.process, 500);
Observer.obs = new MutationObserver(fn);
Observer.obs.observe(target, { childList: true, subtree: true });
} else {
// Keep checking if the comments section hasn't loaded yet
setTimeout(Observer.start, 2000);
}
}
};
const init = () => {
loadSettings(); UI.injectCSS();
// Register Script Manager Menus
GM_registerMenuCommand("⚙️ Configure Filter Settings", UI.openSettings);
GM_registerMenuCommand("🔁 Switch Observer Mode", () => {
const currentMode = State.mode.toUpperCase();
const targetMode = currentMode === "EFFICIENT" ? "FAST" : "EFFICIENT";
const desc = targetMode === "FAST"
? "Comments will disappear instantly, but browser CPU usage may increase slightly."
: "Saves battery and CPU usage. Comments disappear after scroll stops.";
UI.openConfirm("Switch Observer Mode?", `Current Mode: <b>${currentMode}</b><br><br>Do you want to switch to <b>${targetMode}</b> mode?<br>${desc}`, `Switch to ${targetMode}`, () => {
GM_setValue(CONFIG.MODE, targetMode.toLowerCase());
loadSettings(); Observer.start();
});
});
GM_registerMenuCommand("👁️🗨️ Toggle Button Check", UI.createBtn);
// Enable Cross-Tab Live Sync if supported by the script manager
if (typeof GM_addValueChangeListener === "function") {
const reloadProcess = () => { loadSettings(); Filter.process(); };
GM_addValueChangeListener(CONFIG.USERS, reloadProcess);
GM_addValueChangeListener(CONFIG.KEYWORDS, reloadProcess);
GM_addValueChangeListener(CONFIG.MODE, () => { loadSettings(); Observer.start(); });
}
// Wait for page elements to load, then inject components
const i = setInterval(() => { if (document.querySelector(CONFIG.SEL.HEADER)) { UI.createBtn(); clearInterval(i); } }, 2000);
Observer.start(); setTimeout(Filter.process, 2000);
};
init();
})();