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