Last active
January 2, 2026 08:59
-
-
Save zeyus/dae59e6a4d709bdb8a041a45634450cc to your computer and use it in GitHub Desktop.
Mark + add a border around youtube thumbnails based on video length, disable shorts: Violentmonkey / Greasemonkey / userscript
This file contains hidden or 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 Long Videos are Best Videos | |
| // @namespace zeyus | |
| // @match https://www.youtube.com/* | |
| // @grant GM.registerMenuCommand | |
| // @grant GM.getValues | |
| // @grant GM.setValues | |
| // @inject-into auto | |
| // @version 2.3 | |
| // @author zeyus | |
| // @homepageURL https://www.zeyus.com/ | |
| // @supportURL https://github.com/zeyus | |
| // @downloadURL https://gist.github.com/zeyus/dae59e6a4d709bdb8a041a45634450cc/raw/YTLongForm.user.js | |
| // @description A youtube helper script for people who don't like shorts and who like long-form content. It highlights videos over a specified length, and hides the shorts menu item and shorts from the feed. | |
| // ==/UserScript== | |
| // Configuration for video length, and other options | |
| let userConfig = undefined; | |
| async function getConfig() { | |
| if (userConfig) return userConfig; | |
| userConfig = await GM.getValues({ | |
| longVideoLengthM: 55, | |
| disableShorts: true, | |
| settingsInitialized: false, | |
| }); | |
| console.log(userConfig); | |
| if (!userConfig.settingsInitialized) { | |
| GM.setValues({ | |
| longVideoLengthM: 55, | |
| disableShorts: true, | |
| settingsInitialized: true, | |
| }); | |
| userConfig.settingsInitialized = true; | |
| } | |
| return userConfig; | |
| } | |
| (async () => { | |
| const c = await getConfig(); | |
| const longVideoOnClick = function() { | |
| let longVideoLengthM = prompt("Enter the number of minutes for a video to be considered long", userConfig.longVideoLengthM); | |
| if (longVideoLengthM === null) return; | |
| // try making sure it's a number | |
| if (isNaN(longVideoLengthM)) { | |
| alert("Please enter a valid number"); | |
| return; | |
| } | |
| longVideoLengthM = parseFloat(longVideoLengthM); | |
| userConfig.longVideoLengthM = longVideoLengthM; | |
| GM.setValues({ | |
| longVideoLengthM: longVideoLengthM, | |
| }); | |
| GM.registerMenuCommand("Long video (mins): " + longVideoLengthM, longVideoOnClick, {id: "len"}); | |
| } | |
| GM.registerMenuCommand("Long video (mins): " + c.longVideoLengthM, longVideoOnClick, {id: "len"}); | |
| let shorts_status = c.disableShorts ? "(✅)" : "(❌)"; | |
| const shortsOnClick = async function() { | |
| let disableShorts = confirm("Do you want to disable shorts?"); | |
| userConfig.disableShorts = disableShorts; | |
| GM.setValues({ | |
| disableShorts: disableShorts, | |
| }); | |
| shorts_status = disableShorts ? "(✅)" : "(❌)"; | |
| GM.registerMenuCommand("Disable Shorts " + shorts_status, shortsOnClick, {id: "shorts"}); | |
| }; | |
| GM.registerMenuCommand("Disable Shorts " + shorts_status, shortsOnClick, {id: "shorts"}); | |
| })(); | |
| const markLongVideo = function(lengthEl) { | |
| const parentAel = lengthEl.closest("a"); | |
| lengthEl.classList.add("long-video"); | |
| if (parentAel) { | |
| parentAel.style.border = "2px solid red"; | |
| } | |
| }; | |
| const markShortVideo = function(lengthEl) { | |
| lengthEl.classList.add("short-video"); | |
| }; | |
| const parseLength = function(lengthStr) { | |
| const timeParts = lengthStr.trim().split(':'); | |
| const parsedLength = { | |
| h: 0, | |
| m: 0, | |
| s: 0, | |
| }; | |
| if (timeParts.constructor !== Array || timeParts.length > 3 || timeParts.length < 1) { | |
| return parsedLength; | |
| } | |
| let s = parseInt(timeParts.pop()); | |
| if (isNaN(s)) return parsedLength; | |
| parsedLength.s = s; | |
| if (timeParts.length === 0) return parsedLength; | |
| let m = parseInt(timeParts.pop()); | |
| if (isNaN(m)) return parsedLength; | |
| parsedLength.m = m; | |
| if (timeParts.length === 0) return parsedLength; | |
| let h = parseInt(timeParts.pop()); | |
| if (isNaN(h)) return parsedLength; | |
| parsedLength.h = h; | |
| return parsedLength; | |
| }; | |
| const updateLongVideos = async function() { | |
| // This selector avoids re-checking already parsed videos | |
| const c = await getConfig(); | |
| const videoLengths = document.querySelectorAll( | |
| ".yt-thumbnail-overlay-badge-view-model .yt-badge-shape__text:not(.long-video):not(.short-video)" | |
| ); | |
| console.debug("Found " + videoLengths.length + " unchecked videos."); | |
| // loop through unchecked videos | |
| for (var i = 0; i < videoLengths.length; i++) { | |
| const videoLength = videoLengths[i].innerText; | |
| const parsedLength = parseLength(videoLength); | |
| const parsedLengthM = parsedLength.m + parsedLength.h * 60; | |
| // check the length and mark it as long or short | |
| if (parsedLengthM >= c.longVideoLengthM) { | |
| markLongVideo(videoLengths[i]); | |
| } else { | |
| markShortVideo(videoLengths[i]); | |
| } | |
| } | |
| }; | |
| const hideShorts = async function() { | |
| const c = await getConfig(); | |
| if (!c.disableShorts) return; | |
| // fist hide shorts menu item | |
| const sectionMenuEl = document.querySelector("#sections > ytd-guide-section-renderer"); | |
| // the one that contains an a with the title "Shorts" is the one to remove | |
| if (sectionMenuEl) { | |
| const shortsEl = sectionMenuEl.querySelector("a[title='Shorts']"); | |
| if (shortsEl) { | |
| try { | |
| shortsEl.closest("ytd-guide-entry-renderer").remove(); | |
| } catch (e) { | |
| console.error("Error removing shorts menu item", e); | |
| } | |
| } | |
| } | |
| // now hide shorts videos in the feed | |
| function getShorts() { | |
| return document.querySelectorAll("ytd-rich-section-renderer [is-shorts]") || []; | |
| } | |
| while (getShorts().length > 0) { | |
| getShorts().forEach((shortEl) => { | |
| // remove parent ytd-rich-section-renderer | |
| try { | |
| shortEl.closest("ytd-rich-section-renderer").remove(); | |
| } catch (e) { | |
| console.error("Error removing shorts video", e); | |
| } | |
| }); | |
| } | |
| } | |
| window.addEventListener("load", async function() { | |
| console.debug("setting updateLongVideos to run every 5 seconds."); | |
| // just run it every 5 seconds | |
| try { | |
| await hideShorts(); | |
| } | |
| catch (e) { | |
| console.error("Error hiding shorts", e); | |
| } | |
| try { | |
| await updateLongVideos(); | |
| } catch (e) { | |
| console.error("Error updating long videos", e); | |
| } | |
| setInterval(hideShorts, 5000); | |
| setInterval(updateLongVideos, 5000); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment