Last active
June 20, 2023 08:11
-
-
Save rozboris/f0a4fcd087fe23c198a37c0654af1afc to your computer and use it in GitHub Desktop.
Hide Shorts on YouTube
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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