YouTube Smart Filter - Configurable View Threshold

Remove YouTube videos based on configurable view count thresholds. Filter out low-engagement content with customizable settings via an intuitive floating panel.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         YouTube Smart Filter - Configurable View Threshold
// @namespace    https://greasyfork.org/en/users/866731-sharmanhall
// @version      2.0
// @description  Remove YouTube videos based on configurable view count thresholds. Filter out low-engagement content with customizable settings via an intuitive floating panel.
// @author       sharmanhall
// @match        *://*.youtube.com/*
// @exclude      *://*.youtube.com/feed/subscriptions
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Default configuration
    const DEFAULT_CONFIG = {
        enabled: true,
        minViews: 1000,
        filterVideos: true,
        filterShorts: true,
        showRemovalCount: true,
        debugMode: false
    };

    // Load configuration
    let config = { ...DEFAULT_CONFIG };
    Object.keys(DEFAULT_CONFIG).forEach(key => {
        const saved = GM_getValue(key);
        if (saved !== undefined) {
            config[key] = saved;
        }
    });

    // Statistics
    let stats = {
        videosRemoved: 0,
        shortsSkipped: 0,
        sessionStart: Date.now()
    };

    // UI Elements
    let configPanel = null;
    let floatingButton = null;
    let statsDisplay = null;

    // Utility functions
    function log(message, isDebug = false) {
        if (!isDebug || config.debugMode) {
            console.log(`[YouTube Smart Filter] ${message}`);
        }
    }

    function saveConfig() {
        Object.keys(config).forEach(key => {
            GM_setValue(key, config[key]);
        });
        log('Configuration saved');
    }

    function parseViewCount(text) {
        if (!text) return 0;

        // Handle different number formats
        const cleanText = text.toLowerCase().replace(/,/g, '');
        
        // Handle Indian system
        if (cleanText.includes('crore')) {
            const num = parseFloat(cleanText.match(/[\d.]+/)?.[0] || '0');
            return num * 10000000; // 1 crore = 10 million
        }
        if (cleanText.includes('lakh')) {
            const num = parseFloat(cleanText.match(/[\d.]+/)?.[0] || '0');
            return num * 100000; // 1 lakh = 100k
        }

        // Handle standard suffixes
        if (cleanText.includes('b')) {
            const num = parseFloat(cleanText.match(/[\d.]+/)?.[0] || '0');
            return num * 1000000000;
        }
        if (cleanText.includes('m')) {
            const num = parseFloat(cleanText.match(/[\d.]+/)?.[0] || '0');
            return num * 1000000;
        }
        if (cleanText.includes('k')) {
            const num = parseFloat(cleanText.match(/[\d.]+/)?.[0] || '0');
            return num * 1000;
        }

        // Extract raw number
        const match = cleanText.match(/(\d+(?:[.,]\d+)*)/);
        if (match) {
            return parseInt(match[1].replace(/[.,]/g, ''));
        }

        return 0;
    }

    function shouldRemoveVideo(viewsElement) {
        if (!config.enabled || !viewsElement) return false;

        const viewText = viewsElement.innerText || viewsElement.textContent || '';
        
        // Skip if this doesn't look like a view count
        if (!viewText || !viewText.toLowerCase().includes('view')) return false;
        
        const viewCount = parseViewCount(viewText);
        
        log(`Checking video: "${viewText}" = ${viewCount} views`, true);
        
        return viewCount < config.minViews && viewCount > 0;
    }

    function shouldSkipShort(viewsElement) {
        if (!config.enabled || !config.filterShorts || !viewsElement) return false;

        const text = viewsElement.innerText || viewsElement.textContent || '';
        
        // Shorts with no proper view count or very low engagement
        if (text.length === 0) return true;
        
        // Check for non-breaking space (indicates loading/no views)
        if (text.includes('\xa0')) return false;
        
        const viewCount = parseViewCount(text);
        return viewCount < config.minViews && viewCount > 0;
    }

    // Page detection functions
    function isSubscriptions() {
        return location.pathname.startsWith("/feed/subscriptions");
    }

    function isChannel() {
        return location.pathname.startsWith("/@") || location.pathname.startsWith("/c/") || location.pathname.startsWith("/channel/");
    }

    function isShorts() {
        return location.pathname.startsWith("/shorts");
    }

    function isWatch() {
        return location.pathname.startsWith("/watch");
    }

    // Main filtering function
    function filterContent() {
        if (!config.enabled || isSubscriptions() || isChannel()) {
            return;
        }

        if (isShorts() && config.filterShorts) {
            filterShorts();
        } else if (config.filterVideos) {
            filterVideos();
        }

        updateStatsDisplay();
    }

    function filterVideos() {
        const selectors = [
            // Main page videos
            '.style-scope.ytd-rich-item-renderer#content',
            // Sidebar videos
            '.style-scope.ytd-compact-video-renderer',
            // Watch page related videos
            '.style-scope.ytd-video-preview'
        ];

        let removedCount = 0;
        
        selectors.forEach(selector => {
            const elements = document.querySelectorAll(selector);
            elements.forEach(element => {
                try {
                    // Skip if already processed
                    if (element.hasAttribute('data-smart-filter-processed')) return;
                    element.setAttribute('data-smart-filter-processed', 'true');
                    
                    let viewsElement = null;
                    
                    // Find views element based on container type
                    if (element.classList.contains('ytd-rich-item-renderer') || 
                        element.classList.contains('ytd-compact-video-renderer') ||
                        element.classList.contains('ytd-video-preview')) {
                        viewsElement = element.querySelector('.inline-metadata-item.style-scope.ytd-video-meta-block');
                    }

                    if (viewsElement && shouldRemoveVideo(viewsElement)) {
                        const container = element.closest('ytd-rich-item-renderer, ytd-compact-video-renderer, ytd-video-preview') || element;
                        if (container && container.parentElement) {
                            log(`Removing video: ${viewsElement.textContent}`, true);
                            container.remove();
                            stats.videosRemoved++;
                            removedCount++;
                        }
                    }
                } catch (error) {
                    log(`Error filtering element: ${error.message}`, true);
                }
            });
        });

        // Handle new YouTube layout with debouncing
        if (removedCount < 5) { // Only check new layout if we haven't removed many videos already
            const newLayoutElements = document.querySelectorAll('yt-lockup-view-model:not([data-smart-filter-processed])');
            newLayoutElements.forEach(video => {
                try {
                    video.setAttribute('data-smart-filter-processed', 'true');
                    
                    // Look for view count in the new structure
                    let viewsElement = video.querySelector('.yt-content-metadata-view-model-wiz__metadata-text');
                    if (viewsElement) {
                        // Check if this element contains view count
                        let viewText = viewsElement.innerText;
                        if (viewText && (viewText.includes('views') || viewText.includes('lakh') || viewText.includes('crore') || /\d+\s*views/i.test(viewText))) {
                            if (shouldRemoveVideo(viewsElement)) {
                                video.remove();
                                stats.videosRemoved++;
                            }
                        }
                    }
                } catch (error) {
                    log(`Error filtering new layout element: ${error.message}`, true);
                }
            });
        }
    }

    function filterShorts() {
        const shortElements = document.querySelectorAll('.reel-video-in-sequence.style-scope.ytd-shorts:not([data-smart-filter-processed])');
        
        shortElements.forEach(shortElement => {
            if (!shortElement.isActive) return;
            
            shortElement.setAttribute('data-smart-filter-processed', 'true');
            
            const viewsElement = shortElement.querySelector('.yt-spec-button-shape-with-label__label');
            
            if (shouldSkipShort(viewsElement)) {
                log(`Skipping short: ${viewsElement?.textContent || 'unknown'}`, true);
                const nextButton = document.querySelector('.navigation-button.style-scope.ytd-shorts:nth-child(2) .yt-spec-touch-feedback-shape__fill');
                if (nextButton) {
                    nextButton.click();
                    stats.shortsSkipped++;
                }
            }
        });
    }

    // UI Creation
    function createFloatingButton() {
        floatingButton = document.createElement('div');
        floatingButton.innerHTML = '🎯';
        floatingButton.title = 'YouTube Smart Filter Settings';
        
        Object.assign(floatingButton.style, {
            position: 'fixed',
            top: '20px',
            right: '20px',
            width: '50px',
            height: '50px',
            backgroundColor: '#ff0000',
            color: 'white',
            borderRadius: '50%',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            cursor: 'pointer',
            fontSize: '20px',
            zIndex: '10000',
            boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
            transition: 'all 0.3s ease',
            userSelect: 'none'
        });

        floatingButton.addEventListener('mouseenter', () => {
            floatingButton.style.transform = 'scale(1.1)';
            floatingButton.style.backgroundColor = '#cc0000';
        });

        floatingButton.addEventListener('mouseleave', () => {
            floatingButton.style.transform = 'scale(1)';
            floatingButton.style.backgroundColor = '#ff0000';
        });

        floatingButton.addEventListener('click', toggleConfigPanel);
        document.body.appendChild(floatingButton);
    }

    function createStatsDisplay() {
        if (!config.showRemovalCount) return;

        statsDisplay = document.createElement('div');
        Object.assign(statsDisplay.style, {
            position: 'fixed',
            top: '80px',
            right: '20px',
            backgroundColor: 'rgba(0,0,0,0.8)',
            color: 'white',
            padding: '8px 12px',
            borderRadius: '8px',
            fontSize: '12px',
            zIndex: '9999',
            fontFamily: 'Arial, sans-serif',
            minWidth: '120px',
            textAlign: 'center'
        });

        document.body.appendChild(statsDisplay);
        updateStatsDisplay();
    }

    function updateStatsDisplay() {
        if (!statsDisplay || !config.showRemovalCount) return;

        const sessionTime = Math.round((Date.now() - stats.sessionStart) / 1000 / 60);
        const statsHTML = `
            <div>Videos: ${stats.videosRemoved}</div>
            <div>Shorts: ${stats.shortsSkipped}</div>
            <div>Time: ${sessionTime}m</div>
        `;
        
        // Use textContent to avoid CSP issues
        statsDisplay.textContent = '';
        const tempDiv = document.createElement('div');
        tempDiv.innerHTML = statsHTML;
        while (tempDiv.firstChild) {
            statsDisplay.appendChild(tempDiv.firstChild);
        }
    }

    function createConfigPanel() {
        configPanel = document.createElement('div');
        Object.assign(configPanel.style, {
            position: 'fixed',
            top: '50%',
            left: '50%',
            transform: 'translate(-50%, -50%)',
            backgroundColor: 'white',
            border: '2px solid #ccc',
            borderRadius: '12px',
            padding: '20px',
            zIndex: '10001',
            fontFamily: 'Arial, sans-serif',
            fontSize: '14px',
            color: '#333',
            boxShadow: '0 8px 32px rgba(0,0,0,0.3)',
            minWidth: '400px',
            maxHeight: '80vh',
            overflowY: 'auto'
        });

        const panelHTML = `
            <div style="text-align: center; margin-bottom: 20px;">
                <h2 style="margin: 0; color: #ff0000;">🎯 YouTube Smart Filter</h2>
                <p style="margin: 5px 0; color: #666;">Configure your video filtering preferences</p>
            </div>
            
            <div style="margin-bottom: 15px;">
                <label style="display: flex; align-items: center; margin-bottom: 10px;">
                    <input type="checkbox" id="enabledCheck" style="margin-right: 8px;" ${config.enabled ? 'checked' : ''}>
                    <strong>Enable Filtering</strong>
                </label>
            </div>

            <div style="margin-bottom: 15px;">
                <label style="display: block; margin-bottom: 5px;"><strong>Minimum View Count:</strong></label>
                <input type="number" id="minViewsInput" value="${config.minViews}" 
                       style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px;" min="0" step="100">
                <small style="color: #666;">Videos with fewer views will be filtered out</small>
            </div>

            <div style="margin-bottom: 15px;">
                <label style="display: flex; align-items: center; margin-bottom: 8px;">
                    <input type="checkbox" id="filterVideosCheck" style="margin-right: 8px;" ${config.filterVideos ? 'checked' : ''}>
                    Filter Regular Videos
                </label>
                <label style="display: flex; align-items: center;">
                    <input type="checkbox" id="filterShortsCheck" style="margin-right: 8px;" ${config.filterShorts ? 'checked' : ''}>
                    Filter YouTube Shorts
                </label>
            </div>

            <div style="margin-bottom: 15px;">
                <label style="display: flex; align-items: center; margin-bottom: 8px;">
                    <input type="checkbox" id="showCountCheck" style="margin-right: 8px;" ${config.showRemovalCount ? 'checked' : ''}>
                    Show Removal Statistics
                </label>
                <label style="display: flex; align-items: center;">
                    <input type="checkbox" id="debugModeCheck" style="margin-right: 8px;" ${config.debugMode ? 'checked' : ''}>
                    Debug Mode (Console Logging)
                </label>
            </div>

            <div style="border-top: 1px solid #eee; padding-top: 15px; margin-top: 20px;">
                <div style="display: flex; gap: 10px; justify-content: center;">
                    <button id="saveBtn" style="background: #ff0000; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-weight: bold;">
                        Save Settings
                    </button>
                    <button id="resetBtn" style="background: #666; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer;">
                        Reset to Defaults
                    </button>
                    <button id="closeBtn" style="background: #ccc; color: #333; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer;">
                        Close
                    </button>
                </div>
            </div>

            <div style="margin-top: 15px; text-align: center; font-size: 12px; color: #888;">
                <p>Session Stats: ${stats.videosRemoved} videos removed, ${stats.shortsSkipped} shorts skipped</p>
            </div>
        `;

        // Use textContent to avoid CSP issues
        const tempDiv = document.createElement('div');
        tempDiv.innerHTML = panelHTML;
        while (tempDiv.firstChild) {
            configPanel.appendChild(tempDiv.firstChild);
        }

        // Event listeners
        configPanel.querySelector('#saveBtn').addEventListener('click', saveSettings);
        configPanel.querySelector('#resetBtn').addEventListener('click', resetSettings);
        configPanel.querySelector('#closeBtn').addEventListener('click', closeConfigPanel);

        // Add hover effects to buttons
        configPanel.querySelectorAll('button').forEach(btn => {
            btn.addEventListener('mouseenter', () => btn.style.opacity = '0.8');
            btn.addEventListener('mouseleave', () => btn.style.opacity = '1');
        });

        document.body.appendChild(configPanel);

        // Create backdrop
        const backdrop = document.createElement('div');
        Object.assign(backdrop.style, {
            position: 'fixed',
            top: '0',
            left: '0',
            width: '100%',
            height: '100%',
            backgroundColor: 'rgba(0,0,0,0.5)',
            zIndex: '10000'
        });
        backdrop.addEventListener('click', closeConfigPanel);
        document.body.appendChild(backdrop);
        configPanel.backdrop = backdrop;
    }

    function toggleConfigPanel() {
        if (configPanel && configPanel.parentElement) {
            closeConfigPanel();
        } else {
            createConfigPanel();
        }
    }

    function closeConfigPanel() {
        if (configPanel) {
            if (configPanel.backdrop) {
                configPanel.backdrop.remove();
            }
            configPanel.remove();
            configPanel = null;
        }
    }

    function saveSettings() {
        config.enabled = document.getElementById('enabledCheck').checked;
        config.minViews = parseInt(document.getElementById('minViewsInput').value) || DEFAULT_CONFIG.minViews;
        config.filterVideos = document.getElementById('filterVideosCheck').checked;
        config.filterShorts = document.getElementById('filterShortsCheck').checked;
        config.showRemovalCount = document.getElementById('showCountCheck').checked;
        config.debugMode = document.getElementById('debugModeCheck').checked;

        saveConfig();
        closeConfigPanel();

        // Update UI based on new settings
        if (config.showRemovalCount && !statsDisplay) {
            createStatsDisplay();
        } else if (!config.showRemovalCount && statsDisplay) {
            statsDisplay.remove();
            statsDisplay = null;
        }

        log('Settings saved successfully!');
        
        // Re-run filtering with new settings
        setTimeout(filterContent, 100);
    }

    function resetSettings() {
        config = { ...DEFAULT_CONFIG };
        saveConfig();
        closeConfigPanel();
        
        if (statsDisplay) {
            statsDisplay.remove();
            statsDisplay = null;
        }
        
        if (config.showRemovalCount) {
            createStatsDisplay();
        }
        
        log('Settings reset to defaults');
    }

    // Event listeners for page changes and content updates
    function setupEventListeners() {
        let filterTimeout = null;
        
        function debounceFilter(delay = 250) {
            if (filterTimeout) {
                clearTimeout(filterTimeout);
            }
            filterTimeout = setTimeout(filterContent, delay);
        }

        // YouTube navigation
        document.addEventListener("yt-navigate-finish", () => {
            debounceFilter(500);
        });

        // Content updates with debouncing
        window.addEventListener("message", () => {
            if (!isShorts()) {
                debounceFilter(300);
            }
        });

        window.addEventListener("load", () => {
            if (!isShorts()) {
                debounceFilter(300);
            }
        });

        window.addEventListener("scrollend", () => {
            if (!isShorts()) {
                debounceFilter(100);
            }
        });

        // URL change detection
        let lastUrl = location.href;
        new MutationObserver(() => {
            const url = location.href;
            if (url !== lastUrl) {
                lastUrl = url;
                debounceFilter(500);
            }
        }).observe(document, { subtree: true, childList: true });

        // Dynamic content observer with throttling
        let observerTimeout = null;
        const contentObserver = new MutationObserver((mutations) => {
            if (observerTimeout) return;
            
            let shouldFilter = false;
            mutations.forEach((mutation) => {
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    // Only filter if new video elements are added
                    for (let node of mutation.addedNodes) {
                        if (node.nodeType === 1 && (
                            node.matches && (
                                node.matches('ytd-rich-item-renderer, ytd-compact-video-renderer, yt-lockup-view-model') ||
                                node.querySelector && node.querySelector('ytd-rich-item-renderer, ytd-compact-video-renderer, yt-lockup-view-model')
                            )
                        )) {
                            shouldFilter = true;
                            break;
                        }
                    }
                }
            });
            
            if (shouldFilter) {
                observerTimeout = setTimeout(() => {
                    filterContent();
                    observerTimeout = null;
                }, 200);
            }
        });
        
        contentObserver.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    // Initialize
    function init() {
        log('YouTube Smart Filter initialized');
        log(`Configuration: ${JSON.stringify(config)}`, true);
        
        createFloatingButton();
        
        if (config.showRemovalCount) {
            createStatsDisplay();
        }
        
        setupEventListeners();
        
        // Initial filter run
        setTimeout(filterContent, 1000);
    }

    // Start when DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();