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.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();
    }

})();