您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Filters YouTube videos by duration and age. Hides videos less than X seconds long or older than a specified number of years, excluding channel video tabs.
// ==UserScript== // @name YouTube - Filters // @version 1.4.4 // @description Filters YouTube videos by duration and age. Hides videos less than X seconds long or older than a specified number of years, excluding channel video tabs. // @author Journey Over // @license MIT // @match *://*.youtube.com/* // @require https://cdn.jsdelivr.net/gh/StylusThemes/Userscripts@5f2cbff53b0158ca07c86917994df0ed349eb96c/libs/gm/gmcompat.js // @grant GM.setValue // @grant GM.getValue // @grant GM.registerMenuCommand // @run-at document-body // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com // @homepageURL https://github.com/StylusThemes/Userscripts // @namespace https://greasyfork.org/users/32214 // ==/UserScript== (async function() { 'use strict'; // Retrieve settings or use defaults via GMC (async) const MIN_DURATION_SECONDS = await GMC.getValue('MIN_DURATION_SECONDS', 120); const AGE_THRESHOLD_YEARS = await GMC.getValue('AGE_THRESHOLD_YEARS', 4); const ENABLE_CONSOLE_LOGS = await GMC.getValue('ENABLE_CONSOLE_LOGS', true); const processedVideos = new Set(); // To keep track of processed video containers let scheduledFilter = null; // throttle flag for mutation observer /* ---------------- Settings UI ---------------- */ function openSettingsMenu() { const settingsContainer = document.createElement('div'); settingsContainer.id = 'yt-filters-settings'; settingsContainer.style.position = 'fixed'; settingsContainer.style.top = '50%'; settingsContainer.style.left = '50%'; settingsContainer.style.transform = 'translate(-50%, -50%)'; settingsContainer.style.backgroundColor = '#282c34'; settingsContainer.style.color = '#abb2bf'; settingsContainer.style.padding = '20px'; settingsContainer.style.borderRadius = '10px'; settingsContainer.style.zIndex = '10000'; settingsContainer.style.boxShadow = '0 0 20px rgba(0, 0, 0, 0.5)'; settingsContainer.style.maxWidth = '500px'; settingsContainer.style.maxHeight = '500px'; settingsContainer.style.overflowY = 'auto'; settingsContainer.style.fontFamily = 'Arial, sans-serif'; settingsContainer.innerHTML = ` <div> <h3 style="margin: 0 0 15px 0; font-size: 1.5em; color: #61dafb;">Video Filters Settings</h3> <label for="min-duration" style="display: block; margin-bottom: 5px;">Minimum Duration (seconds):</label> <input type="number" id="min-duration" value="${MIN_DURATION_SECONDS}" style="width: calc(100% - 22px); padding: 8px; border: 1px solid #444c56; border-radius: 5px; background: #3e4451; color: #abb2bf; margin-bottom: 15px;"> <label for="age-threshold" style="display: block; margin-bottom: 5px;">Age Threshold (years):</label> <input type="number" id="age-threshold" value="${AGE_THRESHOLD_YEARS}" style="width: calc(100% - 22px); padding: 8px; border: 1px solid #444c56; border-radius: 5px; background: #3e4451; color: #abb2bf; margin-bottom: 15px;"> <label for="enable-logs" style="display: block; margin-bottom: 5px;">Enable Console Logs:</label> <input type="checkbox" id="enable-logs" ${ENABLE_CONSOLE_LOGS ? 'checked' : ''} style="margin-bottom: 15px;"> <div style="display: flex; justify-content: space-between;"> <button id="save-settings" style="flex: 1; margin-right: 10px; padding: 10px; background-color: #61dafb; color: #282c34; border: none; border-radius: 5px; cursor: pointer;">Save</button> <button id="close-settings" style="flex: 1; padding: 10px; background-color: #e06c75; color: #282c34; border: none; border-radius: 5px; cursor: pointer;">Close</button> </div> </div> `; document.body.appendChild(settingsContainer); const saveButton = document.getElementById('save-settings'); const closeButton = document.getElementById('close-settings'); saveButton.addEventListener('click', async () => { const newMinDuration = parseInt(document.getElementById('min-duration').value, 10); const newAgeThreshold = parseInt(document.getElementById('age-threshold').value, 10); const newEnableLogs = document.getElementById('enable-logs').checked; await GMC.setValue('MIN_DURATION_SECONDS', newMinDuration); await GMC.setValue('AGE_THRESHOLD_YEARS', newAgeThreshold); await GMC.setValue('ENABLE_CONSOLE_LOGS', newEnableLogs); alert('Settings saved!'); settingsContainer.remove(); window.location.reload(); }); closeButton.addEventListener('click', () => { settingsContainer.remove(); }); } // register menu command GMC.registerMenuCommand('Open YouTube Filters Settings', openSettingsMenu); /* ---------------- Utilities ---------------- */ function convertDurationToSeconds(durationText) { // Handles HH:MM:SS, MM:SS, or single "SS" just in case. if (!durationText) return 0; // remove whitespace and any non-digit/: characters const sanitized = durationText.trim().replace(/[^\d:]/g, ''); if (!sanitized) return 0; const parts = sanitized.split(':').map(p => parseInt(p, 10) || 0).reverse(); let seconds = 0; for (let i = 0; i < parts.length; i++) { seconds += parts[i] * Math.pow(60, i); } return seconds; } function isShortVideo(durationInSeconds) { return durationInSeconds < MIN_DURATION_SECONDS && durationInSeconds !== 0; } // Extract "X years ago" style info from a container (works with new and old YouTube DOM) function getVideoAgeTextAndYears(container) { // Search any text nodes or spans near metadata rows that include "ago" const texts = Array.from(container.querySelectorAll('span, .yt-core-attributed-string, .yt-content-metadata-view-model-wiz__metadata-text')) .map(el => el.innerText && el.innerText.trim()) .filter(Boolean); const ageText = texts.find(t => /\bago\b/i.test(t)); if (ageText) { const yearsMatch = ageText.match(/(\d+)\s+(year|years)\s+ago/i); return { text: ageText, years: yearsMatch ? parseInt(yearsMatch[1], 10) : 0 }; } return { text: 'Unknown', years: 0 }; } function getVideoTitle(container) { // new markup const newTitle = container.querySelector('.yt-lockup-metadata-view-model-wiz__title, .yt-lockup-metadata-view-model-wiz__heading-reset a'); if (newTitle) { // the title text may be inside an inner span const inner = newTitle.querySelector('span') || newTitle; return inner.innerText ? inner.innerText.trim() : (newTitle.title || ''); } // fallback to legacy selector const legacy = container.querySelector('#video-title'); return legacy ? legacy.innerText.trim() : ''; } function getDurationText(container) { // try new markup badge text let badge = container.querySelector('.badge-shape-wiz__text, yt-thumbnail-badge-view-model .badge-shape-wiz__text'); if (badge && badge.innerText.trim()) return badge.innerText.trim(); // legacy markup fallback const legacy = container.querySelector('span.ytd-thumbnail-overlay-time-status-renderer, .ytd-thumbnail-overlay-time-status-renderer'); if (legacy && legacy.innerText) return legacy.innerText.trim(); // sometimes duration is on the thumbnail overlay element const overlay = container.querySelector('[aria-label*="duration"], .yt-thumbnail-overlay-time-status-renderer'); if (overlay && overlay.innerText) return overlay.innerText.trim(); return ''; } /* ---------------- Detection of channel / videos tab ---------------- */ function isChannelVideosPage() { // matches /@name/videos or /channel/ /c/ /user/ followed by /videos const path = window.location.pathname || ''; const channelVideosRegex = /^\/(?:(?:@[^\/]+)|(?:channel|c|user)\/[^\/]+)\/videos(\/.*)?$/i; return channelVideosRegex.test(path); } /* ---------------- Main filtering ---------------- */ function collectVideoContainers() { const containers = new Set(); // Preferred: anchors that link to watch pages — covers many renderers including new markup const watchAnchors = document.querySelectorAll('a[href*="/watch?v="]'); watchAnchors.forEach(a => { // try to find a known container ancestor const container = a.closest('div.yt-lockup-view-model-wiz') || a.closest('ytd-rich-item-renderer') || a.closest('ytd-compact-video-renderer') || a.closest('ytd-video-renderer') || a.closest('ytd-playlist-panel-video-renderer') || a.closest('div.yt-lockup') || a.closest('div'); // last-resort (will be filtered) if (container) containers.add(container); }); // Also include older renderer elements that might not have an anchor in the same way Array.from(document.querySelectorAll('ytd-rich-item-renderer, ytd-compact-video-renderer, ytd-video-renderer, div.yt-lockup-view-model-wiz')) .forEach(el => containers.add(el)); return Array.from(containers); } function filterVideos() { // Don't run on channel /videos pages if (isChannelVideosPage()) { if (ENABLE_CONSOLE_LOGS) console.log('[YT Filters] Skipping channel /videos page.'); return; } const containers = collectVideoContainers(); containers.forEach(container => { if (!container || processedVideos.has(container)) return; const title = getVideoTitle(container); const durationText = getDurationText(container) || ''; const durationInSeconds = convertDurationToSeconds(durationText); const { text: videoAgeText, years: videoAgeInYears } = getVideoAgeTextAndYears(container); if (isShortVideo(durationInSeconds)) { if (ENABLE_CONSOLE_LOGS) { console.log(`%cDuration Removal: %c"${title}" %c(${durationText})`, "color: red;", "color: orange;", "color: deepskyblue;"); } container.style.display = 'none'; processedVideos.add(container); } else if (videoAgeInYears >= AGE_THRESHOLD_YEARS) { if (ENABLE_CONSOLE_LOGS) { console.log(`%cAge Removal: %c"${title}" %c(${videoAgeText})`, "color: red;", "color: orange;", "color: deepskyblue;"); } container.style.display = 'none'; processedVideos.add(container); } else { // If previously hidden by you but now ok, don't unhide automatically — keep it simple. processedVideos.add(container); // mark processed to avoid reprocessing } }); } /* ---------------- Mutation observer (throttled) ---------------- */ const observer = new MutationObserver((mutations) => { if (scheduledFilter) return; scheduledFilter = setTimeout(() => { try { filterVideos(); } catch (e) { if (ENABLE_CONSOLE_LOGS) console.error('[YT Filters] Error during filter:', e); } finally { scheduledFilter = null; } }, 250); // small throttle }); observer.observe(document.body, { childList: true, subtree: true }); // Initial run after a short delay to allow content to render setTimeout(filterVideos, 600); })();