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.

// ==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();
    }

})();