YT Comment Filter (Improved)

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

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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