Adds buttons to filter out videos by type and/or status. The toggles can be hidden/shown at any time by pressing the button added to the header.
// ==UserScript== // @name YouTube - Toggle videos buttons // @description Adds buttons to filter out videos by type and/or status. The toggles can be hidden/shown at any time by pressing the button added to the header. // @version 2023.06.13.17.21 // @author MetalTxus // @namespace https://github.com/j###scc1993 // @icon https://www.youtube.com/favicon.ico // @match *://*.youtube.com/* // @require https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js // @grant GM.getValue // @grant GM.setValue // ==/UserScript== /* globals jQuery */ (async () => { 'use strict'; const enableDebug = false; let currentUrl; let videosTotal; let buttonsContainer; let toggleButtonsButton; let toggleLiveButton; let toggleShortsButton; let toggleUpcomingButton; let toggleUploadsButton; let toggleWatchedButton; let buttonsHidden = await GM.getValue('buttonsHidden', false); let liveHidden = await GM.getValue('liveHidden', false); let shortsHidden = await GM.getValue('shortsHidden', false); let upcomingHidden = await GM.getValue('upcomingHidden', false); let uploadsHidden = await GM.getValue('uploadsHidden', false); let watchedHidden = await GM.getValue('watchedHidden', false); const shouldRenderButton = () => { return location.href.match(urlPattern) !== null; }; const shouldFilterByTypeButton = () => { return location.href.match(urlWithTypesPattern) !== null; }; const shouldFilterByStatus = () => { return true; }; const shouldRunScript = () => { const oldUrl = currentUrl; currentUrl = location.href.split('?')[0]; const oldVideosTotal = videosTotal; videosTotal = jQuery(videosSelector).length; const locationChanged = !!oldUrl && oldUrl !== currentUrl; const videosCountChanged = oldVideosTotal !== videosTotal; const videosShouldBeHidden = (liveHidden || shortsHidden || upcomingHidden || uploadsHidden || watchedHidden) && !!document.querySelectorAll(unprocessedVideosSelectors).length; const videosShouldBeShown = !( liveHidden && shortsHidden && upcomingHidden && uploadsHidden && watchedHidden ) && !!document.querySelectorAll(processedVideosSelectors).length; const shouldIt = shouldRenderButton() && (locationChanged || videosCountChanged || videosShouldBeHidden || videosShouldBeShown); if (shouldIt) { debug(`Videos should be processed locationChanged: ${locationChanged} videosCountChanged: ${videosCountChanged} videosShouldBeHidden: ${videosShouldBeHidden} videosShouldBeShown: ${videosShouldBeShown}`); } return shouldIt; }; const runButtonTask = () => { if (shouldRenderButton()) { const buttonsDestinationContainer = jQuery( buttonDestinationContainerSelector ); if ( buttonsDestinationContainer.length && !buttonsDestinationContainer.find(buttonsContainer).length ) { insertButtons(buttonsDestinationContainer); } } else { buttonsContainer.remove(); toggleButtonsButton.remove(); } }; const runVideosTask = () => { if (shouldRunScript()) { setTimeout(processAllVideos, 150); } }; const insertButtons = (buttonDestinationContainer) => { toggleLiveButton.off('click').on('click', toggleLiveVideos); toggleShortsButton.off('click').on('click', toggleShortsVideos); toggleUpcomingButton.off('click').on('click', toggleUpcomingVideos); toggleUploadsButton.off('click').on('click', toggleUploadsVideos); toggleWatchedButton.off('click').on('click', toggleWatchedVideos); setButtonState(toggleLiveButton, liveHidden); setButtonState(toggleShortsButton, shortsHidden); setButtonState(toggleUpcomingButton, upcomingHidden); setButtonState(toggleUploadsButton, uploadsHidden); setButtonState(toggleWatchedButton, watchedHidden); buttonDestinationContainer.prepend(buttonsContainer); toggleButtonsButton.off('click').on('click', toggleButtons); jQuery(buttonsToggleDestinationSelector).prepend(toggleButtonsButton); }; const processAllVideos = () => { debug(`Processing videos...`); if (liveHidden) processLiveVideos(); if (shortsHidden) processShortsVideos(); if (upcomingHidden) processUpcomingVideos(); if (uploadsHidden) processUploadsVideos(); if (watchedHidden) processWatchedVideos(); debug(`All videos processed`); }; const toggleLiveVideos = () => { liveHidden = !liveHidden; GM.setValue('liveHidden', liveHidden); processLiveVideos(); }; const toggleShortsVideos = () => { shortsHidden = !shortsHidden; GM.setValue('shortsHidden', shortsHidden); processShortsVideos(); }; const toggleUpcomingVideos = () => { upcomingHidden = !upcomingHidden; GM.setValue('upcomingHidden', upcomingHidden); processUpcomingVideos(); }; const toggleUploadsVideos = () => { uploadsHidden = !uploadsHidden; GM.setValue('uploadsHidden', uploadsHidden); processUploadsVideos(); }; const toggleWatchedVideos = () => { watchedHidden = !watchedHidden; GM.setValue('watchedHidden', watchedHidden); processWatchedVideos(); }; const toggleButtons = (newValue) => { buttonsHidden = typeof newValue == 'boolean' ? newValue : !buttonsHidden; GM.setValue('buttonsHidden', buttonsHidden); buttonsHidden ? buttonsContainer.addClass('hide-buttons') : buttonsContainer.removeClass('hide-buttons'); }; const processLiveVideos = () => { if (shouldFilterByTypeButton()) { processVideos(toggleLiveButton, liveHidden, liveVideosSelector); } }; const processShortsVideos = () => { if (shouldFilterByTypeButton()) { processVideos(toggleShortsButton, shortsHidden, shortsVideosSelector); } }; const processUpcomingVideos = () => { if (shouldFilterByStatus()) { processVideos( toggleUpcomingButton, upcomingHidden, upcomingVideosSelector ); } }; const processUploadsVideos = () => { if (shouldFilterByTypeButton()) { processVideos(toggleUploadsButton, uploadsHidden, uploadsVideosSelector); } }; const processWatchedVideos = () => { if (shouldFilterByStatus()) { processVideos(toggleWatchedButton, watchedHidden, watchedVideosSelector); } }; const processVideos = (button, hidden, matchingSelector) => { const matchingVideos = jQuery(matchingSelector).parents(videosSelector); matchingVideos.toggleClass('mt-hidden', hidden); setButtonState(button, hidden); }; const setButtonState = (button, hidden) => { button.toggleClass('on', !hidden); }; const debug = enableDebug ? (message) => console.debug(`${scriptPrefix} ${message}`) : () => {}; const initialize = () => { jQuery('head').append(baseStyle); toggleLiveButton = jQuery(toggleVideosButtonTemplate) .addClass(`${i18n.live} type`) .text(i18n.live); toggleShortsButton = jQuery(toggleVideosButtonTemplate) .addClass(`${i18n.shorts} type`) .text(i18n.shorts); toggleUpcomingButton = jQuery(toggleVideosButtonTemplate) .addClass(`${i18n.upcoming} status`) .text(i18n.upcoming); toggleUploadsButton = jQuery(toggleVideosButtonTemplate) .addClass(`${i18n.uploads} type`) .text(i18n.uploads); toggleWatchedButton = jQuery(toggleVideosButtonTemplate) .addClass(`${i18n.watched} status`) .text(i18n.watched); buttonsContainer = jQuery(buttonsContainerTemplate); buttonsContainer.append(toggleUpcomingButton); buttonsContainer.append(toggleLiveButton); buttonsContainer.append(toggleUploadsButton); buttonsContainer.append(toggleShortsButton); buttonsContainer.append(toggleWatchedButton); toggleButtonsButton = jQuery(toggleButtonsButtonTemplate); toggleButtons(buttonsHidden); setInterval(runButtonTask, 150); setInterval(runVideosTask, 1000); console.info(`${scriptPrefix} Script initialized.`); }; const scriptPrefix = `[Toggle videos buttons]`; const urlPattern = /youtube.com(\/?$|\/((channel\/|c\/|@)(\w*)(\/(featured|videos|shorts|streams)|\/?$)|feed\/subscriptions|r###lts|playlist))/; const urlWithTypesPattern = /youtube.com(\/?$|\/((channel\/|c\/|@)(\w*)(\/(featured)|\/?$)|feed\/subscriptions|r###lts|playlist))/; // texts const i18n = { toggleButtons: 'Toggle video filter buttons', live: 'live', shorts: 'shorts', upcoming: 'upcoming', uploads: 'videos', watched: 'watched', }; // selectors const liveVideosSelector = ` [role="main"] .badge-style-type-live-now-alternate `; const shortsVideosSelector = ` [role="main"] ytd-thumbnail-overlay-time-status-renderer[overlay-style="SHORTS"], [role="main"] .ytd-thumbnail[href^="/shorts/"] `; const upcomingVideosSelector = ` [role="main"] ytd-thumbnail-overlay-time-status-renderer[overlay-style="UPCOMING"] `; const uploadsVideosSelector = ` [role="main"] ytd-thumbnail-overlay-time-status-renderer:not([overlay-style="SHORTS"]) `; const watchedVideosSelector = ` [role="main"] [id="progress"] `; const buttonDestinationContainerSelector = ` [page-subtype="channels"][role="main"] #primary > ytd-section-list-renderer, [page-subtype="channels"][role="main"] ytd-rich-grid-renderer, [page-subtype="home"][role="main"] #primary > ytd-rich-grid-renderer, [page-subtype="playlist"][role="main"] ytd-item-section-renderer, [page-subtype="subscriptions"][role="main"] ytd-shelf-renderer, ytd-search[role="main"] ytd-section-list-renderer `; const buttonsToggleDestinationSelector = `#masthead #end`; const videosSelector = ` [role="main"] ytd-grid-video-renderer, [role="main"] ytd-playlist-video-renderer, [role="main"] ytd-rich-item-renderer, [role="main"] ytd-video-renderer, [role="main"] .ytd-rich-section-renderer[is-shorts], [role="main"] ytd-reel-shelf-renderer, [role="main"] ytd-reel-item-renderer `; const unprocessedVideosSelectors = videosSelector .replace(/\n\s*/g, '') .split(',') .map( (selector) => `${selector}:not(.mt-hidden) ${watchedVideosSelector}, ${selector}:not(.mt-hidden) ${upcomingVideosSelector}` ) .join(','); const processedVideosSelectors = videosSelector .replace(/\n\s*/g, '') .split(',') .map( (selector) => `${selector}.mt-hidden ${watchedVideosSelector}, ${selector}.mt-hidden ${upcomingVideosSelector}` ) .join(','); // templates const toggleVideosButtonTemplate = ` <tp-yt-paper-button class="ytd-subscribe-button-renderer mt-button mt-toggle-videos-button" /> `; const toggleButtonsButtonTemplate = ` <tp-yt-paper-button class="mt-button mt-toggle-buttons-button"> <svg viewBox="0 0 24 24"> <g> <path fill="#FFF" d="M20,7H4V6h16V7z M22,9v12H2V9H22z M15,15l-5-3v6L15,15z M17,3H7v1h10V3z"></path> </g> </svg> <tp-yt-paper-tooltip class="ytd-topbar-menu-button-renderer"> ${i18n.toggleButtons} </tp-yt-paper-tooltip> </tp-yt-paper-button> `; const buttonsContainerTemplate = ` <div class="mt-toggle-videos-container"></div> `; // style const baseStyle = ` <style> .mt-toggle-videos-container { display: flex; justify-content: center; margin: 0 auto; } .mt-toggle-videos-container.hide-buttons { display: none; } .mt-button { border-radius: 20px !important; } .mt-toggle-videos-button { border-radius: 0 !important; margin: 0 !important; text-align: center; min-width: 112px; background: var(--yt-spec-additive-background) !important; } .mt-toggle-videos-button.on { background: var(--yt-spec-10-percent-layer) !important; } .mt-toggle-videos-button:first-child { border-radius: 20px 0 0 20px !important; } .mt-toggle-videos-button:last-child { border-radius: 0 20px 20px 0 !important; } .mt-toggle-buttons-button { background: transparent !important; height: 40px; margin: 0 8px 0 0; min-width: 40px; padding: 0 !important; } .mt-toggle-buttons-button:hover { background: var(--yt-spec-10-percent-layer) !important; } .mt-toggle-buttons-button svg { width: 24px; } .mt-hidden { display: none !important; } [page-subtype="channels"] .mt-toggle-videos-container { margin-top: 24px; } [page-subtype="channels"] ytd-rich-grid-renderer .mt-button.type, ytd-rich-grid-renderer[is-shorts-grid] .mt-button { background: transparent !important; opacity: .1; pointer-events: none; } [page-subtype="playlist"] .mt-toggle-videos-container { box-sizing: border-box; padding: 0 24px; } .ytd-search ytd-section-list-renderer .mt-toggle-videos-container { margin: 12px 0; } </style> `; initialize(); })();