您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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(); } })();