Youtube button to delete a video from a playlist (Fixed & Robust)

Добавляет кнопку для удаления видео из плейлиста на ютубе. Работает с разными языками и устойчив к обновлениям интерфейса.

// ==UserScript==
// @name         Youtube button to delete a video from a playlist (Fixed & Robust)
// @name:en      Youtube button to delete a video from a playlist (Fixed & Robust)
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description:en Adds a button to directly remove videos from the playlist on YouTube. Works across different languages and UI updates.
// @description  Добавляет кнопку для удаления видео из плейлиста на ютубе. Работает с разными языками и устойчив к обновлениям интерфейса.
// @author       You
// @match        https://www.youtube.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    console.log('Robust script v2.1 started');

    // Уникальный SVG-путь для иконки удаления (корзины). Самый надежный идентификатор.
    const TRASH_ICON_SVG_PATH = "M11 17H9V8h2v9zm4-9h-2v9h2V8zm4-4v1h-1v16H6V5H5V4h4V3h6v1h4zm-2 1H7v15h10V5z";

    // Словарь с переводами для поиска по тексту (запасной вариант).
    const REMOVE_TEXT = {
        'en': 'Remove from',
        'ru': 'Удалить из плейлиста',
        'de': 'Aus Playlist entfernen',
        'fr': 'Retirer de',
        'es': 'Quitar de',
        'pt': 'Remover da playlist',
        'it': 'Rimuovi da',
    };

    /**
     * Функция для надежного ожидания появления элемента в DOM.
     * @param {string} selector - CSS селектор для поиска.
     * @param {number} timeout - Максимальное время ожидания в мс.
     * @returns {Promise<Element|null>}
     */
    function waitForElement(selector, timeout = 3000) {
        return new Promise(resolve => {
            const interval = setInterval(() => {
                const element = document.querySelector(selector);
                if (element) {
                    clearInterval(interval);
                    clearTimeout(timer);
                    resolve(element);
                }
            }, 100);

            const timer = setTimeout(() => {
                clearInterval(interval);
                console.warn(`waitForElement: Element "${selector}" not found.`);
                resolve(null);
            }, timeout);
        });
    }

    const style = document.createElement('style');
    style.textContent = `
        .remove-button-custom {
            display: flex;
            align-items: center;
            border: none;
            background: transparent;
            color: #aaa; /* Сделал чуть ярче */
            cursor: pointer;
            margin-top: 5px;
            padding: 0;
            transition: color 0.2s, transform 0.2s;
            font-size: 20px;
        }
        .remove-button-custom:hover { color: #f1f1f1; }
        .remove-button-custom:active { transform: scale(0.85); }
    `;
    document.head.append(style);

    function addRemoveButton(videoElement) {
        if (videoElement.querySelector('.remove-button-custom')) return;

        const button = document.createElement('button');
        button.className = 'remove-button-custom';
        button.title = 'Remove from playlist';

        // === ГЛАВНОЕ ИСПРАВЛЕНИЕ ===
        // БЫЛО (вызывает ошибку TrustedHTML): button.innerHTML = '🗑️';
        // СТАЛО (безопасно):
        button.textContent = '🗑️';

        button.addEventListener('click', async (event) => {
            event.preventDefault();
            event.stopPropagation();

            const menuButton = videoElement.querySelector('#button.ytd-menu-renderer');
            if (!menuButton) return;
            menuButton.click();

            const menuContainer = await waitForElement('ytd-menu-popup-renderer, iron-dropdown');
            if (!menuContainer) {
                alert('Menu not found. Please try again.');
                return;
            }

            const menuItems = menuContainer.querySelectorAll('ytd-menu-service-item-renderer');
            let removeMenuItem = null;

            // Способ 1: Поиск по SVG-иконке (самый надежный)
            removeMenuItem = Array.from(menuItems).find(item =>
                item.querySelector(`path[d="${TRASH_ICON_SVG_PATH}"]`)
            );

            if (removeMenuItem) {
                console.log('Found remove button by ICON');
            }

            // Способ 2: Поиск по тексту (запасной, если иконка изменится)
            if (!removeMenuItem) {
                const lang = document.documentElement.lang.split('-')[0] || 'en';
                const removeText = REMOVE_TEXT[lang] || REMOVE_TEXT['en'];

                removeMenuItem = Array.from(menuItems).find(item =>
                    item.innerText.trim().startsWith(removeText)
                );
                if (removeMenuItem) console.log(`Found remove button by TEXT for lang "${lang}"`);
            }

            if (removeMenuItem) {
                removeMenuItem.click();
            } else {
                console.error('Could not find the remove button in the menu.', Array.from(menuItems).map(i => i.innerText));
                alert('Script could not find the remove button.');
                document.body.click(); // Закрываем меню
            }
        });

        const metaContainer = videoElement.querySelector('#meta');
        if (metaContainer) {
            metaContainer.appendChild(button);
        }
    }

    function processPage() {
        document.querySelectorAll('ytd-playlist-video-renderer').forEach(addRemoveButton);
    }

    const observer = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
            for (const node of mutation.addedNodes) {
                if (node.nodeType === 1) {
                    if (node.matches('ytd-playlist-video-renderer')) {
                        addRemoveButton(node);
                    }
                    node.querySelectorAll('ytd-playlist-video-renderer').forEach(addRemoveButton);
                }
            }
        }
    });

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

    window.addEventListener('yt-navigate-finish', processPage);
    // Для надежности запускаем и при первоначальной загрузке
    if (document.body) {
        processPage();
    } else {
        document.addEventListener('DOMContentLoaded', processPage);
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址