VK Video Filter (Text-Based Scanner)

Скрывает видео в VK по количеству просмотров (минимум, максимум, диапазон). Ищет по тексту, игнорирует фреймы.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         VK Video Filter (Text-Based Scanner)
// @namespace    http://tampermonkey.net/
// @version      4.2
// @description  Скрывает видео в VK по количеству просмотров (минимум, максимум, диапазон). Ищет по тексту, игнорирует фреймы.
// @author       torch
// @match        https://vk.com/*
// @match        https://vkvideo.ru/*
// @match        https://vksport.vkvideo.ru/*
// @icon         https://vk.com/images/icons/favicons/fav_logo_2x.ico
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // Защита от запуска внутри скрытых фреймов
    if (window.top !== window.self) {
        return;
    }

    const CONFIG_KEY = 'vk_vf_4_settings';

    // --- State ---
    let config = {
        minViews: 10000,
        maxViews: 0, // 0 = нет ограничений
        isEnabled: true,
        debugMode: false
    };

    // Загрузка настроек
    try {
        const saved = localStorage.getItem(CONFIG_KEY);
        if (saved) config = { ...config, ...JSON.parse(saved) };
    } catch (e) {}

    // --- Helpers ---
    const log = (msg, color = '#0f0') => {
        console.log(`%c[VK Filter] ${msg}`, `color: ${color}; background: #222; padding: 2px;`);
    };

    // Парсер: ищет число перед словом "просмотров"
    function extractViewsFromText(fullText) {
        if (!fullText) return -1;

        // Нормализация текста
        let text = fullText.toLowerCase()
            .replace(/[\n\r]/g, ' ')
            .replace(/ /g, ' ')
            .replace(/\u00A0/g, ' ');

        // Убираем пробелы МЕЖДУ цифрами (например, "1 250" -> "1250")
        text = text.replace(/(\d)\s+(?=\d)/g, '$1');

        // Сжимаем множественные пробелы
        text = text.replace(/\s+/g, ' ');

        // Регулярка
        const regex = /([\d,.]+)\s*(тыс|млн|млрд|k|m|b)?\.?\s*просмотр/gi;
        const matches = [...text.matchAll(regex)];

        if (matches.length === 0) return -1;

        // Берем последнее совпадение (защита от "просмотров" в названии видео)
        const match = matches[matches.length - 1];

        let numStr = match[1].replace(',', '.'); // 1,5 -> 1.5
        let num = parseFloat(numStr);
        const multiplier = match[2];

        if (multiplier) {
            if (multiplier.startsWith('тыс') || multiplier === 'k') num *= 1000;
            else if (multiplier.startsWith('млн') || multiplier === 'm') num *= 1000000;
            else if (multiplier.startsWith('млрд') || multiplier === 'b') num *= 1000000000;
        }

        return Math.round(num);
    }

    // --- Core Logic ---
    function scanAndHide() {
        if (!config.isEnabled && !config.debugMode) return;

        const selector = [
            '[data-testid="video_card_layout"]', // Новый дизайн VK Video
            '.VideoCard',                        // Старый дизайн
            '.video_item',                       // Классический ВК
            '.vkitVideoCardLayout__card--xI1tS', // Обфусцированные классы
            '.VideoCardList__videoItem--VPDyl'   // Контейнер списка
        ].join(',');

        const cards = document.querySelectorAll(selector);
        const processHash = `${config.minViews}_${config.maxViews}`;

        cards.forEach(card => {
            // Пропускаем уже обработанные (учитываем хэш настроек, чтобы при смене От/До перепроверить все)
            if (card.dataset.vvfProcessed === processHash && !config.debugMode) return;

            // Ищем текст карточки
            const cardText = card.innerText || card.textContent;
            const views = extractViewsFromText(cardText);

            if (views === -1) {
                if (config.debugMode) {
                    card.style.border = '2px dashed orange';
                    card.title = `[VK Filter] Не нашел слово "просмотров" в тексте:\n${cardText.substring(0, 50)}...`;
                }
                return;
            }

            // Логика фильтрации (Диапазон, Мин, Макс)
            let shouldHide = false;
            let hideReason = '';

            if (config.minViews > 0 && views < config.minViews) {
                shouldHide = true;
                hideReason = `Меньше минимума (${config.minViews})`;
            } else if (config.maxViews > 0 && views > config.maxViews) {
                shouldHide = true;
                hideReason = `Больше максимума (${config.maxViews})`;
            }

            // Применяем стили скрытия
            if (shouldHide) {
                if (config.debugMode) {
                    card.style.border = '4px solid red';
                    card.style.opacity = '0.5';
                    card.style.display = '';
                    card.title = `[VK Filter] ПРОСМОТРОВ: ${views} (${hideReason})`;
                } else {
                    card.style.display = 'none';
                    card.style.border = '';
                }
            } else {
                card.style.display = '';
                if (config.debugMode) {
                    card.style.border = '4px solid green';
                    card.style.opacity = '1';
                    card.title = `[VK Filter] ПРОСМОТРОВ: ${views} (Попадает в фильтр)`;
                } else {
                    card.style.border = '';
                }
            }

            card.dataset.vvfProcessed = processHash;
        });
    }

    function resetAll() {
        const cards = document.querySelectorAll('[data-testid="video_card_layout"], .VideoCard, .video_item, [class*="VideoCard"]');
        cards.forEach(c => {
            c.style.display = '';
            c.style.border = '';
            c.style.opacity = '';
            delete c.dataset.vvfProcessed;
        });
    }

    // --- UI Construction ---
    function buildUI() {
        if (document.getElementById('vvf-root')) return;

        const root = document.createElement('div');
        root.id = 'vvf-root';
        root.innerHTML = `
            <style>
                #vvf-btn {
                    position: fixed; bottom: 20px; left: 20px; width: 50px; height: 50px;
                    background: #2D2D2D; border: 2px solid #555; border-radius: 50%;
                    color: white; display: flex; align-items: center; justify-content: center;
                    cursor: pointer; z-index: 9999999; font-size: 24px; user-select: none;
                    transition: 0.2s;
                }
                #vvf-btn:hover { background: #444; transform: scale(1.05); }
                #vvf-menu {
                    position: fixed; bottom: 80px; left: 20px; width: 280px;
                    background: #191919; color: #eee; padding: 20px; border-radius: 16px;
                    z-index: 9999999; display: none; border: 1px solid #444;
                    font-family: -apple-system, system-ui, sans-serif;
                    box-shadow: 0 10px 40px rgba(0,0,0,0.8);
                }
                .vvf-row { margin-bottom: 15px; display: flex; align-items: center; justify-content: space-between; }
                .vvf-title { font-size: 16px; font-weight: bold; margin-bottom: 15px; display: block; color: #fff; }
                .vvf-input {
                    background: #333; border: 1px solid #555; color: white;
                    padding: 8px; border-radius: 8px; width: 45%; font-size: 14px; box-sizing: border-box;
                }
                .vvf-btn-action {
                    width: 100%; padding: 10px; background: #0077FF; color: white;
                    border: none; border-radius: 8px; cursor: pointer; font-weight: 600;
                    font-size: 14px;
                }
                .vvf-btn-action:hover { background: #0066dd; }
                .vvf-chk { transform: scale(1.3); }
                .vvf-hint { font-size: 11px; color: #888; text-align: center; margin-bottom: 15px; margin-top: -10px; }
            </style>
            <div id="vvf-btn">👁️</div>
            <div id="vvf-menu">
                <span class="vvf-title">Фильтр Просмотров v4.2</span>

                <div class="vvf-row">
                    <label>Включено</label>
                    <input type="checkbox" id="vvf-enabled" class="vvf-chk" ${config.isEnabled ? 'checked' : ''}>
                </div>

                <div class="vvf-row">
                    <label style="color:#fa0">Режим отладки<br><span style="font-size:10px; color:#888">(рамки вместо скрытия)</span></label>
                    <input type="checkbox" id="vvf-debug" class="vvf-chk" ${config.debugMode ? 'checked' : ''}>
                </div>

                <div class="vvf-row" style="margin-bottom: 5px;">
                    <label>Диапазон (оставьте 0, если не нужно)</label>
                </div>
                <div class="vvf-row" style="justify-content: flex-start; gap: 10px;">
                    <input type="number" id="vvf-min" class="vvf-input" placeholder="От" value="${config.minViews || ''}" title="Скрывать видео, где просмотров меньше чем">
                    <span style="color: #666;">—</span>
                    <input type="number" id="vvf-max" class="vvf-input" placeholder="До" value="${config.maxViews || ''}" title="Скрывать видео, где просмотров больше чем">
                </div>
                <div class="vvf-hint">Например: От 10000 До 0</div>

                <button id="vvf-save" class="vvf-btn-action">Применить</button>
            </div>
        `;
        document.body.appendChild(root);

        const btn = document.getElementById('vvf-btn');
        const menu = document.getElementById('vvf-menu');
        const save = document.getElementById('vvf-save');

        btn.onclick = () => { menu.style.display = menu.style.display === 'none' ? 'block' : 'none'; };

        save.onclick = () => {
            config.isEnabled = document.getElementById('vvf-enabled').checked;
            config.debugMode = document.getElementById('vvf-debug').checked;

            // Читаем значения. Пустое поле, отрицательное число или NaN превращаем в 0
            const rawMin = parseInt(document.getElementById('vvf-min').value);
            const rawMax = parseInt(document.getElementById('vvf-max').value);
            config.minViews = isNaN(rawMin) || rawMin < 0 ? 0 : rawMin;
            config.maxViews = isNaN(rawMax) || rawMax < 0 ? 0 : rawMax;

            // Обновляем визуально поля, если пользователь ввел дичь (минусы, буквы)
            document.getElementById('vvf-min').value = config.minViews || '';
            document.getElementById('vvf-max').value = config.maxViews || '';

            localStorage.setItem(CONFIG_KEY, JSON.stringify(config));

            resetAll();
            scanAndHide();
        };
    }

    // --- Init ---
    log('Скрипт v4.2 загружен');
    buildUI();

    // Запускаем цикл проверки
    setInterval(scanAndHide, 1000);

})();