YouTube Filter: Home Only + Master Switch

Фильтр по просмотрам в рекомендациях

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         YouTube Filter: Home Only + Master Switch
// @namespace    http://tampermonkey.net/
// @version      13.0
// @description  Фильтр по просмотрам в рекомендациях
// @author       You
// @match        https://www.youtube.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-end
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- НАСТРОЙКИ ---
    let IS_DEBUG = false;

    // --- 1. СТИЛИ ---
    const style = document.createElement('style');
    style.textContent = `
        /* КНОПКА ОТКРЫТИЯ МЕНЮ */
        #yt-filter-toggle-btn {
            position: fixed;
            bottom: 20px;
            right: 20px;
            width: 45px;
            height: 45px;
            background: #212121;
            border: 2px solid #3ea6ff;
            border-radius: 50%;
            cursor: pointer;
            z-index: 2147483648;
            display: none; /* Скрыта по умолчанию (покажем JS-ом) */
            justify-content: center;
            align-items: center;
            font-size: 24px;
            color: #3ea6ff;
            box-shadow: 0 4px 10px rgba(0,0,0,0.5);
            transition: transform 0.2s, background 0.2s, opacity 0.3s;
        }
        #yt-filter-toggle-btn:hover {
            background: #3ea6ff;
            color: #000;
            transform: scale(1.1);
        }
        #yt-filter-toggle-btn.hidden-page {
            display: none !important;
        }

        /* МЕНЮ */
        #yt-view-filter-panel {
            position: fixed;
            bottom: 80px;
            right: 20px;
            background: #181818;
            color: #eee;
            padding: 15px;
            border-radius: 12px;
            z-index: 2147483647;
            box-shadow: 0 4px 25px rgba(0,0,0,0.9);
            border: 1px solid #333;
            font-family: Roboto, Arial, sans-serif;
            width: 220px;
            display: none;
            flex-direction: column;
            gap: 10px;
        }
        #yt-view-filter-panel.visible { display: flex; }

        /* Элементы меню */
        .yt-row { display: flex; justify-content: space-between; align-items: center; font-size: 13px; }
        .yt-row-check { display: flex; justify-content: flex-start; align-items: center; gap: 8px; font-size: 13px; margin-top:2px; }
        .yt-row-check label { cursor: pointer; }
        .yt-inp {
            background: #0f0f0f;
            border: 1px solid #444;
            color: white;
            padding: 5px;
            border-radius: 4px;
            width: 80px;
            font-size: 13px;
        }
        .yt-btn {
            background: #3ea6ff;
            color: #000;
            border: none;
            padding: 8px;
            border-radius: 6px;
            cursor: pointer;
            font-weight: bold;
            font-size: 13px;
            margin-top: 5px;
        }
        .yt-btn:hover { background: #65b8ff; }

        /* Master Switch */
        .yt-master-switch {
            width: 100%;
            padding: 8px;
            border-radius: 6px;
            font-weight: bold;
            text-align: center;
            cursor: pointer;
            border: 1px solid #444;
            margin-bottom: 5px;
        }
        .yt-master-on { background: #1b3a1e; color: #4caf50; border-color: #2e5c32; }
        .yt-master-off { background: #3a1b1b; color: #f44336; border-color: #5c2e2e; }

        .yt-filter-hidden { display: none !important; }

        /* Рамки отладки */
        .yt-dbg-shorts { border: 4px solid #00bbff !important; box-sizing: border-box; position: relative; z-index: 5000; }
        .yt-dbg-mix { border: 4px solid #ff00ff !important; box-sizing: border-box; position: relative; z-index: 5000; }
        .yt-dbg-views { border: 4px solid #ff0000 !important; box-sizing: border-box; position: relative; z-index: 5000; opacity: 0.7; }
        .yt-dbg-ok { border: 2px solid #00ff00 !important; box-sizing: border-box; position: relative; z-index: 5000; opacity: 0.3; }
    `;
    document.head.appendChild(style);

    // --- 2. ПАРСИНГ ---
    function parseStringViewCount(text) {
        if (!text) return -1;
        let clean = text.toLowerCase()
            .replace(/ /g, ' ')
            .replace(/\u00A0/g, ' ')
            .replace(/\s+/g, ' ')
            .trim();

        if (clean.includes('зрител') || clean.includes('watching') || clean.includes('ждет')) return -2;
        if (clean.includes('нет просмотров') || clean.includes('no views')) return 0;

        const strictMatch = clean.match(/(\d+[.,]?\d*)\s*(тыс\.?|млн\.?|млрд\.?|k|m|b)?\s*(просмотр|view)/);
        if (!strictMatch) return -1;

        let numStr = strictMatch[1].replace(',', '.');
        let num = parseFloat(numStr);
        const multStr = strictMatch[2] || '';

        let multiplier = 1;
        if (multStr.startsWith('тыс') || multStr === 'k') multiplier = 1000;
        else if (multStr.startsWith('млн') || multStr === 'm') multiplier = 1000000;
        else if (multStr.startsWith('млрд') || multStr === 'b') multiplier = 1000000000;

        return Math.round(num * multiplier);
    }

    // --- 3. ФИЛЬТРАЦИЯ ---
    let isRunning = false;

    // Вспомогательная функция очистки (показывает всё обратно)
    function cleanUp() {
        const hidden = document.querySelectorAll('.yt-filter-hidden');
        hidden.forEach(el => el.classList.remove('yt-filter-hidden'));

        if (IS_DEBUG) {
            const debugged = document.querySelectorAll('.yt-dbg-shorts, .yt-dbg-mix, .yt-dbg-views, .yt-dbg-ok');
            debugged.forEach(el => el.classList.remove('yt-dbg-shorts', 'yt-dbg-mix', 'yt-dbg-views', 'yt-dbg-ok'));
        }
    }

    function isHomePage() {
        return window.location.pathname === '/' || window.location.pathname === '';
    }

    function runFilter() {
        if (isRunning) return;
        isRunning = true;

        // 1. Проверка главного переключателя
        const masterEnabled = GM_getValue('masterEnabled', true);
        if (!masterEnabled) {
            cleanUp();
            isRunning = false;
            return;
        }

        // 2. Проверка: Мы на главной?
        if (!isHomePage()) {
            cleanUp(); // Если ушли с главной - чистим фильтры
            isRunning = false;
            return;
        }

        const uiMin = document.getElementById('yt-inp-min');
        const min = uiMin ? parseInt(uiMin.value) : GM_getValue('minViews', 0);
        const max = document.getElementById('yt-inp-max') ? parseInt(document.getElementById('yt-inp-max').value) : GM_getValue('maxViews', 1000);

        const removeShorts = document.getElementById('yt-cb-shorts') ? document.getElementById('yt-cb-shorts').checked : GM_getValue('removeShorts', false);
        const removeMixes = document.getElementById('yt-cb-mixes') ? document.getElementById('yt-cb-mixes').checked : GM_getValue('removeMixes', false);
        const removeTopics = document.getElementById('yt-cb-topics') ? document.getElementById('yt-cb-topics').checked : GM_getValue('removeTopics', false);

        // --- Обработка Секций ---
        const sections = document.querySelectorAll('ytd-rich-section-renderer');
        sections.forEach(section => {
            if (!IS_DEBUG && section.classList.contains('yt-filter-hidden')) return;
            if (IS_DEBUG) section.classList.remove('yt-dbg-shorts', 'yt-dbg-topics');

            let shouldHide = false;
            let hideType = '';

            if (removeShorts) {
                const titleSpan = section.querySelector('#title-text span#title');
                const isShortsTag = section.querySelector('ytd-rich-shelf-renderer[is-shorts]');
                const hasShortsIcon = section.innerHTML.includes('M10 14.65v-5.3L15 12l-5 2.65zm7.77-4.33');

                if ((titleSpan && titleSpan.textContent.trim() === 'Shorts') || isShortsTag || hasShortsIcon) {
                    shouldHide = true;
                    hideType = 'shorts';
                }
            }

            if (removeTopics && !shouldHide) {
                if (section.querySelector('ytd-chips-shelf-with-video-shelf-renderer')) {
                    shouldHide = true;
                    hideType = 'topics';
                }
                const headerTitle = section.querySelector('h2 span');
                if (headerTitle && (headerTitle.textContent.includes('Ещё темы') || headerTitle.textContent.includes('More topics'))) {
                    shouldHide = true;
                    hideType = 'topics';
                }
            }

            if (shouldHide) {
                section.classList.add('yt-filter-hidden');
                if (IS_DEBUG) {
                    section.classList.remove('yt-filter-hidden');
                    if (hideType === 'shorts') section.classList.add('yt-dbg-shorts');
                    if (hideType === 'topics') section.classList.add('yt-dbg-topics');
                }
            }
        });

        // --- Обработка Карточек ---
        const items = document.querySelectorAll('ytd-rich-item-renderer'); // На главной в основном они
        let hiddenCount = 0;

        items.forEach(el => {
            if (!IS_DEBUG && el.classList.contains('yt-filter-hidden')) {
                hiddenCount++;
                return;
            }
            if (IS_DEBUG) el.classList.remove('yt-dbg-ok', 'yt-dbg-views', 'yt-dbg-shorts', 'yt-dbg-mix');

            let shouldHide = false;
            let hideReason = '';

            if (removeMixes && !shouldHide) {
                const badges = el.querySelectorAll('.yt-badge-shape__text');
                for (const badge of badges) {
                    const txt = badge.textContent.trim().toLowerCase();
                    if (txt === 'джем' || txt === 'микс' || txt === 'mix') {
                        shouldHide = true;
                        hideReason = 'mix';
                        break;
                    }
                }
                if (!shouldHide) {
                    if (el.querySelector('yt-collections-stack') || el.querySelector('.yt-lockup-view-model--collection-stack-2') || el.querySelector('a[href*="start_radio=1"]')) {
                        shouldHide = true;
                        hideReason = 'mix';
                    }
                }
            }

            if (removeShorts && !shouldHide) {
                if (el.querySelector('a[href*="/shorts/"]')) {
                    shouldHide = true;
                    hideReason = 'shorts';
                }
            }

            if (!shouldHide && hideReason === '') {
                if (el.closest('.yt-filter-hidden')) return;

                let views = -1;
                const metaBlocks = el.querySelectorAll('.inline-metadata-item, #metadata-line span, .yt-core-attributed-string, .yt-content-metadata-view-model');

                for (const block of metaBlocks) {
                    const txt = block.textContent;
                    const parsed = parseStringViewCount(txt);
                    if (parsed !== -1) {
                        views = parsed;
                        break;
                    }
                }

                if (views === -1) {
                    const lines = el.innerText.split(/\r?\n/);
                    for (const line of lines) {
                        const parsed = parseStringViewCount(line);
                        if (parsed !== -1) {
                            views = parsed;
                            break;
                        }
                    }
                }

                if (views !== -1 && views !== -2) {
                    if (views < min || (max > 0 && views > max)) {
                        shouldHide = true;
                        hideReason = 'views';
                    }
                }
            }

            if (shouldHide) {
                el.classList.add('yt-filter-hidden');
                if (IS_DEBUG) {
                    el.classList.remove('yt-filter-hidden');
                    if (hideReason === 'mix') el.classList.add('yt-dbg-mix');
                    else if (hideReason === 'shorts') el.classList.add('yt-dbg-shorts');
                    else el.classList.add('yt-dbg-views');
                }
                hiddenCount++;
            } else {
                el.classList.remove('yt-filter-hidden');
                if (IS_DEBUG && el.tagName.toLowerCase() !== 'ytd-rich-section-renderer') el.classList.add('yt-dbg-ok');
            }
        });

        const stat = document.getElementById('yt-status');
        if (stat) stat.textContent = `Скрыто: ${hiddenCount}`;

        isRunning = false;
    }

    // --- 4. УПРАВЛЕНИЕ ВИДИМОСТЬЮ КНОПКИ ---
    function updateButtonVisibility() {
        const btn = document.getElementById('yt-filter-toggle-btn');
        const panel = document.getElementById('yt-view-filter-panel');

        if (!btn) return;

        if (isHomePage()) {
            btn.style.display = 'flex'; // Показываем на главной
        } else {
            btn.style.display = 'none'; // Скрываем везде, кроме главной
            if (panel) panel.classList.remove('visible'); // Закрываем меню, если ушли с главной
        }
    }

    // --- 5. ИНТЕРФЕЙС ---
    function createUI() {
        if (document.getElementById('yt-filter-toggle-btn')) return;

        // 1. Кнопка
        const toggleBtn = document.createElement('div');
        toggleBtn.id = 'yt-filter-toggle-btn';
        toggleBtn.textContent = '⚙️';
        toggleBtn.title = 'Фильтр YouTube';
        document.body.appendChild(toggleBtn);

        // 2. Панель
        const panel = document.createElement('div');
        panel.id = 'yt-view-filter-panel';

        const createEl = (tag, text = '', className = '', id = '') => {
            const el = document.createElement(tag);
            if (text) el.textContent = text;
            if (className) el.className = className;
            if (id) el.id = id;
            return el;
        };

        const title = createEl('div', 'Настройки фильтра', '', '');
        title.style.textAlign = 'center';
        title.style.fontWeight = 'bold';
        title.style.marginBottom = '5px';
        title.style.color = '#3ea6ff';
        panel.appendChild(title);

        // --- ГЛАВНЫЙ ПЕРЕКЛЮЧАТЕЛЬ ---
        const masterSwitch = createEl('div', '', 'yt-master-switch', 'yt-master-switch-btn');
        const isMaster = GM_getValue('masterEnabled', true);
        masterSwitch.textContent = isMaster ? '🟢 ФИЛЬТР ВКЛЮЧЕН' : '🔴 ФИЛЬТР ВЫКЛЮЧЕН';
        masterSwitch.className = `yt-master-switch ${isMaster ? 'yt-master-on' : 'yt-master-off'}`;
        masterSwitch.onclick = () => {
            const newState = !GM_getValue('masterEnabled', true);
            GM_setValue('masterEnabled', newState);
            masterSwitch.textContent = newState ? '🟢 ФИЛЬТР ВКЛЮЧЕН' : '🔴 ФИЛЬТР ВЫКЛЮЧЕН';
            masterSwitch.className = `yt-master-switch ${newState ? 'yt-master-on' : 'yt-master-off'}`;
            runFilter();
        };
        panel.appendChild(masterSwitch);
        // ------------------------------

        const rowMin = createEl('div', '', 'yt-row');
        rowMin.appendChild(createEl('label', 'Мин:'));
        const inpMin = createEl('input', '', 'yt-inp', 'yt-inp-min');
        inpMin.type = 'number';
        inpMin.value = GM_getValue('minViews', 0);
        rowMin.appendChild(inpMin);
        panel.appendChild(rowMin);

        const rowMax = createEl('div', '', 'yt-row');
        rowMax.appendChild(createEl('label', 'Макс:'));
        const inpMax = createEl('input', '', 'yt-inp', 'yt-inp-max');
        inpMax.type = 'number';
        inpMax.value = GM_getValue('maxViews', 1000);
        rowMax.appendChild(inpMax);
        panel.appendChild(rowMax);

        const addCheckbox = (labelTxt, id, checked) => {
            const row = createEl('div', '', 'yt-row-check');
            const cb = createEl('input', '', '', id);
            cb.type = 'checkbox';
            cb.checked = checked;
            const lbl = createEl('label', labelTxt);
            lbl.htmlFor = id;
            row.appendChild(cb);
            row.appendChild(lbl);
            panel.appendChild(row);

            cb.addEventListener('change', () => {
                saveSettings();
                runFilter();
            });
        };

        addCheckbox('Скрыть Shorts', 'yt-cb-shorts', GM_getValue('removeShorts', false));
        addCheckbox('Скрыть Миксы', 'yt-cb-mixes', GM_getValue('removeMixes', false));
        addCheckbox('Скрыть "Ещё темы"', 'yt-cb-topics', GM_getValue('removeTopics', false));
        addCheckbox('Debug Mode', 'yt-cb-debug', false);

        const btn = createEl('button', 'Применить', 'yt-btn');
        btn.onclick = () => {
            saveSettings();
            runFilter();
        };
        panel.appendChild(btn);

        const stat = createEl('div', '...', '', 'yt-status');
        stat.style.textAlign = 'center';
        stat.style.color = '#777';
        stat.style.fontSize = '10px';
        stat.style.marginTop = '5px';
        panel.appendChild(stat);

        document.body.appendChild(panel);

        // Логика открытия меню
        toggleBtn.addEventListener('click', () => {
            if (panel.classList.contains('visible')) {
                panel.classList.remove('visible');
            } else {
                panel.classList.add('visible');
            }
        });

        // Сразу проверяем, нужно ли показывать кнопку
        updateButtonVisibility();
    }

    function saveSettings() {
        const iMin = document.getElementById('yt-inp-min');
        const iMax = document.getElementById('yt-inp-max');
        const cShorts = document.getElementById('yt-cb-shorts');
        const cMix = document.getElementById('yt-cb-mixes');
        const cTopics = document.getElementById('yt-cb-topics');
        const cDebug = document.getElementById('yt-cb-debug');

        if (iMin) GM_setValue('minViews', iMin.value);
        if (iMax) GM_setValue('maxViews', iMax.value);
        if (cShorts) GM_setValue('removeShorts', cShorts.checked);
        if (cMix) GM_setValue('removeMixes', cMix.checked);
        if (cTopics) GM_setValue('removeTopics', cTopics.checked);
        if (cDebug) IS_DEBUG = cDebug.checked;
    }

    // --- 6. ЗАПУСК И НАВИГАЦИЯ ---

    // Событие перехода по страницам внутри YouTube (SPA)
    window.addEventListener('yt-navigate-finish', () => {
        updateButtonVisibility();
        runFilter();
    });

    const initInt = setInterval(() => {
        if (document.body) {
            createUI();
            clearInterval(initInt);
            updateButtonVisibility();
            runFilter();
        }
    }, 1000);

    let scrollTimer;
    const observer = new MutationObserver((mutations) => {
        if (scrollTimer) clearTimeout(scrollTimer);
        scrollTimer = setTimeout(() => {
            updateButtonVisibility(); // Проверяем на всякий случай
            runFilter();
        }, 100);
    });

    setTimeout(() => {
        if (document.body) {
            observer.observe(document.body, { childList: true, subtree: true });
        }
    }, 1500);

    setInterval(() => {
        if (!document.hidden) runFilter();
    }, 2000);

})();