// ==UserScript==
// @name YouTube Filter & Sorter (Compact UI)
// @namespace http://tampermonkey.net/
// @version 2.1
// @description STABLE & FINAL FIX. Live filtering is now reliable. Filter & Sort YouTube with a compact UI. Save your favorite settings as the new default.
// @author Opita04
// @license MIT
// @match *://www.youtube.com/*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
(function() {
'use strict';
// --- --- CONFIGURATION (loaded once at start) --- ---
let filtersGloballyEnabled = GM_getValue('filtersGloballyEnabled', true);
const initialValues = {
viewCount: GM_getValue('viewCountThreshold', 1000), viewType: GM_getValue('viewFilterType', 'greater'),
ageEnabled: GM_getValue('ageFilterEnabled', false), ageVal: GM_getValue('ageValue', 1), ageUnit: GM_getValue('ageUnit', 'years'), ageType: GM_getValue('ageFilterType', 'newer'),
durEnabled: GM_getValue('durationFilterEnabled', false), durVal: GM_getValue('durationValue', 10), durUnit: GM_getValue('durationUnit', 'minutes'), durType: GM_getValue('durationFilterType', 'longer'),
sort: GM_getValue('sortOrder', 'default'), panelVisible: GM_getValue('isPanelVisible', true)
};
// --- --- PARSING FUNCTIONS --- ---
function parseViews(v) { if (!v) return 0; const c = v.toLowerCase().replace(/views|,/g, '').trim(), n = parseFloat(c); if (c.includes('k')) return n * 1e3; if (c.includes('m')) return n * 1e6; if (c.includes('b')) return n * 1e9; return parseInt(c, 10) || 0; }
function parseAgeToDays(a) { if (!a) return Infinity; const c = a.toLowerCase(), n = parseInt(c.match(/\d+/)) || 0; if (c.includes('year')) return n * 365; if (c.includes('month')) return n * 30; if (c.includes('week')) return n * 7; if (c.includes('day')) return n; if (c.includes('hour') || c.includes('minute') || c.includes('second')) return 0; return Infinity; }
function parseDurationToSeconds(d) { if (!d) return null; const p = d.trim().split(':').map(Number); if (p.some(isNaN)) return null; let s = 0; if (p.length === 3) s = p[0] * 3600 + p[1] * 60 + p[2]; else if (p.length === 2) s = p[0] * 60 + p[1]; else if (p.length === 1) s = p[0]; return s; }
// --- --- CORE LOGIC --- ---
function getVideoElements() { return document.querySelectorAll('ytd-rich-item-renderer, ytd-video-renderer, ytd-compact-video-renderer, ytd-grid-video-renderer, ytd-playlist-video-renderer'); }
function showAllVideos() { getVideoElements().forEach(v => { if (v.style.display === 'none') v.style.display = ''; }); }
function filterVideos() {
const viewCount = parseInt(document.getElementById('view-input').value, 10) || 0;
const viewType = document.getElementById('view-select').value;
const ageEnabled = document.getElementById('age-check').checked;
const ageVal = parseInt(document.getElementById('age-input').value, 10) || 0;
const ageUnit = document.getElementById('age-unit-select').value;
const ageType = document.getElementById('age-type-select').value;
const durEnabled = document.getElementById('duration-check').checked;
const durVal = parseInt(document.getElementById('duration-input').value, 10) || 0;
const durUnit = document.getElementById('duration-unit-select').value;
const durType = document.getElementById('duration-type-select').value;
const videos = getVideoElements();
const ageThreshold = ageEnabled ? (ageUnit === 'years' ? ageVal * 365 : (ageUnit === 'months' ? ageVal * 30 : ageVal)) : 0;
const durationThreshold = durEnabled ? (durUnit === 'hours' ? durVal * 3600 : durVal * 60) : 0;
videos.forEach(video => {
const metadata = video.querySelectorAll('#metadata-line span');
const durationEl = video.querySelector('#time-status.ytd-thumbnail-overlay-time-status-renderer');
let viewTxt = '', ageTxt = '';
metadata.forEach(span => { const txt = span.textContent.toLowerCase(); if (txt.includes('view')) viewTxt = txt; else if (txt.includes('ago')) ageTxt = txt; });
const views = parseViews(viewTxt), ageDays = parseAgeToDays(ageTxt), durationSecs = durationEl ? parseDurationToSeconds(durationEl.textContent) : null;
let hide = false;
if (views > 0 && ( (viewType === 'greater' && views < viewCount) || (viewType === 'less' && views > viewCount) )) hide = true;
if (!hide && ageEnabled && ageDays !== Infinity && ( (ageType === 'newer' && ageDays > ageThreshold) || (ageType === 'older' && ageDays < ageThreshold) )) hide = true;
if (!hide && durEnabled && durationSecs !== null && ( (durType === 'longer' && durationSecs < durationThreshold) || (durType === 'shorter' && durationSecs > durationThreshold) )) hide = true;
if (video.style.display !== (hide ? 'none' : '')) video.style.display = hide ? 'none' : '';
});
}
function sortVideos() {
const selectedOrder = document.getElementById('sort-order-select').value;
GM_setValue('sortOrder', selectedOrder);
if (selectedOrder === 'default') return;
const container = document.querySelector('#contents.ytd-rich-grid-renderer, #contents.ytd-item-section-renderer, #primary #contents');
if (!container) { console.warn("Filter Script: Could not find video container to sort."); return; }
const videos = Array.from(container.children).filter(el => el.tagName.match(/YTD-(RICH-ITEM|VIDEO)-RENDERER/));
const videosData = videos.map(video => {
const metadata = video.querySelectorAll('#metadata-line span');
const durationEl = video.querySelector('#time-status.ytd-thumbnail-overlay-time-status-renderer');
let viewTxt = '', ageTxt = '';
metadata.forEach(span => { const txt = span.textContent.toLowerCase(); if (txt.includes('view')) viewTxt = txt; else if (txt.includes('ago')) ageTxt = txt; });
return { element: video, views: parseViews(viewTxt), ageDays: parseAgeToDays(ageTxt), durationSeconds: durationEl ? parseDurationToSeconds(durationEl.textContent) : -1 };
});
videosData.sort((a, b) => {
switch (selectedOrder) {
case 'views_desc': return b.views - a.views; case 'views_asc': return a.views - b.views;
case 'age_asc': return a.ageDays - b.ageDays; case 'age_desc': return b.ageDays - a.ageDays;
case 'duration_desc': return b.durationSeconds - a.durationSeconds; case 'duration_asc': return a.durationSeconds - b.durationSeconds;
default: return 0;
}
});
videosData.forEach(vid => container.appendChild(vid.element));
}
function isFilterablePage() { return !window.location.pathname.startsWith('/watch'); }
// --- --- UI & MAIN LOOP --- ---
function createMenu() {
const container = document.createElement('div'); container.id = 'view-filter-container'; document.body.appendChild(container);
const quickToggleButton = document.createElement('button'); quickToggleButton.id = 'quick-toggle-filter-btn'; container.appendChild(quickToggleButton);
function updateQuickToggleButton() { if (filtersGloballyEnabled) { quickToggleButton.textContent = 'Filters On'; quickToggleButton.className = 'filters-on'; } else { quickToggleButton.textContent = 'Filters Off'; quickToggleButton.className = 'filters-off'; } }
quickToggleButton.addEventListener('click', () => {
filtersGloballyEnabled = !filtersGloballyEnabled;
GM_setValue('filtersGloballyEnabled', filtersGloballyEnabled);
updateQuickToggleButton();
mainLogic();
});
const toggleButton = document.createElement('button'); toggleButton.id = 'view-filter-toggle-btn'; container.appendChild(toggleButton);
const panel = document.createElement('div'); panel.id = 'view-filter-panel'; container.appendChild(panel);
panel.innerHTML = `
<h3>View Count Filter</h3><div class="input-row"><input type="number" id="view-input" value="${initialValues.viewCount}"><select id="view-select"><option value="greater" ${initialValues.viewType === 'greater' ? 'selected' : ''}>Greater Than</option><option value="less" ${initialValues.viewType === 'less' ? 'selected' : ''}>Less Than</option></select></div><hr>
<h3>Video Age Filter</h3><label class="filter-label"><input type="checkbox" id="age-check" ${initialValues.ageEnabled ? 'checked' : ''}> Enable Age Filter</label><div class="input-row"><input type="number" id="age-input" value="${initialValues.ageVal}"><select id="age-unit-select"><option value="days" ${initialValues.ageUnit === 'days' ? 'selected' : ''}>Days</option><option value="months" ${initialValues.ageUnit === 'months' ? 'selected' : ''}>Months</option><option value="years" ${initialValues.ageUnit === 'years' ? 'selected' : ''}>Years</option></select></div><select id="age-type-select"><option value="newer" ${initialValues.ageType === 'newer' ? 'selected' : ''}>Newer Than</option><option value="older" ${initialValues.ageType === 'older' ? 'selected' : ''}>Older Than</option></select><hr>
<h3>Video Duration Filter</h3><label class="filter-label"><input type="checkbox" id="duration-check" ${initialValues.durEnabled ? 'checked' : ''}> Enable Duration Filter</label><div class="input-row"><input type="number" id="duration-input" value="${initialValues.durVal}"><select id="duration-unit-select"><option value="minutes" ${initialValues.durUnit === 'minutes' ? 'selected' : ''}>Minutes</option><option value="hours" ${initialValues.durUnit === 'hours' ? 'selected' : ''}>Hours</option></select></div><select id="duration-type-select"><option value="longer" ${initialValues.durType === 'longer' ? 'selected' : ''}>Longer Than</option><option value="shorter" ${initialValues.durType === 'shorter' ? 'selected' : ''}>Shorter Than</option></select><hr>
<h3>Sort Order</h3><div class="input-row"><select id="sort-order-select"><option value="default" ${initialValues.sort === 'default' ? 'selected' : ''}>Default</option><option value="views_desc" ${initialValues.sort === 'views_desc' ? 'selected' : ''}>Views (High-Low)</option><option value="views_asc" ${initialValues.sort === 'views_asc' ? 'selected' : ''}>Views (Low-High)</option><option value="age_asc" ${initialValues.sort === 'age_asc' ? 'selected' : ''}>Age (New-Old)</option><option value="age_desc" ${initialValues.sort === 'age_desc' ? 'selected' : ''}>Age (Old-New)</option><option value="duration_desc" ${initialValues.sort === 'duration_desc' ? 'selected' : ''}>Duration (Long-Short)</option><option value="duration_asc" ${initialValues.sort === 'duration_asc' ? 'selected' : ''}>Duration (Short-Long)</option></select><button id="sort-now-btn">Sort Now</button></div><hr>
<button id="save-btn">Save as Default</button>
`;
const saveButton = document.getElementById('save-btn');
document.getElementById('sort-now-btn').addEventListener('click', sortVideos);
saveButton.addEventListener('click', () => {
GM_setValue('viewCountThreshold', parseInt(document.getElementById('view-input').value, 10)); GM_setValue('viewFilterType', document.getElementById('view-select').value);
GM_setValue('ageFilterEnabled', document.getElementById('age-check').checked); GM_setValue('ageValue', parseInt(document.getElementById('age-input').value, 10)); GM_setValue('ageUnit', document.getElementById('age-unit-select').value); GM_setValue('ageFilterType', document.getElementById('age-type-select').value);
GM_setValue('durationFilterEnabled', document.getElementById('duration-check').checked); GM_setValue('durationValue', parseInt(document.getElementById('duration-input').value, 10)); GM_setValue('durationUnit', document.getElementById('duration-unit-select').value); GM_setValue('durationFilterType', document.getElementById('duration-type-select').value);
GM_setValue('sortOrder', document.getElementById('sort-order-select').value);
const originalText = saveButton.textContent; saveButton.textContent = 'Saved!'; saveButton.style.backgroundColor = '#27813a'; saveButton.disabled = true;
setTimeout(() => { saveButton.textContent = originalText; saveButton.style.backgroundColor = '#3ea6ff'; saveButton.disabled = false; }, 1500);
});
let isPanelVisible = initialValues.panelVisible;
function updatePanelVisibility() { if (isPanelVisible) { panel.style.display = 'block'; toggleButton.textContent = 'Hide Controls'; } else { panel.style.display = 'none'; toggleButton.textContent = 'Show Controls'; } }
toggleButton.addEventListener('click', () => { isPanelVisible = !isPanelVisible; GM_setValue('isPanelVisible', isPanelVisible); updatePanelVisibility(); });
updateQuickToggleButton(); updatePanelVisibility();
// Start the main logic loop ONLY AFTER the menu is created to prevent race conditions
setInterval(mainLogic, 500);
}
function mainLogic() {
const menuContainer = document.getElementById('view-filter-container');
if (!menuContainer) return;
if (isFilterablePage()) {
if (menuContainer.style.display !== 'flex') menuContainer.style.display = 'flex';
if (filtersGloballyEnabled) { filterVideos(); } else { showAllVideos(); }
} else {
if (menuContainer.style.display !== 'none') menuContainer.style.display = 'none';
showAllVideos();
}
}
GM_addStyle(`
#view-filter-container { position: fixed; top: 80px; right: 20px; z-index: 9999; display: flex; flex-direction: column; align-items: flex-end; }
#quick-toggle-filter-btn, #view-filter-toggle-btn { border: 1px solid #3f3f3f; padding: 5px 10px; cursor: pointer; border-radius: 5px; margin-bottom: 5px; order: -1; font-weight: bold; }
#quick-toggle-filter-btn.filters-on { background-color: #27813a; color: white; }
#quick-toggle-filter-btn.filters-off { background-color: #555; color: #ddd; }
#view-filter-toggle-btn { background-color: #0f0f0f; color: white; }
#view-filter-panel { background-color: #282828; color: white; border: 1px solid #3f3f3f; padding: 15px; border-radius: 8px; width: 220px; box-shadow: 0 4px 8px rgba(0,0,0,0.3); }
#view-filter-panel h3 { margin: 10px 0; font-size: 16px; text-align: center; border-bottom: 1px solid #444; padding-bottom: 5px; }
#view-filter-panel h3:first-of-type { margin-top: 0; }
#view-filter-panel input, #view-filter-panel select, #view-filter-panel button { display: block; width: 100%; box-sizing: border-box; padding: 8px; border-radius: 4px; border: 1px solid #555; background-color: #1e1e1e; color: white; }
#view-filter-panel hr { border: none; border-top: 1px solid #444; margin: 20px 0; }
.filter-label { display: flex; align-items: center; margin-bottom: 10px; }
.filter-label input[type="checkbox"] { width: auto; margin-right: 10px; }
.input-row { display: flex; gap: 10px; align-items: center; margin-bottom: 10px; }
.input-row > * { margin-bottom: 0 !important; }
.input-row > input, .input-row > select { flex: 1; }
#sort-now-btn { flex: 0 1 auto; }
#save-btn { background-color: #3ea6ff; color: black; font-weight: bold; margin-top: 10px; transition: background-color 0.2s; }
#sort-now-btn { background-color: #da860a; color: white; font-weight: bold; }
#save-btn:hover, #sort-now-btn:hover { opacity: 0.9; }
`);
// Wait until the page body is ready before creating the menu and starting the loop
if (document.body) {
createMenu();
} else {
document.addEventListener('DOMContentLoaded', createMenu);
}
})();