Clean YouTube homepage from sponsored, low-view, and old content
// ==UserScript==
// @name YouTube Feed Purifier
// @namespace http://tampermonkey.net/
// @version 3.4.0
// @description Clean YouTube homepage from sponsored, low-view, and old content
// @match https://www.youtube.com/*
// @match https://youtube.com/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Add custom CSS for our UI
GM_addStyle(`
/* YouTube Feed Purifier - Scoped Styling */
.yt-purifier-settings-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #fff;
color: #0f0f0f;
border: 1px solid #ddd;
border-radius: 12px;
padding: 24px;
z-index: 10000;
width: 90%;
max-width: 800px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 4px 32px rgba(0,0,0,0.4);
font-family: 'Roboto', 'Arial', sans-serif;
animation: yt-purifier-modal-appear 0.2s ease;
}
.yt-purifier-sections-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
margin: 20px 0;
}
@media (max-width: 700px) {
.yt-purifier-sections-grid {
grid-template-columns: 1fr;
}
}
/* Dark theme support */
@media (prefers-color-scheme: dark) {
.yt-purifier-settings-modal {
background: #0f0f0f;
color: #f1f1f1;
border-color: #3a3a3a;
}
}
.yt-purifier-settings-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.7);
z-index: 9999;
animation: yt-purifier-fade-in 0.2s ease;
}
@keyframes yt-purifier-modal-appear {
from {
opacity: 0;
transform: translate(-50%, -48%);
}
to {
opacity: 1;
transform: translate(-50%, -50%);
}
}
@keyframes yt-purifier-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
.yt-purifier-section {
margin: 0;
padding: 20px;
background: #f8f9fa;
border-radius: 12px;
border-left: 4px solid #3ea6ff;
}
@media (prefers-color-scheme: dark) {
.yt-purifier-section {
background: #212121;
}
}
.yt-purifier-toggle {
display: flex;
align-items: center;
justify-content: space-between;
margin: 12px 0;
padding: 8px 0;
}
.yt-purifier-toggle label {
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
user-select: none;
font-size: 14px;
line-height: 20px;
}
.yt-purifier-switch {
position: relative;
display: inline-block;
width: 52px;
height: 28px;
flex-shrink: 0;
}
.yt-purifier-switch input {
opacity: 0;
width: 0;
height: 0;
}
.yt-purifier-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .2s;
border-radius: 34px;
}
@media (prefers-color-scheme: dark) {
.yt-purifier-slider {
background-color: #606060;
}
}
.yt-purifier-slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .2s;
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
input:checked + .yt-purifier-slider {
background-color: #3ea6ff;
}
input:checked + .yt-purifier-slider:before {
transform: translateX(24px);
}
.yt-purifier-select {
width: 100%;
padding: 10px 12px;
margin: 8px 0;
background: white;
color: #0f0f0f;
border: 1px solid #ddd;
border-radius: 8px;
font-family: 'Roboto', 'Arial', sans-serif;
font-size: 14px;
transition: border-color 0.2s;
}
@media (prefers-color-scheme: dark) {
.yt-purifier-select {
background: #0f0f0f;
color: #f1f1f1;
border-color: #606060;
}
}
.yt-purifier-select:focus {
outline: none;
border-color: #3ea6ff;
}
.yt-purifier-input {
width: 100%;
padding: 10px 12px;
margin: 8px 0;
background: white;
color: #0f0f0f;
border: 1px solid #ddd;
border-radius: 8px;
font-family: 'Roboto', 'Arial', sans-serif;
font-size: 14px;
transition: border-color 0.2s;
}
@media (prefers-color-scheme: dark) {
.yt-purifier-input {
background: #0f0f0f;
color: #f1f1f1;
border-color: #606060;
}
}
.yt-purifier-input:focus {
outline: none;
border-color: #3ea6ff;
}
.yt-purifier-button {
background: #3ea6ff;
color: white;
border: none;
padding: 10px 24px;
border-radius: 20px;
cursor: pointer;
font-weight: 500;
font-size: 14px;
font-family: 'Roboto', 'Arial', sans-serif;
margin: 5px;
transition: all 0.2s;
min-height: 36px;
}
.yt-purifier-button:hover {
background: #65b8ff;
transform: translateY(-1px);
}
.yt-purifier-button-secondary {
background: transparent;
color: #606060;
border: 1px solid #ddd;
}
@media (prefers-color-scheme: dark) {
.yt-purifier-button-secondary {
color: #aaa;
border-color: #606060;
}
}
.yt-purifier-button-secondary:hover {
background: rgba(0,0,0,0.05);
border-color: #aaa;
}
@media (prefers-color-scheme: dark) {
.yt-purifier-button-secondary:hover {
background: rgba(255,255,255,0.1);
}
}
.yt-purifier-indicator {
position: absolute;
top: 8px;
right: 8px;
background: rgba(255, 50, 50, 0.95);
color: white;
padding: 4px 10px;
border-radius: 6px;
font-size: 11px;
font-weight: 500;
z-index: 1000;
cursor: help;
backdrop-filter: blur(4px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
font-family: 'Roboto', 'Arial', sans-serif;
letter-spacing: 0.3px;
}
.yt-purifier-toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: #0f0f0f;
color: #f1f1f1;
padding: 14px 24px;
border-radius: 20px;
border: 1px solid #3a3a3a;
z-index: 10001;
backdrop-filter: blur(10px);
display: flex;
align-items: center;
gap: 12px;
animation: yt-purifier-slide-up 0.3s ease;
font-family: 'Roboto', 'Arial', sans-serif;
font-size: 14px;
box-shadow: 0 4px 20px rgba(0,0,0,0.4);
}
@keyframes yt-purifier-slide-up {
from {
transform: translate(-50%, 100%);
opacity: 0;
}
to {
transform: translate(-50%, 0);
opacity: 1;
}
}
.yt-purifier-stats {
position: fixed;
bottom: 24px;
right: 24px;
background: #fff;
color: #0f0f0f;
padding: 16px;
border-radius: 12px;
font-size: 13px;
z-index: 9998;
backdrop-filter: blur(10px);
border: 1px solid #ddd;
min-width: 220px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
font-family: 'Roboto', 'Arial', sans-serif;
}
@media (prefers-color-scheme: dark) {
.yt-purifier-stats {
background: #0f0f0f;
color: #f1f1f1;
border-color: #3a3a3a;
}
}
.yt-purifier-stats-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
font-weight: 500;
font-size: 14px;
color: #3ea6ff;
}
.yt-purifier-stats-row {
display: flex;
justify-content: space-between;
margin: 6px 0;
padding: 4px 0;
border-bottom: 1px solid rgba(0,0,0,0.1);
}
@media (prefers-color-scheme: dark) {
.yt-purifier-stats-row {
border-bottom-color: rgba(255,255,255,0.1);
}
}
.yt-purifier-stats-percentage {
font-weight: 500;
color: #3ea6ff;
}
.yt-purifier-health-score {
margin-top: 12px;
padding: 12px;
background: rgba(62, 166, 255, 0.1);
border-radius: 8px;
text-align: center;
}
@media (prefers-color-scheme: dark) {
.yt-purifier-health-score {
background: rgba(62, 166, 255, 0.15);
}
}
.yt-purifier-health-score-value {
font-size: 28px;
font-weight: 700;
color: #3ea6ff;
margin: 4px 0;
}
.yt-purifier-health-score-label {
font-size: 11px;
color: #606060;
text-transform: uppercase;
letter-spacing: 1px;
}
@media (prefers-color-scheme: dark) {
.yt-purifier-health-score-label {
color: #aaa;
}
}
.yt-purifier-help-text {
font-size: 13px;
color: #606060;
margin-top: 6px;
line-height: 1.5;
font-weight: 400;
}
@media (prefers-color-scheme: dark) {
.yt-purifier-help-text {
color: #aaa;
}
}
.yt-purifier-help-section {
margin-top: 24px;
padding: 20px;
background: rgba(62, 166, 255, 0.08);
border-radius: 10px;
border-left: 4px solid #3ea6ff;
border-top: 1px solid rgba(62, 166, 255, 0.2);
border-right: 1px solid rgba(62, 166, 255, 0.1);
border-bottom: 1px solid rgba(62, 166, 255, 0.1);
}
@media (prefers-color-scheme: dark) {
.yt-purifier-help-section {
background: rgba(62, 166, 255, 0.12);
border-color: rgba(62, 166, 255, 0.3);
}
}
/* UPDATED: Fixed position floating button that works with extension conflicts */
.yt-purifier-floating-button {
position: fixed !important;
top: 10px !important;
right: 10px !important;
background: #3ea6ff !important;
color: white !important;
border: none !important;
border-radius: 50% !important;
width: 40px !important;
height: 40px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
cursor: pointer !important;
z-index: 999999 !important;
box-shadow: 0 2px 8px rgba(0,0,0,0.3) !important;
transition: all 0.2s !important;
font-family: 'Roboto', 'Arial', sans-serif !important;
padding: 0 !important;
margin: 0 !important;
line-height: 1 !important;
text-decoration: none !important;
}
.yt-purifier-floating-button:hover {
background: #65b8ff !important;
transform: scale(1.1) !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.4) !important;
}
.yt-purifier-floating-button:active {
transform: scale(0.95) !important;
}
/* Video hiding with smooth transition */
.yt-purifier-hidden {
opacity: 0.3 !important;
transition: opacity 0.5s ease !important;
pointer-events: none !important;
user-select: none !important;
filter: grayscale(100%) blur(1px) !important;
transform: scale(0.98) !important;
}
.yt-purifier-hidden:hover {
opacity: 0.4 !important;
}
/* Debug panel styling */
.yt-purifier-debug-panel {
position: fixed;
top: 20px;
right: 20px;
background: rgba(15, 15, 15, 0.95);
color: #0f0;
padding: 12px;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 11px;
z-index: 9997;
max-height: 300px;
overflow-y: auto;
max-width: 500px;
border: 1px solid #3ea6ff;
display: none;
backdrop-filter: blur(10px);
}
.yt-purifier-debug-panel.visible {
display: block;
}
.yt-purifier-debug-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
padding-bottom: 4px;
border-bottom: 1px solid #0f0;
}
.yt-purifier-debug-close {
color: #0f0;
background: none;
border: none;
cursor: pointer;
font-size: 16px;
}
.yt-purifier-debug-log {
margin: 4px 0;
padding: 2px 0;
border-bottom: 1px solid rgba(0, 255, 0, 0.2);
}
.yt-purifier-debug-timestamp {
color: #8f8;
margin-right: 8px;
}
.yt-purifier-debug-toggle {
position: fixed;
top: 10px;
right: 10px;
background: #0f0;
color: #000;
border: none;
border-radius: 4px;
padding: 4px 8px;
font-size: 10px;
font-weight: bold;
cursor: pointer;
z-index: 9996;
opacity: 0.7;
}
.yt-purifier-debug-toggle:hover {
opacity: 1;
}
`);
// ====================
// DEBUG LOGGER
// ====================
class DebugLogger {
constructor() {
this.logs = [];
this.maxLogs = 100;
this.panel = null;
this.toggleButton = null;
this.enabled = false;
}
init() {
this.createDebugPanel();
this.createToggleButton();
}
createDebugPanel() {
this.panel = document.createElement('div');
this.panel.className = 'yt-purifier-debug-panel';
const header = document.createElement('div');
header.className = 'yt-purifier-debug-header';
const title = document.createElement('div');
title.textContent = '🛠️ YouTube Purifier Debug Log';
const closeButton = document.createElement('button');
closeButton.className = 'yt-purifier-debug-close';
closeButton.textContent = '×';
closeButton.onclick = () => this.hidePanel();
header.appendChild(title);
header.appendChild(closeButton);
this.panel.appendChild(header);
document.body.appendChild(this.panel);
}
createToggleButton() {
this.toggleButton = document.createElement('button');
this.toggleButton.className = 'yt-purifier-debug-toggle';
this.toggleButton.textContent = 'DEBUG';
this.toggleButton.onclick = () => this.togglePanel();
document.body.appendChild(this.toggleButton);
}
togglePanel() {
if (this.panel.classList.contains('visible')) {
this.hidePanel();
} else {
this.showPanel();
}
}
showPanel() {
this.panel.classList.add('visible');
this.toggleButton.textContent = 'HIDE DEBUG';
this.toggleButton.style.opacity = '1';
}
hidePanel() {
this.panel.classList.remove('visible');
this.toggleButton.textContent = 'DEBUG';
this.toggleButton.style.opacity = '0.7';
}
log(message, data = null) {
if (!this.enabled) return;
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.className = 'yt-purifier-debug-log';
const timestampSpan = document.createElement('span');
timestampSpan.className = 'yt-purifier-debug-timestamp';
timestampSpan.textContent = `[${timestamp}]`;
const messageSpan = document.createElement('span');
messageSpan.textContent = message;
logEntry.appendChild(timestampSpan);
logEntry.appendChild(messageSpan);
if (data) {
const dataSpan = document.createElement('div');
dataSpan.style.color = '#8ff';
dataSpan.style.fontSize = '10px';
dataSpan.style.marginTop = '2px';
dataSpan.textContent = JSON.stringify(data, null, 2).slice(0, 200) + '...';
logEntry.appendChild(dataSpan);
}
this.panel.appendChild(logEntry);
this.logs.push({timestamp, message, data});
// Keep only maxLogs entries
while (this.logs.length > this.maxLogs) {
this.logs.shift();
if (this.panel.children.length > 1) {
this.panel.removeChild(this.panel.children[1]);
}
}
// Auto-scroll to bottom
this.panel.scrollTop = this.panel.scrollHeight;
// Also log to console for development
console.log(`[YouTube Purifier] ${message}`, data || '');
}
setEnabled(enabled) {
this.enabled = enabled;
if (enabled) {
this.toggleButton.style.display = 'block';
this.log('Debug logging enabled');
} else {
this.toggleButton.style.display = 'none';
if (this.panel.classList.contains('visible')) {
this.hidePanel();
}
}
}
clear() {
this.logs = [];
// Remove all log entries except header
while (this.panel.children.length > 1) {
this.panel.removeChild(this.panel.lastChild);
}
}
}
// Create global debug logger instance
const debugLogger = new DebugLogger();
// ====================
// CONFIGURATION SYSTEM
// ====================
const DEFAULT_CONFIG = {
enabled: true,
filters: {
sponsored: {
enabled: true,
strictness: 'medium' // low, medium, high
},
lowViews: {
enabled: true,
threshold: 1000
},
oldContent: {
enabled: true,
maxAgeDays: 90,
exemptCategories: ['Education', 'Science', 'Technology']
}
},
ui: {
showFilteredCount: true,
showFilterIndicators: true,
showToastNotifications: true,
enableDebugLogging: false
},
scope: {
homepage: true,
subscriptions: false,
channelPages: false
}
};
class ConfigManager {
constructor() {
this.config = this.loadConfig();
debugLogger.log('Config loaded', this.config);
this.initMenuCommands();
this.toastTimeout = null;
this.buttonObserver = null;
this.floatingButton = null;
// Start button management
this.manageFloatingButton();
}
loadConfig() {
try {
const saved = GM_getValue('yt_purifier_config');
let config;
if (saved) {
config = {...DEFAULT_CONFIG, ...saved};
// Clean up old config property if it exists
if (config.filters && config.filters.lowViews && config.filters.lowViews.exemptSubscriptions !== undefined) {
delete config.filters.lowViews.exemptSubscriptions;
}
} else {
config = DEFAULT_CONFIG;
}
debugLogger.setEnabled(config.ui.enableDebugLogging);
return config;
} catch (e) {
debugLogger.log('Error loading config, using defaults', e);
return DEFAULT_CONFIG;
}
}
saveConfig() {
GM_setValue('yt_purifier_config', this.config);
debugLogger.setEnabled(this.config.ui.enableDebugLogging);
debugLogger.log('Config saved', this.config);
}
manageFloatingButton() {
// Check if we should show the button on current page
if (!this.shouldShowFloatingButton()) {
this.removeFloatingButton();
return;
}
// Create button if it doesn't exist
if (!this.floatingButton || !document.body.contains(this.floatingButton)) {
this.createFloatingButton();
}
// Set up observer to re-add button if removed by other extensions
this.setupButtonObserver();
}
shouldShowFloatingButton() {
const path = window.location.pathname;
if (!this.config.enabled) {
debugLogger.log('Purifier not enabled');
return false;
}
if (path === '/' || path === '' || path === '/') {
debugLogger.log('On homepage, scope.homepage =', this.config.scope.homepage);
return this.config.scope.homepage;
} else if (path.startsWith('/feed/subscriptions')) {
debugLogger.log('On subscriptions, scope.subscriptions =', this.config.scope.subscriptions);
return this.config.scope.subscriptions;
} else if (path.startsWith('/@') || path.startsWith('/c/') || path.startsWith('/user/') || path.startsWith('/channel/')) {
debugLogger.log('On channel page, scope.channelPages =', this.config.scope.channelPages);
return this.config.scope.channelPages;
}
debugLogger.log('Not on supported page for floating button');
return false;
}
createFloatingButton() {
// Remove existing button if any
this.removeFloatingButton();
debugLogger.log('Creating floating button...');
this.floatingButton = document.createElement('button');
this.floatingButton.className = 'yt-purifier-floating-button';
this.floatingButton.title = 'YouTube Purifier Settings';
this.floatingButton.setAttribute('aria-label', 'YouTube Purifier Settings');
// Create SVG icon
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', '20');
svg.setAttribute('height', '20');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('fill', 'none');
svg.setAttribute('aria-hidden', 'true');
const path1 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path1.setAttribute('d', 'M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z');
path1.setAttribute('fill', 'currentColor');
const path2 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path2.setAttribute('d', 'M19.43 12.98C19.47 12.66 19.5 12.34 19.5 12C19.5 11.66 19.47 11.34 19.43 11.02L21.54 9.37C21.73 9.22 21.78 8.95 21.66 8.73L19.66 5.27C19.54 5.05 19.27 4.96 19.05 5.05L16.56 6.05C16.04 5.66 15.5 5.32 14.87 5.07L14.5 2.42C14.46 2.18 14.25 2 14 2H10C9.75 2 9.54 2.18 9.5 2.42L9.13 5.07C8.5 5.32 7.96 5.66 7.44 6.05L4.95 5.05C4.73 4.96 4.46 5.05 4.34 5.27L2.34 8.73C2.22 8.95 2.27 9.22 2.46 9.37L4.57 11.02C4.53 11.34 4.5 11.66 4.5 12C4.5 12.34 4.53 12.66 4.57 12.98L2.46 14.63C2.27 14.78 2.22 15.05 2.34 15.27L4.34 18.73C4.46 18.95 4.73 19.04 4.95 18.95L7.44 17.95C7.96 18.34 8.5 18.68 9.13 18.93L9.5 21.58C9.54 21.82 9.75 22 10 22H14C14.25 22 14.46 21.82 14.5 21.58L14.87 18.93C15.5 18.68 16.04 18.34 16.56 17.95L19.05 18.95C19.27 19.04 19.54 18.95 19.66 18.73L21.66 15.27C21.78 15.05 21.73 14.78 21.54 14.63L19.43 12.98Z');
path2.setAttribute('fill', 'currentColor');
svg.appendChild(path1);
svg.appendChild(path2);
this.floatingButton.appendChild(svg);
// Add click handler
this.floatingButton.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
this.openSettingsModal();
};
// Append to body - using fixed positioning to avoid conflicts
document.body.appendChild(this.floatingButton);
debugLogger.log('Floating button created and appended to body');
// Verify button is visible
setTimeout(() => {
if (this.floatingButton) {
const rect = this.floatingButton.getBoundingClientRect();
const styles = window.getComputedStyle(this.floatingButton);
debugLogger.log('Floating button status', {
exists: !!this.floatingButton,
inDOM: document.body.contains(this.floatingButton),
visible: rect.width > 0 && rect.height > 0,
display: styles.display,
visibility: styles.visibility,
opacity: styles.opacity,
zIndex: styles.zIndex
});
// If button is not visible, try to force it
if (rect.width === 0 || rect.height === 0 || styles.display === 'none') {
debugLogger.log('Button not visible, attempting to fix...');
this.floatingButton.style.display = 'flex !important';
this.floatingButton.style.visibility = 'visible !important';
this.floatingButton.style.opacity = '1 !important';
}
}
}, 100);
}
removeFloatingButton() {
if (this.floatingButton && this.floatingButton.parentNode) {
this.floatingButton.parentNode.removeChild(this.floatingButton);
debugLogger.log('Floating button removed');
}
this.floatingButton = null;
}
setupButtonObserver() {
// Clean up existing observer
if (this.buttonObserver) {
this.buttonObserver.disconnect();
}
// Create new observer to watch for button removal
this.buttonObserver = new MutationObserver((mutations) => {
let buttonRemoved = false;
for (const mutation of mutations) {
if (mutation.removedNodes.length > 0) {
for (const node of mutation.removedNodes) {
if (node === this.floatingButton ||
(node.contains && node.contains(this.floatingButton))) {
buttonRemoved = true;
break;
}
}
}
if (buttonRemoved) break;
}
if (buttonRemoved ||
(this.floatingButton && !document.body.contains(this.floatingButton))) {
debugLogger.log('Floating button was removed by another extension, re-adding...');
// Re-add button after a short delay
setTimeout(() => {
if (this.shouldShowFloatingButton()) {
this.createFloatingButton();
}
}, 1000);
}
});
// Start observing the body for changes
if (document.body) {
this.buttonObserver.observe(document.body, {
childList: true,
subtree: true
});
debugLogger.log('Button observer started');
}
}
initMenuCommands() {
try {
GM_registerMenuCommand('⚙️ YouTube Purifier Settings', () => this.openSettingsModal());
GM_registerMenuCommand('🔄 Toggle Purifier', () => this.toggleEnabled());
GM_registerMenuCommand('📊 Show Stats', () => this.showStats());
GM_registerMenuCommand('🗑️ Reset Settings', () => this.resetSettings());
GM_registerMenuCommand('🐛 Show Debug Panel', () => debugLogger.showPanel());
} catch (e) {
debugLogger.log('Error registering menu commands:', e);
}
}
toggleEnabled() {
this.config.enabled = !this.config.enabled;
this.saveConfig();
this.showToast(`YouTube Purifier ${this.config.enabled ? 'ENABLED' : 'DISABLED'}`);
setTimeout(() => window.location.reload(), 1500);
}
showToast(message, duration = 3000) {
if (!this.config.ui.showToastNotifications) return;
// Remove existing toast
const existingToast = document.querySelector('.yt-purifier-toast');
if (existingToast) {
existingToast.remove();
}
// Create new toast
const toast = document.createElement('div');
toast.className = 'yt-purifier-toast';
const icon = document.createElement('span');
icon.textContent = '🛡️';
const text = document.createElement('span');
text.textContent = message;
toast.appendChild(icon);
toast.appendChild(text);
document.body.appendChild(toast);
// Auto-remove after duration
clearTimeout(this.toastTimeout);
this.toastTimeout = setTimeout(() => {
if (toast.parentNode) {
toast.remove();
}
}, duration);
}
openSettingsModal() {
this.createSettingsModal();
}
showStats() {
const stats = this.getStats();
this.showToast(`📊 Filtered ${stats.filtered} videos out of ${stats.processed} (${stats.percentage}%)`);
}
getStats() {
const stats = GM_getValue('yt_purifier_stats', { filtered: 0, processed: 0 });
const percentage = stats.processed > 0
? Math.round((stats.filtered / stats.processed) * 100)
: 0;
return {...stats, percentage};
}
resetSettings() {
if (confirm('Are you sure you want to reset all settings to default?')) {
this.config = DEFAULT_CONFIG;
this.saveConfig();
this.showToast('Settings reset to default');
setTimeout(() => window.location.reload(), 1000);
}
}
createSettingsModal() {
// Remove existing modal if any
const existingModal = document.querySelector('.yt-purifier-settings-modal');
const existingOverlay = document.querySelector('.yt-purifier-settings-overlay');
if (existingModal) existingModal.remove();
if (existingOverlay) existingOverlay.remove();
// Create overlay
const overlay = document.createElement('div');
overlay.className = 'yt-purifier-settings-overlay';
// Create modal
const modal = document.createElement('div');
modal.className = 'yt-purifier-settings-modal';
// Header
const header = document.createElement('h2');
header.textContent = '🛡️ YouTube Purifier Settings';
header.style.marginTop = '0';
// Global toggle
const globalSection = document.createElement('div');
globalSection.className = 'yt-purifier-section';
const globalToggle = this.createToggle(
'Enabled',
'enabled',
this.config.enabled,
'Enable or disable the entire purifier'
);
globalSection.appendChild(globalToggle);
// Scope settings
const scopeSection = document.createElement('div');
scopeSection.className = 'yt-purifier-section';
const scopeTitle = document.createElement('h3');
scopeTitle.textContent = '📍 Apply On';
scopeSection.appendChild(scopeTitle);
const scopeToggle1 = this.createToggle(
'Homepage',
'scope-homepage',
this.config.scope.homepage,
'Apply filters on YouTube homepage'
);
scopeSection.appendChild(scopeToggle1);
const scopeToggle2 = this.createToggle(
'Subscriptions feed',
'scope-subscriptions',
this.config.scope.subscriptions,
'Apply filters on subscriptions feed'
);
scopeSection.appendChild(scopeToggle2);
const scopeToggle3 = this.createToggle(
'Channel pages',
'scope-channelpages',
this.config.scope.channelPages,
'Apply filters on channel/video pages'
);
scopeSection.appendChild(scopeToggle3);
// Sponsored filter
const sponsoredSection = document.createElement('div');
sponsoredSection.className = 'yt-purifier-section';
const sponsoredTitle = document.createElement('h3');
sponsoredTitle.textContent = '🎯 Sponsored Content Filter';
sponsoredSection.appendChild(sponsoredTitle);
const sponsoredToggle = this.createToggle(
'Filter sponsored content',
'sponsored-enabled',
this.config.filters.sponsored.enabled,
'Hide videos marked as sponsored or containing promotion'
);
sponsoredSection.appendChild(sponsoredToggle);
const strictnessLabel = document.createElement('label');
strictnessLabel.textContent = 'Strictness level:';
strictnessLabel.style.display = 'block';
strictnessLabel.style.margin = '10px 0 5px 0';
const strictnessSelect = document.createElement('select');
strictnessSelect.className = 'yt-purifier-select';
strictnessSelect.id = 'sponsored-strictness';
const options = [
{value: 'low', text: 'Low (only obvious ads)'},
{value: 'medium', text: 'Medium (most promotions)'},
{value: 'high', text: 'High (all promotional content)'}
];
options.forEach(opt => {
const option = document.createElement('option');
option.value = opt.value;
option.textContent = opt.text;
if (opt.value === this.config.filters.sponsored.strictness) {
option.selected = true;
}
strictnessSelect.appendChild(option);
});
sponsoredSection.appendChild(strictnessLabel);
sponsoredSection.appendChild(strictnessSelect);
// Low views filter
const viewsSection = document.createElement('div');
viewsSection.className = 'yt-purifier-section';
const viewsTitle = document.createElement('h3');
viewsTitle.textContent = '👁️ Low Views Filter';
viewsSection.appendChild(viewsTitle);
const viewsToggle = this.createToggle(
'Filter low-view videos',
'lowviews-enabled',
this.config.filters.lowViews.enabled,
'Hide videos with view count below threshold'
);
viewsSection.appendChild(viewsToggle);
const thresholdLabel = document.createElement('label');
thresholdLabel.textContent = 'Minimum views:';
thresholdLabel.style.display = 'block';
thresholdLabel.style.margin = '10px 0 5px 0';
const thresholdInput = document.createElement('input');
thresholdInput.type = 'number';
thresholdInput.className = 'yt-purifier-input';
thresholdInput.id = 'lowviews-threshold';
thresholdInput.value = this.config.filters.lowViews.threshold;
thresholdInput.min = 0;
thresholdInput.step = 100;
viewsSection.appendChild(thresholdLabel);
viewsSection.appendChild(thresholdInput);
// Old content filter
const ageSection = document.createElement('div');
ageSection.className = 'yt-purifier-section';
const ageTitle = document.createElement('h3');
ageTitle.textContent = '📅 Old Content Filter';
ageSection.appendChild(ageTitle);
const ageToggle = this.createToggle(
'Filter old content',
'age-enabled',
this.config.filters.oldContent.enabled,
'Hide videos older than specified days'
);
ageSection.appendChild(ageToggle);
const ageLabel = document.createElement('label');
ageLabel.textContent = 'Maximum age (days):';
ageLabel.style.display = 'block';
ageLabel.style.margin = '10px 0 5px 0';
const ageInput = document.createElement('input');
ageInput.type = 'number';
ageInput.className = 'yt-purifier-input';
ageInput.id = 'age-maxdays';
ageInput.value = this.config.filters.oldContent.maxAgeDays;
ageInput.min = 1;
ageInput.max = 3650;
ageSection.appendChild(ageLabel);
ageSection.appendChild(ageInput);
// UI settings
const uiSection = document.createElement('div');
uiSection.className = 'yt-purifier-section';
const uiTitle = document.createElement('h3');
uiTitle.textContent = '🎨 UI Settings';
uiSection.appendChild(uiTitle);
const uiToggle1 = this.createToggle(
'Show filtered count',
'ui-showcount',
this.config.ui.showFilteredCount,
'Display stats indicator in corner'
);
uiSection.appendChild(uiToggle1);
const uiToggle2 = this.createToggle(
'Show filter indicators',
'ui-showindicators',
this.config.ui.showFilterIndicators,
'Show why videos were filtered'
);
uiSection.appendChild(uiToggle2);
const uiToggle3 = this.createToggle(
'Show toast notifications',
'ui-shownotifications',
this.config.ui.showToastNotifications,
'Show temporary notifications for actions'
);
uiSection.appendChild(uiToggle3);
const uiToggle4 = this.createToggle(
'Enable debug logging',
'ui-debug',
this.config.ui.enableDebugLogging,
'Show detailed debug information (for troubleshooting)'
);
uiSection.appendChild(uiToggle4);
// Help section
const helpSection = document.createElement('div');
helpSection.className = 'yt-purifier-help-section';
const helpTitle = document.createElement('h3');
helpTitle.textContent = '💡 Why Filter These?';
helpTitle.style.marginTop = '0';
helpTitle.style.marginBottom = '12px';
helpTitle.style.fontSize = '16px';
helpTitle.style.color = '#3ea6ff';
const helpContent = document.createElement('div');
helpContent.style.fontSize = '13px';
// Sponsored content explanation
const sponsoredDiv = document.createElement('div');
sponsoredDiv.style.marginBottom = '14px';
const sponsoredStrong = document.createElement('strong');
sponsoredStrong.textContent = '🎯 Sponsored Content: ';
sponsoredStrong.style.color = '#3ea6ff';
sponsoredDiv.appendChild(sponsoredStrong);
const sponsoredText = document.createTextNode('These are paid promotions that interrupt your organic discovery. Hiding them helps maintain an authentic browsing experience.');
sponsoredDiv.appendChild(sponsoredText);
// Low views explanation
const viewsDiv = document.createElement('div');
viewsDiv.style.marginBottom = '14px';
const viewsStrong = document.createElement('strong');
viewsStrong.textContent = '👁️ Low Views: ';
viewsStrong.style.color = '#3ea6ff';
viewsDiv.appendChild(viewsStrong);
const viewsText = document.createTextNode('Videos with very few views are often low-quality reuploads, AI-generated content, or spam. Filtering these helps surface content with community validation.');
viewsDiv.appendChild(viewsText);
// Old content explanation
const oldDiv = document.createElement('div');
const oldStrong = document.createElement('strong');
oldStrong.textContent = '📅 Old Content: ';
oldStrong.style.color = '#3ea6ff';
oldDiv.appendChild(oldStrong);
const oldText = document.createTextNode('While some evergreen content is valuable, very old videos (especially news, tech, and trends) can be outdated. This filter keeps your feed fresh.');
oldDiv.appendChild(oldText);
helpContent.appendChild(sponsoredDiv);
helpContent.appendChild(viewsDiv);
helpContent.appendChild(oldDiv);
helpSection.appendChild(helpTitle);
helpSection.appendChild(helpContent);
// Buttons
const buttonContainer = document.createElement('div');
buttonContainer.style.display = 'flex';
buttonContainer.style.justifyContent = 'flex-end';
buttonContainer.style.marginTop = '20px';
buttonContainer.style.gap = '10px';
const saveButton = document.createElement('button');
saveButton.className = 'yt-purifier-button';
saveButton.textContent = 'Save';
saveButton.onclick = () => this.saveSettings(modal, overlay);
const cancelButton = document.createElement('button');
cancelButton.className = 'yt-purifier-button yt-purifier-button-secondary';
cancelButton.textContent = 'Cancel';
cancelButton.onclick = () => {
modal.remove();
overlay.remove();
};
buttonContainer.appendChild(cancelButton);
buttonContainer.appendChild(saveButton);
// Create grid container for sections
const sectionsGrid = document.createElement('div');
sectionsGrid.className = 'yt-purifier-sections-grid';
// Add sections to grid
sectionsGrid.appendChild(scopeSection);
sectionsGrid.appendChild(sponsoredSection);
sectionsGrid.appendChild(viewsSection);
sectionsGrid.appendChild(ageSection);
sectionsGrid.appendChild(uiSection);
// Assemble modal
modal.appendChild(header);
modal.appendChild(globalSection);
modal.appendChild(sectionsGrid);
modal.appendChild(helpSection);
modal.appendChild(buttonContainer);
// Add to page
overlay.appendChild(modal);
document.body.appendChild(overlay);
// Close on overlay click
overlay.onclick = (e) => {
if (e.target === overlay) {
modal.remove();
overlay.remove();
}
};
}
createToggle(labelText, id, checked, tooltip) {
const container = document.createElement('div');
container.className = 'yt-purifier-toggle';
const label = document.createElement('label');
label.title = tooltip;
const switchContainer = document.createElement('div');
switchContainer.className = 'yt-purifier-switch';
const input = document.createElement('input');
input.type = 'checkbox';
input.id = id;
input.checked = checked;
const slider = document.createElement('span');
slider.className = 'yt-purifier-slider';
const textSpan = document.createElement('span');
textSpan.textContent = labelText;
switchContainer.appendChild(input);
switchContainer.appendChild(slider);
label.appendChild(switchContainer);
label.appendChild(textSpan);
container.appendChild(label);
return container;
}
saveSettings(modal, overlay) {
// Update config from form
this.config.enabled = document.getElementById('enabled').checked;
// Scope settings
this.config.scope.homepage = document.getElementById('scope-homepage').checked;
this.config.scope.subscriptions = document.getElementById('scope-subscriptions').checked;
this.config.scope.channelPages = document.getElementById('scope-channelpages').checked;
this.config.filters.sponsored.enabled = document.getElementById('sponsored-enabled').checked;
this.config.filters.sponsored.strictness = document.getElementById('sponsored-strictness').value;
this.config.filters.lowViews.enabled = document.getElementById('lowviews-enabled').checked;
this.config.filters.lowViews.threshold = parseInt(document.getElementById('lowviews-threshold').value) || 1000;
this.config.filters.oldContent.enabled = document.getElementById('age-enabled').checked;
this.config.filters.oldContent.maxAgeDays = parseInt(document.getElementById('age-maxdays').value) || 90;
this.config.ui.showFilteredCount = document.getElementById('ui-showcount').checked;
this.config.ui.showFilterIndicators = document.getElementById('ui-showindicators').checked;
this.config.ui.showToastNotifications = document.getElementById('ui-shownotifications').checked;
this.config.ui.enableDebugLogging = document.getElementById('ui-debug').checked;
this.saveConfig();
this.showToast('Settings saved! Reloading...');
modal.remove();
overlay.remove();
setTimeout(() => window.location.reload(), 1000);
}
shouldRunOnCurrentPage() {
const path = window.location.pathname;
if (!this.config.enabled) return false;
if (path === '/' || path === '' || path === '/') {
return this.config.scope.homepage;
} else if (path.startsWith('/feed/subscriptions')) {
return this.config.scope.subscriptions;
} else if (path.startsWith('/@') || path.startsWith('/c/') || path.startsWith('/user/') || path.startsWith('/channel/')) {
return this.config.scope.channelPages;
}
// Default: only run on homepage/feed pages
return path.startsWith('/feed');
}
}
// ====================
// FILTERING ENGINE
// ====================
class YouTubeFilter {
constructor(configManager) {
this.configManager = configManager;
this.config = configManager.config;
this.stats = { filtered: 0, processed: 0 };
this.loadStats();
this.observer = null;
this.processedItems = new WeakSet();
debugLogger.log('YouTubeFilter initialized', {
enabled: this.config.enabled,
filters: this.config.filters
});
}
loadStats() {
try {
const saved = GM_getValue('yt_purifier_stats');
if (saved) this.stats = saved;
} catch (e) {
this.stats = { filtered: 0, processed: 0 };
}
debugLogger.log('Loaded stats', this.stats);
}
saveStats() {
GM_setValue('yt_purifier_stats', this.stats);
}
init() {
if (!this.configManager.shouldRunOnCurrentPage()) {
debugLogger.log('Script should not run on this page, skipping initialization');
return;
}
debugLogger.log('Starting initialization...');
// Initial cleanup
this.cleanupFeed();
// Set up observer for infinite scroll
this.setupObserver();
// Add stats indicator if enabled
if (this.config.ui.showFilteredCount) {
this.addStatsIndicator();
}
// Show welcome message
if (this.config.ui.showToastNotifications) {
setTimeout(() => {
this.configManager.showToast('YouTube Purifier is active', 2000);
}, 1000);
}
debugLogger.log('Initialization complete');
}
setupObserver() {
debugLogger.log('Setting up MutationObserver...');
// Try multiple selectors for YouTube's content container
const selectors = [
'#contents',
'ytd-rich-grid-renderer',
'#primary',
'#items',
'#content',
'ytd-browse[page-subtype="home"]'
];
let targetNode = null;
for (const selector of selectors) {
targetNode = document.querySelector(selector);
if (targetNode) {
debugLogger.log(`Found target node with selector: ${selector}`, {
nodeType: targetNode.nodeName,
id: targetNode.id,
className: targetNode.className
});
break;
}
}
if (!targetNode) {
debugLogger.log('No target node found, will retry in 1 second');
setTimeout(() => this.setupObserver(), 1000);
return;
}
this.observer = new MutationObserver((mutations) => {
let shouldCleanup = false;
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0) {
debugLogger.log('Mutation detected with added nodes', {
addedNodes: mutation.addedNodes.length,
type: mutation.type
});
shouldCleanup = true;
break;
}
}
if (shouldCleanup) {
debugLogger.log('Scheduling cleanup...');
setTimeout(() => this.cleanupFeed(), 500);
}
});
const observerConfig = {
childList: true,
subtree: true
};
this.observer.observe(targetNode, observerConfig);
debugLogger.log('MutationObserver started on target node');
}
cleanupFeed() {
if (!this.configManager.shouldRunOnCurrentPage()) {
debugLogger.log('Script should not run on this page, skipping cleanup');
return;
}
debugLogger.log('Starting cleanup...');
// Try multiple selectors for video items
const videoSelectors = [
'ytd-rich-item-renderer',
'ytd-video-renderer',
'#dismissible',
'ytd-grid-video-renderer'
];
let videoItems = [];
for (const selector of videoSelectors) {
const items = document.querySelectorAll(selector);
if (items.length > 0) {
videoItems = Array.from(items);
debugLogger.log(`Found ${items.length} video items with selector: ${selector}`);
break;
}
}
if (videoItems.length === 0) {
debugLogger.log('No video items found with any selector');
return;
}
debugLogger.log(`Processing ${videoItems.length} video items`);
videoItems.forEach((item, index) => {
if (this.processedItems.has(item)) {
debugLogger.log(`Item ${index} already processed, skipping`);
return;
}
this.stats.processed++;
let shouldHide = false;
let reason = '';
// Extract basic info for debugging
const titleElement = item.querySelector('#video-title, .yt-core-attributed-string, #video-title-link');
const title = titleElement ? titleElement.textContent.substring(0, 50) + '...' : 'No title';
const videoId = item.getAttribute('href') || item.getAttribute('video-id') || 'unknown';
debugLogger.log(`Processing item ${index}: ${title}`, {
id: videoId,
processed: this.stats.processed
});
if (this.config.filters.sponsored.enabled) {
const isSponsored = this.isSponsored(item);
debugLogger.log(`Sponsored check: ${isSponsored ? 'TRUE' : 'FALSE'}`, {
title: title,
result: isSponsored
});
if (isSponsored) {
shouldHide = true;
reason = 'Sponsored';
}
}
if (!shouldHide && this.config.filters.lowViews.enabled) {
const hasLowViews = this.hasLowViews(item);
debugLogger.log(`Low views check: ${hasLowViews ? 'TRUE' : 'FALSE'}`, {
title: title,
result: hasLowViews
});
if (hasLowViews) {
shouldHide = true;
reason = 'Low Views';
}
}
if (!shouldHide && this.config.filters.oldContent.enabled) {
const isTooOld = this.isTooOld(item);
debugLogger.log(`Old content check: ${isTooOld ? 'TRUE' : 'FALSE'}`, {
title: title,
result: isTooOld
});
if (isTooOld) {
shouldHide = true;
reason = 'Old Content';
}
}
if (shouldHide) {
debugLogger.log(`Hiding video: ${title} - Reason: ${reason}`);
this.hideVideo(item, reason);
this.stats.filtered++;
} else {
debugLogger.log(`Keeping video: ${title} - Passed all filters`);
}
this.processedItems.add(item);
});
debugLogger.log('Cleanup complete', {
processed: this.stats.processed,
filtered: this.stats.filtered,
percentage: this.stats.processed > 0 ? Math.round((this.stats.filtered / this.stats.processed) * 100) : 0
});
this.saveStats();
this.updateStatsIndicator();
}
isSponsored(videoElement) {
const strictness = this.config.filters.sponsored.strictness;
let indicators = [];
if (strictness === 'low') {
indicators = ['[sponsored]', '#ad', 'paid partnership'];
} else if (strictness === 'medium') {
indicators = ['[sponsored]', '#ad', 'paid partnership', 'includes paid promotion', 'sponsored by'];
} else { // high
indicators = ['[sponsored]', '#ad', 'paid', 'promotion', 'sponsored', 'partner'];
}
// Check for sponsor thumbnail label (uBlock selector)
const sponsorSelectors = [
'.sponsorThumbnailLabel',
'.sponsorThumbnailLabelVisible',
'[class*="sponsorThumbnail"]',
// More comprehensive selectors
'ytd-ad-badge-renderer',
'[aria-label*="advertisement"]',
'[aria-label*="sponsored"]',
'.video-ad-badge',
'.badge-style-type-ad',
'.ytd-badge-supported-renderer[aria-label*="ad"]'
];
for (const selector of sponsorSelectors) {
const sponsorElements = videoElement.querySelectorAll(selector);
if (sponsorElements.length > 0) {
debugLogger.log(`Sponsored: Found ${sponsorElements.length} elements with selector "${selector}"`);
// Check if visible
for (const elem of sponsorElements) {
const style = window.getComputedStyle(elem);
if (style.display !== 'none' && style.visibility !== 'hidden') {
debugLogger.log(`Sponsored: Visible sponsor element found: ${selector}`);
return true;
}
}
// Presence alone might indicate sponsorship
return true;
}
}
// Check title and metadata
const titleElement = videoElement.querySelector('#video-title, .yt-core-attributed-string, #video-title-link');
if (titleElement) {
const title = titleElement.textContent.toLowerCase();
for (const indicator of indicators) {
if (title.includes(indicator.toLowerCase())) {
debugLogger.log(`Sponsored: Found indicator "${indicator}" in title`, {title: title.substring(0, 100)});
return true;
}
}
}
// Check all text content for sponsor indicators
const allText = videoElement.textContent?.toLowerCase() || '';
if (indicators.some(ind => allText.includes(ind.toLowerCase()))) {
debugLogger.log(`Sponsored: Found indicator in video text`);
return true;
}
return false;
}
hasLowViews(videoElement) {
// First try the specific selectors we found in diagnostics
const metadataSelectors = [
'div.yt-content-metadata-view-model__metadata-row',
'div.yt-lockup-metadata-view-model__metadata',
'yt-content-metadata-view-model.yt-content-metadata-view-model',
'#metadata-line',
'.ytd-video-meta-block'
];
for (const selector of metadataSelectors) {
const elements = videoElement.querySelectorAll(selector);
for (const element of elements) {
const text = element.textContent || '';
debugLogger.log(`Checking views in ${selector}: "${text.substring(0, 100)}"`);
// View patterns in multiple languages
const viewPatterns = [
// Russian
/([\d,\.]+)\s*(тыс\.?|тысяч?|тыс\. просмотр)/i,
/([\d,\.]+[KMB]?)\s*просмотр/i,
// English
/([\d,\.]+[KMB]?)\s*views?/i,
// German
/([\d,\.]+[KMB]?)\s*Aufruf/i,
// French
/([\d,\.]+[KMB]?)\s*vue/i,
// Spanish
/([\d,\.]+[KMB]?)\s*visualizaci/i,
// Japanese
/([\d,\.]+[KMB]?)\s*次視聴/i,
// Generic
/([\d,\.]+[KMB]?)\s*watching/i,
/Live now/i
];
for (const pattern of viewPatterns) {
const match = text.match(pattern);
if (match) {
let views = 0;
const viewStr = match[1].toUpperCase();
// Handle Russian "тыс" (thousand)
if (text.includes('тыс')) {
views = parseFloat(viewStr.replace(',', '.')) * 1000;
}
// Handle K, M, B suffixes
else if (viewStr.includes('K')) {
views = parseFloat(viewStr.replace('K', '')) * 1000;
} else if (viewStr.includes('M')) {
views = parseFloat(viewStr.replace('M', '')) * 1000000;
} else if (viewStr.includes('B')) {
views = parseFloat(viewStr.replace('B', '')) * 1000000000;
} else {
views = parseInt(viewStr.replace(/,/g, '')) || 0;
}
debugLogger.log(`Parsed view count: ${views} from "${viewStr}"`);
const result = views < this.config.filters.lowViews.threshold;
debugLogger.log(`Low views result: ${result} (${views} < ${this.config.filters.lowViews.threshold})`);
return result;
}
}
}
}
// If no view count found, check if it says "No views" or similar
const allText = videoElement.textContent || '';
if (allText.match(/no views|0 views|без просмотр/i)) {
return this.config.filters.lowViews.threshold > 0;
}
debugLogger.log('No view count found in any metadata element');
return false;
}
isTooOld(videoElement) {
// Try the specific selectors we found in diagnostics
const metadataSelectors = [
'div.yt-content-metadata-view-model__metadata-row',
'div.yt-lockup-metadata-view-model__metadata',
'yt-content-metadata-view-model.yt-content-metadata-view-model',
'span.yt-core-attributed-string',
'#metadata-line',
'.ytd-video-meta-block'
];
for (const selector of metadataSelectors) {
const elements = videoElement.querySelectorAll(selector);
for (const element of elements) {
const text = element.textContent || '';
debugLogger.log(`Checking age in ${selector}: "${text.substring(0, 100)}"`);
// Age patterns in multiple languages
const agePatterns = [
// Russian
/(\d+)\s*(минут|час|день|недел|месяц|год)\s*назад/i,
/(\d+)\s*(минут|час|день|недел|месяц|год)/i,
// English
/(\d+)\s*(minutes?|hours?|days?|weeks?|months?|years?)\s*ago/i,
// German
/vor\s*(\d+)\s*(Minuten|Stunden|Tagen|Wochen|Monaten|Jahren)/i,
// French
/il\s*y\s*a\s*(\d+)\s*(minute|heure|jour|semaine|mois|an)/i,
// Spanish
/hace\s*(\d+)\s*(minuto|hora|día|semana|mes|año)/i
];
for (const pattern of agePatterns) {
const match = text.match(pattern);
if (match) {
const amount = parseInt(match[1]);
const unit = match[2] ? match[2].toLowerCase() : '';
let days = 0;
// Russian units
if (unit.includes('год')) {
days = amount * 365;
} else if (unit.includes('месяц')) {
days = amount * 30;
} else if (unit.includes('недел')) {
days = amount * 7;
} else if (unit.includes('день')) {
days = amount;
} else if (unit.includes('час')) {
days = amount / 24;
} else if (unit.includes('минут')) {
days = amount / 1440;
}
// English units
else if (unit.includes('year')) {
days = amount * 365;
} else if (unit.includes('month')) {
days = amount * 30;
} else if (unit.includes('week')) {
days = amount * 7;
} else if (unit.includes('day')) {
days = amount;
} else if (unit.includes('hour')) {
days = amount / 24;
} else if (unit.includes('minute')) {
days = amount / 1440;
}
// German units
else if (unit.includes('jahr')) {
days = amount * 365;
} else if (unit.includes('monat')) {
days = amount * 30;
} else if (unit.includes('woche')) {
days = amount * 7;
} else if (unit.includes('tag')) {
days = amount;
} else if (unit.includes('stunde')) {
days = amount / 24;
} else if (unit.includes('minute')) {
days = amount / 1440;
}
// Default fallback
else {
const firstChar = unit.charAt(0);
if (firstChar === 'y') days = amount * 365;
else if (firstChar === 'm') {
if (unit.includes('month') || unit.includes('месяц') || unit.includes('monat')) {
days = amount * 30;
} else {
days = amount / (24 * 60);
}
} else if (firstChar === 'w') days = amount * 7;
else if (firstChar === 'd') days = amount;
else if (firstChar === 'h') days = amount / 24;
else if (firstChar === 'n' || unit.includes('min')) days = amount / 1440;
}
debugLogger.log(`Age parsed: ${amount} ${unit} = ${days.toFixed(2)} days`);
const result = days > this.config.filters.oldContent.maxAgeDays;
debugLogger.log(`Too old result: ${result} (${days.toFixed(2)} > ${this.config.filters.oldContent.maxAgeDays})`);
return result;
}
}
}
}
debugLogger.log('No age information found');
return false;
}
hideVideo(videoElement, reason) {
debugLogger.log(`Hiding video with reason: ${reason}`);
// Apply smooth transition class
videoElement.classList.add('yt-purifier-hidden');
// Add filter indicator
if (this.config.ui.showFilterIndicators) {
const existingIndicator = videoElement.querySelector('.yt-purifier-indicator');
if (!existingIndicator) {
const indicator = document.createElement('div');
indicator.className = 'yt-purifier-indicator';
indicator.textContent = `🛡️ ${reason}`;
indicator.title = `Filtered: ${reason}\nClick gear icon for settings`;
videoElement.style.position = 'relative';
videoElement.appendChild(indicator);
// Make indicator interactive
indicator.onclick = (e) => {
e.stopPropagation();
this.configManager.openSettingsModal();
};
indicator.style.cursor = 'pointer';
indicator.style.zIndex = '1000';
}
}
}
addStatsIndicator() {
const existing = document.querySelector('.yt-purifier-stats');
if (existing) return;
const statsContainer = document.createElement('div');
statsContainer.className = 'yt-purifier-stats';
const header = document.createElement('div');
header.className = 'yt-purifier-stats-header';
const icon = document.createElement('span');
icon.textContent = '🛡️';
const title = document.createElement('span');
title.textContent = 'Feed Purifier';
header.appendChild(icon);
header.appendChild(title);
const processedRow = document.createElement('div');
processedRow.className = 'yt-purifier-stats-row';
const processedLabel = document.createElement('span');
processedLabel.textContent = 'Videos Processed:';
processedLabel.style.color = '#606060';
const processedValue = document.createElement('span');
processedValue.id = 'yt-purifier-processed';
processedValue.textContent = this.stats.processed.toString();
processedRow.appendChild(processedLabel);
processedRow.appendChild(processedValue);
const filteredRow = document.createElement('div');
filteredRow.className = 'yt-purifier-stats-row';
const filteredLabel = document.createElement('span');
filteredLabel.textContent = 'Videos Filtered:';
filteredLabel.style.color = '#606060';
const filteredValue = document.createElement('span');
filteredValue.id = 'yt-purifier-filtered';
filteredValue.textContent = this.stats.filtered.toString();
filteredValue.style.color = '#ff4e45';
filteredRow.appendChild(filteredLabel);
filteredRow.appendChild(filteredValue);
const percentageRow = document.createElement('div');
percentageRow.className = 'yt-purifier-stats-row';
const percentageLabel = document.createElement('span');
percentageLabel.textContent = 'Filter Rate:';
percentageLabel.style.color = '#606060';
const percentageValue = document.createElement('span');
percentageValue.id = 'yt-purifier-percentage';
percentageValue.className = 'yt-purifier-stats-percentage';
percentageRow.appendChild(percentageLabel);
percentageRow.appendChild(percentageValue);
// Health Score Section
const healthScore = document.createElement('div');
healthScore.className = 'yt-purifier-health-score';
const healthScoreLabel = document.createElement('div');
healthScoreLabel.className = 'yt-purifier-health-score-label';
healthScoreLabel.textContent = 'Feed Health Score';
const healthScoreValue = document.createElement('div');
healthScoreValue.id = 'yt-purifier-health-score';
healthScoreValue.className = 'yt-purifier-health-score-value';
const healthScoreDesc = document.createElement('div');
healthScoreDesc.className = 'yt-purifier-help-text';
healthScoreDesc.textContent = 'Higher is better';
healthScoreDesc.style.marginTop = '4px';
healthScore.appendChild(healthScoreLabel);
healthScore.appendChild(healthScoreValue);
healthScore.appendChild(healthScoreDesc);
const hint = document.createElement('div');
hint.className = 'yt-purifier-help-text';
hint.style.marginTop = '12px';
hint.textContent = 'Click gear icon for settings';
statsContainer.appendChild(header);
statsContainer.appendChild(processedRow);
statsContainer.appendChild(filteredRow);
statsContainer.appendChild(percentageRow);
statsContainer.appendChild(healthScore);
statsContainer.appendChild(hint);
document.body.appendChild(statsContainer);
this.updateStatsIndicator();
debugLogger.log('Added enhanced stats indicator');
}
updateStatsIndicator() {
if (!this.config.ui.showFilteredCount) return;
const processedElem = document.getElementById('yt-purifier-processed');
const filteredElem = document.getElementById('yt-purifier-filtered');
const percentageElem = document.getElementById('yt-purifier-percentage');
const healthScoreElem = document.getElementById('yt-purifier-health-score');
if (!processedElem || !filteredElem || !percentageElem || !healthScoreElem) return;
processedElem.textContent = this.stats.processed.toString();
filteredElem.textContent = this.stats.filtered.toString();
const percentage = this.stats.processed > 0
? Math.round((this.stats.filtered / this.stats.processed) * 100)
: 0;
percentageElem.textContent = `${percentage}%`;
// Calculate health score (100 - filter percentage, but with bonus for keeping quality content)
// Base score: 100% - filter rate, adjusted to favor moderate filtering
const filterRate = this.stats.processed > 0 ? this.stats.filtered / this.stats.processed : 0;
// Ideal filtering is around 20-40% for optimal feed health
let healthScore;
if (filterRate === 0) {
healthScore = 70; // No filtering = moderate health (ads still there)
} else if (filterRate <= 0.1) {
healthScore = 75 + (filterRate * 100); // Light filtering
} else if (filterRate <= 0.3) {
healthScore = 80 + (filterRate * 50); // Good filtering range
} else if (filterRate <= 0.5) {
healthScore = 85 + (filterRate * 30); // Heavy filtering
} else {
healthScore = 90; // Maximum
}
// Cap at 100
healthScore = Math.min(100, Math.round(healthScore));
// Color code based on score
if (healthScore >= 90) {
healthScoreElem.style.color = '#2ba640';
} else if (healthScore >= 70) {
healthScoreElem.style.color = '#3ea6ff';
} else {
healthScoreElem.style.color = '#ffa500';
}
healthScoreElem.textContent = `${healthScore}`;
}
}
// ====================
// INITIALIZATION
// ====================
function init() {
// Initialize debug logger first
debugLogger.init();
debugLogger.log('=== YouTube Purifier Starting ===');
// Wait for page to load
if (!document.body) {
debugLogger.log('Document body not ready, waiting...');
setTimeout(init, 100);
return;
}
debugLogger.log('Document body ready, initializing...');
const configManager = new ConfigManager();
const filter = new YouTubeFilter(configManager);
// Initialize after a short delay to ensure YouTube's DOM is fully loaded
setTimeout(() => {
debugLogger.log('Delayed initialization starting...');
filter.init();
}, 3000);
// Show initial notification
if (configManager.config.enabled && configManager.config.ui.showToastNotifications && configManager.shouldRunOnCurrentPage()) {
setTimeout(() => {
configManager.showToast('YouTube Purifier is active', 2000);
}, 4000);
}
// Re-initialize on page changes (SPA navigation)
let lastUrl = location.href;
const pageObserver = new MutationObserver(() => {
const url = location.href;
if (url !== lastUrl) {
debugLogger.log(`Page changed: ${lastUrl} -> ${url}`);
lastUrl = url;
// Re-manage floating button
setTimeout(() => {
configManager.manageFloatingButton();
debugLogger.log('Re-initializing after page change...');
debugLogger.clear();
filter.init();
}, 2000);
}
});
pageObserver.observe(document, {
subtree: true,
childList: true,
attributes: true,
attributeFilter: ['href']
});
debugLogger.log('Page observer set up');
}
// Start the script
if (document.readyState === 'loading') {
debugLogger.log('Document still loading, adding DOMContentLoaded listener');
document.addEventListener('DOMContentLoaded', init);
} else {
debugLogger.log('Document already loaded, initializing now');
init();
}
})();