YouTube Comment Filter

Automatically hide YouTube comments based on username, specific keywords, and spam text patterns. Features Dual-Mode Observer, Live Sync, and Data Export/Import.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Advertisement:

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

Advertisement:

// ==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();
})();