YouTube Video Hider with 🚫 Icon and Shorts Toggle

Adds a 🚫 symbol to video metadata for hiding videos, excludes Shorts thumbnails, with persistent Shorts toggle state

安装此脚本
作者推荐脚本

您可能也喜欢Twitter/X Timeline Sync

安装此脚本
// ==UserScript==
// @name YouTube Video Hider with 🚫 Icon and Shorts Toggle
// @name:de YouTube Video Ausblender mit 🚫 Symbol und Shorts Umschalter
// @name:es Ocultador de Videos de YouTube con Icono 🚫 y Alternador de Shorts
// @name:fr Masqueur de Vidéos YouTube avec Icône 🚫 et Basculeur de Shorts
// @name:it Nascondi Video YouTube con Icona 🚫 e Interruttore Shorts
// @namespace http://tampermonkey.net/
// @version 2025.9.17.1
// @description Adds a 🚫 symbol to video metadata for hiding videos, excludes Shorts thumbnails, with persistent Shorts toggle state
// @description:de Fügt ein 🚫 Symbol zu Video-Metadaten hinzu, exklusive Shorts, und einen kompakten Button zum Ein-/Ausblenden von Shorts mit persistentem Zustand
// @description:es Agrega un símbolo 🚫 a los metadatos de video, excluyendo Shorts, y un botón compacto para alternar Shorts con estado persistente
// @description:fr Ajoute un symbole 🚫 aux métadonnées des vidéos, sauf pour les Shorts, et un bouton compact pour activer/désactiver les Shorts avec état persistant
// @description:it Aggiunge un simbolo 🚫 ai metadati dei video, esclusi i Shorts, e un pulsante compatto per attivare/disattivare i Shorts con stato persistente
// @icon https://youtube.com/favicon.ico
// @author Copiis
// @license MIT
// @match https://www.youtube.com/*
// @grant GM_setValue
// @grant GM_getValue
// If you find this script useful and would like to support my work, consider making a small donation!
// Bitcoin (BTC): bc1quc5mkudlwwkktzhvzw5u2nruxyepef957p68r7
// PayPal: https://www.paypal.com/paypalme/Coopiis?country.x=DE&locale.x=de_DE
// ==/UserScript==

(function () {
    'use strict';

    // Konfigurationsobjekt
    const config = {
        hideButtonSize: '24px',
        hideButtonOpacity: '0.7',
        shortsCheckInterval: 500,
        maxShortsAttempts: 3,
        debugMode: true,
        reapplyInterval: 1000,
        menuLoadDelay: 150,
        maxMenuAttempts: 15
    };

    // Spracherkennung
    const userLang = (navigator.language || navigator.languages[0] || 'en').substring(0, 2);
    if (config.debugMode) console.log(`[Initializer] Erkannte Sprache: ${userLang}`);

    // Übersetzungen
    const translations = {
        en: {
            hideVideosFound: 'Found videos: ${count}',
            hideButtonAdded: 'Video ${index}: Button added',
            hideNoMenuButton: 'Video ${index}: No menu button found',
            hideMenuOpened: 'Video ${index}: Menu opened',
            hideOptionClicked: 'Video ${index}: Hide option clicked',
            hideOptionNotFound: 'Video ${index}: Hide option not found',
            hideError: 'Video ${index}: Error while hiding: ${error}',
            hideConfirmClicked: 'Video ${index}: Confirm button clicked',
            hideConfirmNotFound: 'Video ${index}: Confirm button not found',
            shortsNoTopbar: 'Topbar or YouTube logo not found',
            shortsButtonExists: 'Toggle button already exists, skipping',
            shortsButtonAdded: 'Toggle button added to topbar',
            shortsNotFound: 'Shorts section not found',
            shortsFound: 'Shorts section found: ${details}',
            shortsSectionHidden: 'Shorts section: hidden',
            shortsSectionShown: 'Shorts section: shown',
            shortsButtonText: 'Shorts',
            initStarted: 'Script initialized',
            initAttempt: 'Attempt ${current} of ${max} for Shorts section',
            initMaxAttempts: 'Maximum attempts reached, no Shorts section found',
            initError: 'Error during initialization: ${error}',
            observerError: 'Error in MutationObserver: ${error}',
            noMetadataFound: 'Video ${index}: No metadata container found'
        },
        de: {
            hideVideosFound: 'Gefundene Videos: ${count}',
            hideButtonAdded: 'Video ${index}: Button hinzugefügt',
            hideNoMenuButton: 'Video ${index}: Kein Menü-Button gefunden',
            hideMenuOpened: 'Video ${index}: Menü geöffnet',
            hideOptionClicked: 'Video ${index}: Ausblenden geklickt',
            hideOptionNotFound: 'Video ${index}: Ausblenden-Option nicht gefunden',
            hideError: 'Video ${index}: Fehler beim Ausblenden: ${error}',
            hideConfirmClicked: 'Video ${index}: Bestätigen-Button geklickt',
            hideConfirmNotFound: 'Video ${index}: Bestätigen-Button nicht gefunden',
            shortsNoTopbar: 'Obere Leiste oder YouTube-Logo nicht gefunden',
            shortsButtonExists: 'Toggle-Button bereits vorhanden, überspringe',
            shortsButtonAdded: 'Toggle-Button in oberer Leiste hinzugefügt',
            shortsNotFound: 'Shorts-Abschnitt nicht gefunden',
            shortsFound: 'Shorts-Abschnitt gefunden: ${details}',
            shortsSectionHidden: 'Shorts-Abschnitt: ausgeblendet',
            shortsSectionShown: 'Shorts-Abschnitt: eingeblendet',
            shortsButtonText: 'Shorts',
            initStarted: 'Skript initialisiert',
            initAttempt: 'Versuch ${current} von ${max} für Shorts-Abschnitt',
            initMaxAttempts: 'Maximale Versuche erreicht, kein Shorts-Abschnitt gefunden',
            initError: 'Fehler bei der Initialisierung: ${error}',
            observerError: 'Fehler im MutationObserver: ${error}',
            noMetadataFound: 'Video ${index}: Kein Metadaten-Container gefunden'
        }
    };

    const t = translations[userLang] || translations.en;

    // Funktion zum Formatieren von Übersetzungen
    function formatTranslation(key, params = {}) {
        let str = t[key] || translations.en[key] || key;
        Object.keys(params).forEach(param => {
            str = str.replace(`\${${param}}`, params[param]);
        });
        return str;
    }

    // Funktion zum Warten auf ein Element
    async function waitForElement(selector, timeout = 3000, maxAttempts = 5) {
        for (let attempt = 1; attempt <= maxAttempts; attempt++) {
            const start = Date.now();
            while (Date.now() - start < timeout) {
                const element = document.querySelector(selector);
                if (element) return element;
                await new Promise(resolve => setTimeout(resolve, 50));
            }
            if (config.debugMode) console.log(`[Wait Debug] Versuch ${attempt}/${maxAttempts}: Element ${selector} nicht gefunden`);
        }
        return null;
    }

    // Funktion zum Simulieren eines Klicks
    function simulateClick(element) {
        const rect = element.getBoundingClientRect();
        const events = [
            new PointerEvent('pointerdown', { bubbles: true, cancelable: true, view: window, clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2, isTrusted: true, pointerType: 'mouse' }),
            new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window, clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2 }),
            new MouseEvent('click', { bubbles: true, cancelable: true, view: window, clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2 }),
            new PointerEvent('pointerup', { bubbles: true, cancelable: true, view: window, clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2, isTrusted: true, pointerType: 'mouse' }),
            new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window, clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2 })
        ];
        element.focus();
        events.forEach(event => element.dispatchEvent(event));
    }

    // Debounce-Funktion
    function debounce(func, wait) {
        let timeout;
        return function (...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    }

    // Funktion zum Hinzufügen des Ausblende-Buttons
    const addHideButton = debounce(async () => {
        const videoContainers = document.querySelectorAll('yt-lockup-view-model:not([data-hide-button-added])');
        if (videoContainers.length > 0) {
            console.log(formatTranslation('hideVideosFound', { count: videoContainers.length }));
        } else if (config.debugMode) {
            console.log('[Debug] Keine Videos gefunden mit erweitertem Selektor');
        }
        for (let index = 0; index < videoContainers.length; index++) {
            const video = videoContainers[index];
            const metadataContainer = video.querySelector('.yt-lockup-metadata-view-model');
            if (!metadataContainer) {
                console.log(formatTranslation('noMetadataFound', { index }));
                continue;
            }
            const avatarContainer = metadataContainer.querySelector('.yt-lockup-metadata-view-model__avatar');
            if (!avatarContainer) {
                console.log(`[Hide Debug] Kein Avatar-Container für Video ${index} gefunden`);
                continue;
            }
            if (avatarContainer.querySelector('.hide-video-btn')) {
                if (config.debugMode) console.log(`[Hide Debug] Button bereits vorhanden für Video ${index}`);
                continue;
            }
            const hideButton = document.createElement('div');
            hideButton.className = 'hide-video-btn';
            hideButton.textContent = '🚫';
            Object.assign(hideButton.style, {
                display: 'inline-flex',
                alignItems: 'center',
                justifyContent: 'center',
                width: config.hideButtonSize,
                height: config.hideButtonSize,
                borderRadius: '50%',
                fontSize: '16px',
                color: 'white',
                backgroundColor: `rgba(0, 0, 0, ${config.hideButtonOpacity})`,
                cursor: 'pointer',
                pointerEvents: 'auto',
                visibility: 'visible',
                opacity: '1',
                marginLeft: '8px',
                zIndex: '10003'
            });
            avatarContainer.appendChild(hideButton);
            video.setAttribute('data-hide-button-added', 'true');
            console.log(formatTranslation('hideButtonAdded', { index }));
            if (config.debugMode) console.log(`[Hide Debug] Button hinzugefügt zu: ${avatarContainer.outerHTML.slice(0, 100)}`);
            hideButton.addEventListener('click', async (e) => {
                e.stopPropagation();
                e.preventDefault();
                try {
                    const menuButton = video.querySelector(
                        'button-view-model button.yt-spec-button-shape-next, ' +
                        'yt-icon-button#button.dropdown-trigger, ' +
                        'button.yt-icon-button'
                    );
                    if (!menuButton) {
                        console.log(formatTranslation('hideNoMenuButton', { index }));
                        return;
                    }
                    simulateClick(menuButton);
                    console.log(formatTranslation('hideMenuOpened', { index }));
                    let menu = null;
                    let attempt = 0;
                    while (!menu && attempt < config.maxMenuAttempts) {
                        attempt++;
                        menu = document.querySelector('tp-yt-iron-dropdown:not([aria-hidden="true"])');
                        if (!menu) {
                            await new Promise(resolve => setTimeout(resolve, config.menuLoadDelay));
                        }
                    }
                    if (!menu) {
                        console.log(formatTranslation('hideOptionNotFound', { index }));
                        return;
                    }
                    let hideOption = null;
                    attempt = 0;
                    while (!hideOption && attempt < config.maxMenuAttempts) {
                        attempt++;
                        const menuItems = menu.querySelectorAll('yt-list-item-view-model');
                        hideOption = Array.from(menuItems).find(item => {
                            const textElement = item.querySelector('span.yt-core-attributed-string');
                            const text = textElement?.textContent?.trim().toLowerCase();
                            return text?.includes('ausblenden') || text?.includes('hide');
                        });
                        if (hideOption) {
                            hideOption.focus();
                            simulateClick(hideOption);
                            console.log(formatTranslation('hideOptionClicked', { index }));
                            await new Promise(resolve => setTimeout(resolve, config.menuLoadDelay));
                        } else {
                            await new Promise(resolve => setTimeout(resolve, config.menuLoadDelay));
                        }
                    }
                    if (!hideOption) {
                        console.log(formatTranslation('hideOptionNotFound', { index }));
                        if (config.debugMode) {
                            const menuItems = menu.querySelectorAll('yt-list-item-view-model');
                            const menuItemDetails = Array.from(menuItems).map(item => ({
                                html: item.outerHTML.slice(0, 100),
                                text: item.querySelector('span.yt-core-attributed-string')?.textContent?.trim() || 'Kein Text'
                            }));
                            console.log('[Hide Debug] Verfügbare Menü-Elemente:', JSON.stringify(menuItemDetails, null, 2));
                            console.log('[Hide Debug] Gesamte Menüstruktur:', menu.outerHTML.slice(0, 500));
                        }
                        return;
                    }
                    const confirmButton = await waitForElement(
                        'yt-button-renderer#confirm-button, ' +
                        'tp-yt-paper-button:not([disabled])',
                        2000, 3
                    );
                    if (confirmButton) {
                        simulateClick(confirmButton);
                        console.log(formatTranslation('hideConfirmClicked', { index }));
                    } else {
                        console.log(formatTranslation('hideConfirmNotFound', { index }));
                    }
                } catch (err) {
                    console.error(formatTranslation('hideError', { index, error: err.message }));
                }
            });
        }
    }, 100);

    // Funktion zum Hinzufügen des Shorts-Toggle-Buttons
    let shortsButton = null;
    let shortsSection = null;
    let isShortsHidden = GM_getValue('isShortsHidden', false); // Lade gespeicherten Zustand

    function addShortsToggleButton() {
        const topbar = document.querySelector('ytd-masthead #masthead-container') || document.querySelector('ytd-masthead');
        if (!topbar) {
            console.log(formatTranslation('shortsNoTopbar'));
            return;
        }
        if (document.querySelector('.shorts-toggle-wrapper')) {
            console.log(formatTranslation('shortsButtonExists'));
            return;
        }
        const toggleWrapper = document.createElement('div');
        toggleWrapper.className = 'shorts-toggle-wrapper';
        shortsButton = document.createElement('button');
        shortsButton.className = 'shorts-toggle-btn';
        const textSpan = document.createElement('span');
        textSpan.textContent = formatTranslation('shortsButtonText');
        const iconSpan = document.createElement('span');
        iconSpan.className = 'shorts-toggle-icon';
        iconSpan.textContent = '🚫';
        shortsButton.appendChild(textSpan);
        shortsButton.appendChild(iconSpan);
        Object.assign(shortsButton.style, {
            padding: '2px 8px',
            border: 'none',
            borderRadius: '4px',
            backgroundColor: 'transparent',
            color: 'white',
            cursor: 'pointer',
            fontSize: '12px',
            display: 'flex',
            alignItems: 'center',
            gap: '4px'
        });
        toggleWrapper.appendChild(shortsButton);
        const logoContainer = topbar.querySelector('#logo') || topbar.querySelector('#container');
        if (logoContainer) {
            logoContainer.appendChild(toggleWrapper);
            console.log(formatTranslation('shortsButtonAdded'));
        }
        const checkShortsSection = () => {
            shortsSection = document.querySelector(
                'ytd-rich-shelf-renderer[is-shorts], ' +
                'ytd-rich-shelf-renderer span#title[textContent*="Shorts" i]'
            );
            if (shortsSection) {
                console.log(formatTranslation('shortsFound', { details: shortsSection.outerHTML.slice(0, 100) }));
                shortsButton.disabled = false;
                iconSpan.style.display = isShortsHidden ? 'none' : 'inline';
                shortsSection.style.display = isShortsHidden ? 'none' : '';
            } else {
                console.log(formatTranslation('shortsNotFound'));
                shortsButton.disabled = true;
                iconSpan.style.display = 'none';
            }
        };
        checkShortsSection();
        shortsButton.addEventListener('click', () => {
            if (shortsSection) {
                isShortsHidden = !isShortsHidden;
                GM_setValue('isShortsHidden', isShortsHidden); // Speichere den Zustand
                shortsSection.style.display = isShortsHidden ? 'none' : '';
                iconSpan.style.display = isShortsHidden ? 'none' : 'inline';
                console.log(isShortsHidden
                    ? formatTranslation('shortsSectionHidden')
                    : formatTranslation('shortsSectionShown'));
            }
        });
        // Initiale Anwendung des gespeicherten Zustands
        if (isShortsHidden && shortsSection) {
            shortsSection.style.display = 'none';
            iconSpan.style.display = 'none';
        }
    }

    // CSS hinzufügen
    const style = document.createElement('style');
    style.textContent = `
        .hide-video-btn {
            color: white !important;
            background-color: rgba(0, 0, 0, ${config.hideButtonOpacity}) !important;
            border-radius: 50% !important;
            font-size: 16px !important;
            width: ${config.hideButtonSize} !important;
            height: ${config.hideButtonSize} !important;
            display: inline-flex !important;
            align-items: center !important;
            justify-content: center !important;
            cursor: pointer !important;
            pointerEvents: auto !important;
            z-index: 10003 !important;
            visibility: visible !important;
            opacity: 1 !important;
        }
        .hide-video-btn:hover {
            background-color: rgba(0, 0, 0, 0.9) !important;
            box-shadow: 0 0 10px 2px rgba(255, 215, 0, 0.8) !important;
        }
        .yt-lockup-metadata-view-model__avatar {
            display: flex !important;
            align-items: center !important;
        }
        .shorts-toggle-btn {
            transition: color 0.2s !important;
        }
        .shorts-toggle-btn:not(:disabled):hover {
            color: #cc0000 !important;
        }
        .shorts-toggle-wrapper {
            display: inline-flex !important;
            align-items: center !important;
            margin-left: 8px !important;
            z-index: 10001 !important;
        }
        .shorts-toggle-icon {
            display: none;
            font-size: 12px;
            width: 16px;
            height: 16px;
            border-radius: 50%;
            background-color: rgba(0, 0, 0, 0.7);
            text-align: center;
            line-height: 16px;
            color: white;
        }
    `;
    document.head.appendChild(style);

    // Initiale Ausführung
    function initialize() {
        try {
            addHideButton();
            addShortsToggleButton();
            console.log(formatTranslation('initStarted'));
            let attempts = 0;
            const interval = setInterval(() => {
                console.log(formatTranslation('initAttempt', { current: attempts + 1, max: config.maxShortsAttempts }));
                const shortsSection = document.querySelector(
                    'ytd-rich-shelf-renderer[is-shorts], ' +
                    'ytd-rich-shelf-renderer span#title[textContent*="Shorts" i]'
                );
                if (shortsSection) {
                    addShortsToggleButton();
                    clearInterval(interval);
                } else if (attempts >= config.maxShortsAttempts) {
                    console.log(formatTranslation('initMaxAttempts'));
                    clearInterval(interval);
                }
                attempts++;
            }, config.shortsCheckInterval);
            setInterval(addHideButton, config.reapplyInterval);
        } catch (err) {
            console.error(formatTranslation('initError', { error: err.message }));
        }
    }

    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        setTimeout(initialize, 1000);
    } else {
        document.addEventListener('DOMContentLoaded', () => setTimeout(initialize, 1000));
    }

    // MutationObserver
    const observerTarget = document.querySelector('ytd-app #contents') || document.body;
    const observer = new MutationObserver((mutations) => {
        try {
            const hasRelevantChanges = mutations.some(mutation =>
                mutation.addedNodes.length > 0 &&
                mutation.addedNodes[0]?.nodeType === Node.ELEMENT_NODE &&
                (mutation.target.matches('yt-lockup-view-model, ytd-rich-grid-media, ytd-grid-video-renderer, ytd-compact-video-renderer, ytd-rich-shelf-renderer') ||
                 mutation.target.querySelector('yt-lockup-view-model, ytd-rich-grid-media, ytd-grid-video-renderer, ytd-compact-video-renderer, ytd-rich-shelf-renderer[is-shorts], .yt-lockup-metadata-view-model')))
            if (hasRelevantChanges) {
                addHideButton();
                addShortsToggleButton();
            }
        } catch (err) {
            console.error(formatTranslation('observerError', { error: err.message }));
        }
    });
    observer.observe(observerTarget, { childList: true, subtree: true });
})();