Skip to content

Instantly share code, notes, and snippets.

@rozboris
Last active June 20, 2023 08:11
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rozboris/f0a4fcd087fe23c198a37c0654af1afc to your computer and use it in GitHub Desktop.
Save rozboris/f0a4fcd087fe23c198a37c0654af1afc to your computer and use it in GitHub Desktop.
Hide Shorts on YouTube
// ==UserScript==
// @name Hide Shorts on YouTube
// @version 4
// @description Hides vides with #shorts tag or the ones shorter than 1 minute on Youtube Subscriptions and Home page
// @author rozboris
// @include https://*.youtube.com/*
// @include https://youtube.com/*
// @grant GM_addStyle
// @updateURL https://gist.githubusercontent.com/rozboris/f0a4fcd087fe23c198a37c0654af1afc/raw/yt-hide-shorts.user.js
// ==/UserScript==
// Heavily inspired by similar greasemonkey: https://github.com/EvHaus/youtube-hide-watched
(function (_undefined) {
// Enable for debugging
const __DEV__ = false;
const LENGTH_LIMIT_SECONDS = 63; //some shorts are exactly 60 seconds
const logDebug = (msg) => {
// eslint-disable-next-line no-console
if (__DEV__) console.log(msg);
};
GM_addStyle(`
.yt-gm-short {
display: none !important;
}
.yt-gm-hidden-row-parent {padding-bottom: 10px}
`);
const debounce = function (func, wait, immediate) {
let timeout;
return (...args) => {
const later = () => {
timeout = null;
if (!immediate) func.apply(this, args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(this, args);
};
};
// ===========================================================
const timeToSeconds = function(timeString) {
const parts = timeString.split(':');
return parts.map((elem, index) => {
const secondsInUnit = 60 ** (parts.length - index - 1); // i.e. if there is 3 elements in `parts` it means the first number is hours which has 60^2 seconds.
return (+elem) * secondsInUnit
}).reduce((a, b) => a + b); // add all up
}
const findShorts = function () {
const withTag = Array.from(document.querySelectorAll('#video-title[title*="#shorts"]'));
const withOverlay = Array.from(document.querySelectorAll('ytd-thumbnail-overlay-time-status-renderer[overlay-style="SHORTS"]'));
const withShortsLink = Array.from(document.querySelectorAll('a#thumbnail[href *= "/shorts/"]'));
const underMinute = Array.from(document.querySelectorAll('.ytd-thumbnail-overlay-time-status-renderer')) // get elements that have video length in them, i.e. '17:42'
.filter((item, _i) => {
const text = item.textContent.replaceAll('\n', '').replaceAll(' ', '');
if (!text) {
return false; // ignore elements without text
}
const length = timeToSeconds(text);
//console.log({text, length})
return length < LENGTH_LIMIT_SECONDS;
});
return withTag.concat(underMinute).concat(withOverlay).concat(withShortsLink);
};
const determineYoutubeSection = function () {
let youtubeSection = 'misc';
if (window.location.href.indexOf('/watch?') > 0) {
youtubeSection = 'watch';
} else if (window.location.href.match(/.*\/(user|channel|c)\/.+\/videos/u)) {
youtubeSection = 'channel';
} else if (window.location.href.indexOf('/feed/subscriptions') > 0) {
youtubeSection = 'subscriptions';
} else if (window.location.href.indexOf('/feed/trending') >= 0) {
youtubeSection = 'trending';
} else if (window.location.href.indexOf('/playlist?') >= 0) {
youtubeSection = 'playlist';
}
return youtubeSection;
};
// ===========================================================
const updateClassOnShorts = function () {
// If we're on the History page -- do nothing.
if (window.location.href.indexOf('/feed/history') >= 0) return;
const section = determineYoutubeSection();
findShorts().forEach((item, _i) => {
// "Subscription" section needs us to hide the "#contents",
// but in the "Trending" section, that class will hide everything.
// So there, we need to hide the "ytd-video-renderer"
let shortsItem;
if (section === 'subscriptions') {
// For rows, hide the row and the header too. We can't hide
// their entire parent because then we'll get the infinite
// page loader to load forever.
shortsItem = (
// Grid item
item.closest('.ytd-grid-renderer') ||
item.closest('.ytd-item-section-renderer') ||
item.closest('ytd-rich-item-renderer') ||
// List item
item.closest('#grid-container')
);
// If we're hiding the .ytd-item-section-renderer element, we need to give it
// some extra spacing otherwise we'll get stuck in infinite page loading
if (shortsItem && shortsItem.classList.contains('ytd-item-section-renderer')) {
shortsItem.closest('ytd-item-section-renderer').classList.add('yt-gm-hidden-row-parent');
}
} else if (section === 'channel') {
// Channel "Videos" section needs special handling
shortsItem = item.closest('.ytd-grid-renderer');
} else if (section === 'playlist') {
shortsItem = item.closest('ytd-playlist-video-renderer');
} else if (section === 'watch') {
shortsItem = item.closest('ytd-compact-video-renderer');
// Don't hide video if it's going to play next.
//
// If there is no watchedItem - we probably got
// `ytd-playlist-panel-video-renderer`:
// let's also ignore it as in case of shuffle enabled
// we could accidentially hide the item which gonna play next.
if (
shortsItem &&
shortsItem.closest('ytd-compact-autoplay-renderer')
) shortsItem = null;
} else {
// For home page and other areas
shortsItem = (
item.closest('ytd-rich-item-renderer') ||
item.closest('ytd-video-renderer') ||
item.closest('ytd-grid-video-renderer')
);
}
if (shortsItem) {
// Add current class
shortsItem.classList.add('yt-gm-short');
}
});
};
const run = debounce((mutations) => {
logDebug('[YT-GM] Running check for shorts');
updateClassOnShorts();
}, 250);
// ===========================================================
// Hijack all XHR calls
const send = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function (data) {
this.addEventListener('readystatechange', function () {
if (
// Anytime more videos are fetched -- re-run script
this.responseURL.indexOf('browse_ajax?action_continuation') > 0
) {
setTimeout(() => {
run();
}, 0);
}
}, false);
send.call(this, data);
};
// ===========================================================
const observeDOM = (function () {
const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
const eventListenerSupported = window.addEventListener;
return function (obj, callback) {
logDebug('[YT-GM] Attaching DOM listener');
// Invalid `obj` given
if (!obj) return;
if (MutationObserver) {
const obs = new MutationObserver(((mutations, _observer) => {
if (mutations[0].addedNodes.length || mutations[0].removedNodes.length) {
// eslint-disable-next-line callback-return
callback(mutations);
}
}));
obs.observe(obj, {childList: true, subtree: true});
} else if (eventListenerSupported) {
obj.addEventListener('DOMNodeInserted', callback, false);
obj.addEventListener('DOMNodeRemoved', callback, false);
}
};
}());
// ===========================================================
logDebug('[YT-GM] Starting Script');
// YouTube does navigation via history and also does a bunch
// of AJAX video loading. In order to ensure we're always up
// to date, we have to listen for ANY DOM change event, and
// re-run our script.
observeDOM(document.body, run);
run();
}());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment