Provides a somewhat easy way to filter/remove videos on the recommended page on youtube, using their own events instead of using a mutation observer
// ==UserScript==
// @name Youtube Frontpage Filterer
// @namespace http://tampermonkey.net/
// @version 0.01
// @description Provides a somewhat easy way to filter/remove videos on the recommended page on youtube, using their own events instead of using a mutation observer
// @author TetteDev
// @match https://www.youtube.com/*
// @grant GM_addStyle
// @noframes
// @license MIT
// ==/UserScript==
const ParseAbbrevNumber = (input) => {
if (typeof input !== 'string') input = String(input);
const str = input.trim().toUpperCase().replace(/,/g, '');
const m = str.match(/^([0-9]*\.?[0-9]+)\s*([KMB])?$/);
if (!m) return NaN;
const n = parseFloat(m[1]); // use parseFloat even though the video viewcount text never contains decimal values, but might in the future?
const mult = m[2] === 'K' ? 1e3 : m[2] === 'M' ? 1e6 : m[2] === 'B' ? 1e9 : 1;
return n * mult;
};
const ParseVideoElementData = (element) => {
const metaDataContainer = element.querySelector('.yt-lockup-view-model__metadata');
if (!metaDataContainer) {
return null;
}
const urlData = metaDataContainer.querySelector('.yt-lockup-view-model__content-image')?.href ?? metaDataContainer.querySelector('a').href;
const titleData =
metaDataContainer.querySelector('.yt-lockup-metadata-view-model__heading-reset')?.getAttribute('title')
?? (metaDataContainer.querySelector('.yt-lockup-metadata-view-model__title')?.getAttribute('aria-label')
?? 'No Title');
let uploaderData = 'No Uploader';
let viewsData = 'No Views';
let uploadDateData = 'No Upload Date';
// This element contains the uploader, viewcount and upload date
const innerMetaDataContainer = metaDataContainer.querySelector('yt-content-metadata-view-model');
let tmp_0 = null;
let _check = (tmp_0 = innerMetaDataContainer.querySelectorAll('.yt-content-metadata-view-model__metadata-row')).length == 2;
if (_check) {
const [uploaderElement, viewsAndDateElement] = tmp_0;
uploaderData = uploaderElement?.innerText.trim() ?? 'No Uploader';
let tmp_1 = null;
_check = (tmp_1 = viewsAndDateElement.querySelectorAll('.yt-core-attributed-string')).length === 2;
if (_check) {
const [viewCountElement, uploadDateElement] = tmp_1;
// This needs to be converted to a number
viewsData = viewCountElement?.innerText.trim() ?? 'No Views';
if (viewsData !== 'No Views') viewsData = ParseAbbrevNumber(viewsData.split(' ')[0]);
// TODO: Figure out in which format we want the relative date in
uploadDateData = uploadDateElement?.innerText.trim() ?? 'No Upload Date';
}
}
let isMixData = titleData.startsWith('Mix -');
let isWatchedData = element.querySelector('.ytThumbnailOverlayProgressBarHostWatchedProgressBarSegment')?.style.width === '100%' ?? false;
// TODO: Better music detection
const DetermineIsMusic = (titleData, uploaderData) => {
if (uploaderData.toLowerCase().includes(' - topic')) {
return true;
}
const musicKeywords = ['official video', 'official audio', 'lyric video', 'ft.', 'feat.'];
if (musicKeywords.some(keyword => titleData.toLowerCase().includes(keyword))) {
return true;
}
return false;
};
let isMusicData = DetermineIsMusic(titleData, uploaderData);
return {
url: urlData.trim(),
title: titleData.trim(),
uploader: uploaderData.trim(),
views: viewsData,
uploadDateRelative: uploadDateData,
isMix: isMixData,
isWatched: isWatchedData,
isMusic: isMusicData,
};
};
// Dont edit these (or do it im not your mom)
const ParsedAttribute = 'data-parsed';
const HideCssClassName = 'hidden-video';
// Edit these to your likings
// Must be valid CSS
const HideCss = 'display: none !important;';
// Hide videos based on the uploader, case sensitive
const HiddenUploaders = [];
// Hide videos containing some words in their title, case insensitive
const HiddenTitleSubstrings = [];
// Hide videos you have watched fully
const HideWatchedVideos = false;
// Videos with less than this amount will be hidden, -1 disables this feature
const HiddenViewCountThreshhold = -1;
const OnPageUpdated = (e) => {
const action = e.detail.actionName;
const targetActions = ['ytd-update-grid-state-action','yt-rich-grid-resize-observed','yt-append-continuation-items-action'];
if (targetActions.includes(action)) {
['ytd-rich-item-renderer'].forEach(selector => {
const elements = document.querySelectorAll(selector);
elements.forEach(element => {
if (element.hasAttribute(ParsedAttribute)) return;
if (element.classList.contains(HideCssClassName)) return;
const data = ParseVideoElementData(element);
if (data !== null) {
let shouldFilter = false;
shouldFilter ||= (HiddenUploaders.length > 0 ? HiddenUploaders.includes(data.uploader) : false);
shouldFilter ||= (HideWatchedVideos && data.isWatched);
shouldFilter ||= HiddenTitleSubstrings.length > 0 ? HiddenTitleSubstrings.some(substring => data.title.toLowerCase().includes(substring)) : false;
shouldFilter ||= (data.views !== 'No Views' && data.views < HiddenViewCountThreshhold);
// Prevent youtube mixes and music from being hidden
shouldFilter &&= !data.isMusic;
shouldFilter &&= !data.isMix;
if (shouldFilter) {
element.classList.add(HideCssClassName);
}
}
element.setAttribute(ParsedAttribute, 'true');
});
});
}
};
const FilteringDisabled =
HiddenUploaders.length === 0 &&
HiddenTitleSubstrings.length === 0 &&
!HideWatchedVideos &&
HiddenViewCountThreshhold === -1;
if (!FilteringDisabled) {
GM_addStyle(`
.${HideCssClassName} { ${HideCss} }
`);
window.addEventListener('yt-action', OnPageUpdated, { passive: true });
}