YouTube - Toggle videos buttons

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.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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/jesuscc1993

// @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|results|playlist))/;

  const urlWithTypesPattern =
    /youtube.com(\/?$|\/((channel\/|c\/|@)(\w*)(\/(featured)|\/?$)|feed\/subscriptions|results|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();
})();