// ==UserScript==
// @name YouTube - Filters
// @version 2.3.0
// @description Filter out unwanted content on YouTube to enhance your browsing experience. (Currently is able to filter videos based on age and members-only status)
// @author Journey Over
// @license MIT
// @match *://*.youtube.com/*
// @match *://*.youtube-nocookie.com/*
// @require https://cdn.jsdelivr.net/gh/StylusThemes/Userscripts@0171b6b6f24caea737beafbc2a8dacd220b729d8/libs/utils/utils.min.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
// @downloadURL https://github.com/StylusThemes/Userscripts/raw/main/userscripts/youtube-filters.user.js
// @updateURL https://github.com/StylusThemes/Userscripts/raw/main/userscripts/youtube-filters.user.js
// ==/UserScript==
(async function() {
'use strict';
// ---------- Constants ----------
const TITLE_SELECTORS = [
'a#video-title',
'h3 .yt-lockup-metadata-view-model__title span.yt-core-attributed-string',
'.yt-lockup-view-model__content-image span.yt-core-attributed-string',
'span.yt-core-attributed-string[role="text"]',
'a.yt-lockup-metadata-view-model__title span.yt-core-attributed-string',
'a.yt-lockup-metadata-view-model__title',
'yt-formatted-string#video-title',
'yt-formatted-string[id="video-title"]',
'yt-formatted-string[class="style-scope ytd-video-renderer"]',
'a#video-title-link span.yt-core-attributed-string',
'span.ytp-modern-videowall-still-info-title'
];
const VIDEO_SELECTORS = [
'ytd-rich-item-renderer',
'yt-lockup-view-model',
'ytd-grid-video-renderer',
'ytd-video-renderer',
'ytd-compact-video-renderer',
'ytd-playlist-video-renderer',
'ytd-playlist-panel-video-renderer',
'ytd-radio-renderer',
'ytd-reel-item-renderer',
'ytd-reel-video-renderer',
'a.ytp-modern-videowall-still'
];
const AGE_SELECTORS = [
'span.inline-metadata-item.style-scope.ytd-video-meta-block',
'span.yt-content-metadata-view-model__metadata-text',
'span.ytp-modern-videowall-still-view-count-and-date-info'
];
const MEMBERS_SELECTORS = [
'.badge.badge-style-type-members-only',
'badge-shape[aria-label*="Members only" i]',
'.yt-badge-shape--commerce .yt-badge-shape__text',
'.yt-badge-shape__text'
];
const MEMBERS_REGEX = /\bmembers\s*[- ]?\s*only\b/i;
// ---------- Settings ----------
let DEBUG_ENABLED = GM_getValue('DEBUG_ENABLED', false);
const logger = Logger('YT - Filters', { debug: DEBUG_ENABLED });
let AGE_THRESHOLD = GM_getValue('AGE_THRESHOLD', { value: 4, unit: 'years' });
let MEMBERS_ONLY_ENABLED = GM_getValue('MEMBERS_ONLY_ENABLED', false);
let AGE_FILTERING_ENABLED = GM_getValue('AGE_FILTERING_ENABLED', true);
const processedVideos = new WeakSet();
// ---------- Utility Functions ----------
function convertToYears(value, unit) {
const conversions = {
minutes: 525600,
hours: 8760,
days: 365,
weeks: 52,
months: 12,
years: 1
};
return value / (conversions[unit] || 1);
}
function matchesAnySelector(element, selectors) {
return selectors.some(selector => element.matches(selector));
}
function queryAll(root, selectors) {
return root.querySelectorAll(selectors.join(','));
}
// ---------- Video Processing ----------
function getVideoAgeTextAndYears(videoElement) {
for (const ageElement of queryAll(videoElement, AGE_SELECTORS)) {
const ageText = (ageElement.textContent || '').trim();
if (/\bago\b/i.test(ageText)) {
const ageMatch = ageText.match(/(\d+)\s+(minute|hour|day|week|month|year)s?\s+ago/i);
if (ageMatch) {
const ageValue = parseInt(ageMatch[1], 10);
const ageUnit = ageMatch[2].toLowerCase();
const ageInYears = convertToYears(ageValue, ageUnit.includes('minute') ? 'minutes' :
ageUnit.includes('hour') ? 'hours' :
ageUnit.includes('day') ? 'days' :
ageUnit.includes('week') ? 'weeks' :
ageUnit.includes('month') ? 'months' :
'years');
return { text: ageText, years: ageInYears };
}
return { text: ageText, years: 0 };
}
}
return { text: 'Unknown', years: 0 };
}
function getVideoTitle(videoElement) {
for (const titleSelector of TITLE_SELECTORS) {
const titleElement = videoElement.querySelector(titleSelector);
if (titleElement && titleElement.innerText.trim()) {
return titleElement.innerText.trim();
}
}
return '';
}
function hideVideo(videoElement, reason) {
for (const selector of VIDEO_SELECTORS) {
const videoContainer = videoElement.closest(selector);
if (videoContainer) {
try {
videoContainer.setAttribute('hidden', 'true');
} catch {
videoContainer.style.display = 'none';
}
}
}
logger.debug(`Hidden "${getVideoTitle(videoElement)}" (${reason})`);
}
// ---------- Age Filtering ----------
function filterVideoByAge(videoElement) {
if (processedVideos.has(videoElement)) return;
const { text: ageText, years: ageYears } = getVideoAgeTextAndYears(videoElement);
if (ageText === 'Unknown') return;
processedVideos.add(videoElement);
videoElement.dataset.processed = 'true';
const thresholdInYears = convertToYears(AGE_THRESHOLD.value, AGE_THRESHOLD.unit);
if (ageYears >= thresholdInYears) {
hideVideo(videoElement, ageText);
}
}
// ---------- Members-Only Filtering ----------
function isMembersOnlyBadge(badge) {
if (badge.classList.contains('badge-style-type-members-only')) return true;
const label = badge.getAttribute('aria-label') || badge.textContent || '';
return MEMBERS_REGEX.test(label);
}
function removeMembersOnlyVideo(badge) {
const videoElement = badge.closest(VIDEO_SELECTORS.join(','));
if (videoElement) {
videoElement.remove();
logger.debug(`Removed Members-only "${getVideoTitle(videoElement)}"`);
}
}
function pruneMembersShelf(root = document) {
for (const shelf of root.querySelectorAll('ytd-shelf-renderer')) {
const title = (shelf.querySelector('#title')?.textContent || '').trim();
const subtitle = (shelf.querySelector('#subtitle')?.textContent || '').trim();
if (MEMBERS_REGEX.test(title) || /videos available to members/i.test(subtitle)) {
shelf.remove();
}
}
}
function scanForMembersOnly(root = document) {
for (const badge of queryAll(root, MEMBERS_SELECTORS)) {
if (isMembersOnlyBadge(badge)) {
removeMembersOnlyVideo(badge);
}
}
pruneMembersShelf(root);
}
// ---------- Observers ----------
async function observeNewVideos() {
while (true) {
try {
const unprocessedVideos = [...document.querySelectorAll(
VIDEO_SELECTORS.map(selector => `${selector}:not([data-processed])`).join(',')
)];
for (const videoElement of unprocessedVideos) {
// Age filtering only on non-channel pages
if (AGE_FILTERING_ENABLED && !window.location.href.includes('@')) {
filterVideoByAge(videoElement);
}
}
if (MEMBERS_ONLY_ENABLED) {
pruneMembersShelf();
}
} catch (error) {
logger.error(error);
}
await new Promise(resolve => setTimeout(resolve, 50));
}
}
function observeMembersOnly() {
const observer = new MutationObserver(mutations => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
for (const node of mutation.addedNodes) {
if (!(node instanceof Element)) continue;
if (matchesAnySelector(node, MEMBERS_SELECTORS) && isMembersOnlyBadge(node)) {
removeMembersOnlyVideo(node);
} else {
scanForMembersOnly(node);
}
}
}
}
});
observer.observe(document.documentElement, { childList: true, subtree: true });
const rescan = () => setTimeout(() => scanForMembersOnly(document), 50);
window.addEventListener('yt-navigate-finish', rescan);
window.addEventListener('yt-page-data-updated', rescan);
}
// ---------- Initialization ----------
observeNewVideos();
if (MEMBERS_ONLY_ENABLED) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
scanForMembersOnly();
observeMembersOnly();
});
} else {
scanForMembersOnly();
observeMembersOnly();
}
}
// ---------- Settings UI ----------
function toggleSwitch(element) {
element.classList.toggle('active');
}
function handleToggle(element) {
toggleSwitch(element);
element.setAttribute('aria-checked', element.classList.contains('active'));
}
function openSettingsMenu() {
if (document.getElementById('yt-filters-settings')) return;
const overlay = document.createElement('div');
overlay.id = 'yt-filters-overlay';
overlay.style = `position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);backdrop-filter:blur(12px);display:flex;align-items:center;justify-content:center;z-index:10000;opacity:0;animation:overlayFadeIn 0.4s ease forwards;`;
const modal = document.createElement('div');
modal.id = 'yt-filters-settings';
modal.style = `background:linear-gradient(135deg,#1a1a1a 0%,#2d2d2d 100%);color:#e4e4e4;border-radius:20px;width:420px;max-width:95vw;max-height:85vh;overflow:hidden;box-shadow:0 25px 50px rgba(0,0,0,0.5),0 0 0 1px rgba(255,255,255,0.1);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;transform:translateY(30px) scale(0.95);opacity:0;animation:modalSlideIn 0.4s cubic-bezier(0.34,1.56,0.64,1) 0.1s forwards;display:flex;flex-direction:column;`;
modal.innerHTML = `
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
// Toggle switches with keyboard support
const membersToggle = document.getElementById('members-only-toggle');
const debugToggle = document.getElementById('debug-toggle');
const ageToggle = document.getElementById('age-filtering-toggle');
membersToggle.addEventListener('click', () => handleToggle(membersToggle));
membersToggle.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleToggle(membersToggle);
}
});
debugToggle.addEventListener('click', () => handleToggle(debugToggle));
debugToggle.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleToggle(debugToggle);
}
});
ageToggle.addEventListener('click', () => handleToggle(ageToggle));
ageToggle.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleToggle(ageToggle);
}
});
document.getElementById('save-settings').addEventListener('click', () => {
const thresholdValue = parseFloat(document.getElementById('age-threshold').value);
const thresholdUnit = document.getElementById('age-unit').value;
AGE_THRESHOLD = { value: thresholdValue, unit: thresholdUnit };
GM_setValue('AGE_THRESHOLD', AGE_THRESHOLD);
MEMBERS_ONLY_ENABLED = membersToggle.classList.contains('active');
GM_setValue('MEMBERS_ONLY_ENABLED', MEMBERS_ONLY_ENABLED);
AGE_FILTERING_ENABLED = ageToggle.classList.contains('active');
GM_setValue('AGE_FILTERING_ENABLED', AGE_FILTERING_ENABLED);
DEBUG_ENABLED = debugToggle.classList.contains('active');
GM_setValue('DEBUG_ENABLED', DEBUG_ENABLED);
// Update logger if debug changed
if (logger.debug !== DEBUG_ENABLED) {
logger.debug = DEBUG_ENABLED;
}
closeModal();
});
document.getElementById('close-settings').addEventListener('click', closeModal);
overlay.addEventListener('click', event => {
if (event.target === overlay) closeModal();
});
function closeModal() {
modal.style.animation = 'modalSlideIn 0.3s ease reverse';
overlay.style.animation = 'overlayFadeIn 0.3s ease reverse';
setTimeout(() => overlay.remove(), 300);
}
}
GM_registerMenuCommand('Open YouTube Filters Settings', openSettingsMenu);
})();