YT Comment Filter (Improved)

Automatically hide YouTube comments based on username and spam text patterns.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         YT Comment Filter (Improved)
// @supportURL   https://gist.github.com/adimuhamad/143a06052413aaecb6ddf1a4e39103c1
// @namespace    https://gist.github.com/adimuhamad/143a06052413aaecb6ddf1a4e39103c1
// @homepageURL  https://gist.github.com/adimuhamad/143a06052413aaecb6ddf1a4e39103c1
// @version      4.0
// @description  Automatically hide YouTube comments based on username and spam text patterns.
// @author       Mochamad Adi MR (adimuham.mad)
// @match        *://www.youtube.com/watch?v=*
// @match        *://www.youtube.com/shorts/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @license      MIT
// @compatible   chrome
// @compatible   firefox
// @compatible   edge
// @compatible   opera
// @compatible   safari
// @run-at       document-end
// ==/UserScript==

(function () {
    "use strict";

    let filterToggleButton = null;
    let isShowingHidden = false;
    const STORAGE_KEY = "yt_filter_custom_words";
    const builtInForbiddenUsernames = ["vip"];

    function loadCustomWords() {
        const savedWords = GM_getValue(STORAGE_KEY, "");

        if (savedWords) {
            return savedWords.split(',').map(word => word.trim()).filter(Boolean);
        }

        return [];
    }

    function showSettingsPrompt() {
        const currentWords = GM_getValue(STORAGE_KEY, "");
        const newWords = prompt("Enter custom username (separate with commas)):\nno need to enter a username where the vowels\nare replaced with numbers", currentWords);
        if (newWords === null) return;
        GM_setValue(STORAGE_KEY, newWords);
        alert("Custom usernames has saved!\nRefresh the page to apply the changes and see the results.");
    }

    function registerMenu() {
        GM_registerMenuCommand("List Blocked Username", showSettingsPrompt);
    }

    const customForbiddenUsernames = loadCustomWords();
    const forbiddenUsernames = Array.from(new Set([...builtInForbiddenUsernames, ...customForbiddenUsernames]));

    const forbiddenRegex = /[^\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]/;

    function escapeRegExp(str) {
        return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    }

    function createLeetPattern(word) {
        let escapedWord = escapeRegExp(word);
        return escapedWord.replace(/a/gi, "[a4]").replace(/i/gi, "[i1]").replace(/e/gi, "[e3]").replace(/o/gi, "[o0]").replace(/s/gi, "[s5]").replace(/t/gi, "[t7]").replace(/g/gi, "[g9]");
    }

    const forbiddenPatterns = forbiddenUsernames.map(createLeetPattern);
    const forbiddenUserRegex = new RegExp(forbiddenPatterns.join("|"), "i");

    function addGlobalStyles() {
        GM_addStyle(`
            .yt-comment-filter-hidden { display: none; }
            body.yt-filter-showing-hidden .yt-comment-filter-hidden {
                display: block !important;
                opacity: 0.6;
                border: 1px dashed rgba(255, 0, 0, 0.5);
                border-radius: 8px;
                margin-bottom: 8px !important;
            }

            #yt-filter-toggle-container {
                display: inline-flex;
                align-items: center;
                vertical-align: middle;
                cursor: pointer;
                font-family: "Roboto","Arial",sans-serif;
                font-size: 14px;
                font-weight: 500;
                color: var(--yt-spec-text-secondary);
                margin-left: 32px;
                position: relative;
                bottom: 3px;
            }

            #yt-filter-toggle-container > div {
                display: inline-flex;
                align-items: center;
            }

            #yt-filter-toggle-container svg {
                width: 24px;
                height: 24px;
                fill: var(--yt-spec-text-secondary);
                margin-right: 4px;
            }

            #yt-filter-toggle-container .yt-filter-label,
            #yt-filter-toggle-container .yt-filter-status {
                text-transform: uppercase;
                margin-right: 4px;
            }

            #yt-filter-toggle-container .icon-visibility-off { display: none; }
            #yt-filter-toggle-container .icon-visibility-on { display: inline-block; }
            body.yt-filter-showing-hidden #yt-filter-toggle-container .icon-visibility-off { display: inline-block; }
            body.yt-filter-showing-hidden #yt-filter-toggle-container .icon-visibility-on { display: none; }
        `);
    }

    function updateHiddenCount() {
        if (!filterToggleButton) return;
        const count = document.querySelectorAll(".yt-comment-filter-hidden").length;
        const label = filterToggleButton.querySelector(".yt-filter-label");
        const status = filterToggleButton.querySelector(".yt-filter-status");
        const info = filterToggleButton.querySelector(".yt-filter-info");
        if (!label || !status || !info) return;

        label.textContent = "Hidden";

        if (isShowingHidden) {
            status.textContent = "OFF";
            info.style.display = "none";
        } else {
            status.textContent = "ON";
            info.style.display = "";
            info.textContent = `(${count})`;
        }
    }

    function injectFilterButton() {
        if (document.getElementById("yt-filter-toggle-container")) return;
        const targetElement = document.querySelector("ytd-comments-header-renderer #additional-section");
        if (!targetElement) return;
        filterToggleButton = document.createElement("span");
        filterToggleButton.id = "yt-filter-toggle-container";
        filterToggleButton.className = "style-scope ytd-comments-header-renderer";
        const visibilityIconSVG = `<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" focusable="false"><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"></path></svg>`;
        const visibilityOffIconSVG = `<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" focusable="false"><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"></path></svg>`;
        filterToggleButton.innerHTML = `<div><svg class="icon-visibility-on" xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" focusable="false">${visibilityIconSVG}</svg><svg class="icon-visibility-off" xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" focusable="false">${visibilityOffIconSVG}</svg><span class="yt-filter-label">Hidden</span><span class="yt-filter-status">OFF</span><span class="yt-filter-info">(0)</span></div>`;
        filterToggleButton.addEventListener("click", () => {
            isShowingHidden = !isShowingHidden;
            document.body.classList.toggle("yt-filter-showing-hidden", isShowingHidden);
            updateHiddenCount();
        });
        targetElement.after(filterToggleButton);
        return true;
    }

    let throttleCooldown = false;
    const throttleDelay = 1000;

    function removeBadComments() {
        const commentContainers = document.querySelectorAll("ytd-comment-view-model:not(.yt-comment-filter-hidden)");

        commentContainers.forEach((commentContainer) => {
            const commentTextElement = commentContainer.querySelector("#content-text");
            const usernameElement = commentContainer.querySelector("#author-text span");
            if (!commentTextElement) return;
            let isSpam = false;
            if (forbiddenRegex.test(commentTextElement.innerText)) isSpam = true;
            if (!isSpam && usernameElement && forbiddenUserRegex.test(usernameElement.innerText)) isSpam = true;

            if (isSpam) {
                commentContainer.style.display = "none";
                commentContainer.classList.add("yt-comment-filter-hidden");

                if (commentContainer.id === "comment") {
                    const thread = commentContainer.closest("ytd-comment-thread-renderer");
                    if (thread) {
                        const repliesSection = thread.querySelector("div#replies");
                        if (repliesSection) {
                            repliesSection.style.display = "none";
                        }
                    }
                }
            }
        });

        updateHiddenCount();
    }

    function throttledRemoveBadComments() {
        if (throttleCooldown) return;
        removeBadComments();
        throttleCooldown = true;
        setTimeout(() => {
            throttleCooldown = false;
        }, throttleDelay);
    }

    registerMenu();
    addGlobalStyles();
    setTimeout(removeBadComments, 3000);

    const observer = new MutationObserver(() => {
        throttledRemoveBadComments();
    });

    observer.observe(document.body, { childList: true, subtree: true });

    const buttonInjectInterval = setInterval(() => {
        if (injectFilterButton()) {
            clearInterval(buttonInjectInterval);
            updateHiddenCount();
        }
    }, 1000);
})();