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.

  1. // ==UserScript==
  2. // @name YouTube - Toggle videos buttons
  3. // @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.
  4. // @version 2023.06.13.17.21
  5. // @author MetalTxus
  6. // @namespace https://github.com/jesuscc1993
  7.  
  8. // @icon https://www.youtube.com/favicon.ico
  9. // @match *://*.youtube.com/*
  10. // @require https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js
  11.  
  12. // @grant GM.getValue
  13. // @grant GM.setValue
  14. // ==/UserScript==
  15.  
  16. /* globals jQuery */
  17.  
  18. (async () => {
  19. 'use strict';
  20.  
  21. const enableDebug = false;
  22.  
  23. let currentUrl;
  24. let videosTotal;
  25.  
  26. let buttonsContainer;
  27. let toggleButtonsButton;
  28. let toggleLiveButton;
  29. let toggleShortsButton;
  30. let toggleUpcomingButton;
  31. let toggleUploadsButton;
  32. let toggleWatchedButton;
  33.  
  34. let buttonsHidden = await GM.getValue('buttonsHidden', false);
  35. let liveHidden = await GM.getValue('liveHidden', false);
  36. let shortsHidden = await GM.getValue('shortsHidden', false);
  37. let upcomingHidden = await GM.getValue('upcomingHidden', false);
  38. let uploadsHidden = await GM.getValue('uploadsHidden', false);
  39. let watchedHidden = await GM.getValue('watchedHidden', false);
  40.  
  41. const shouldRenderButton = () => {
  42. return location.href.match(urlPattern) !== null;
  43. };
  44.  
  45. const shouldFilterByTypeButton = () => {
  46. return location.href.match(urlWithTypesPattern) !== null;
  47. };
  48.  
  49. const shouldFilterByStatus = () => {
  50. return true;
  51. };
  52.  
  53. const shouldRunScript = () => {
  54. const oldUrl = currentUrl;
  55. currentUrl = location.href.split('?')[0];
  56.  
  57. const oldVideosTotal = videosTotal;
  58. videosTotal = jQuery(videosSelector).length;
  59.  
  60. const locationChanged = !!oldUrl && oldUrl !== currentUrl;
  61. const videosCountChanged = oldVideosTotal !== videosTotal;
  62.  
  63. const videosShouldBeHidden =
  64. (liveHidden ||
  65. shortsHidden ||
  66. upcomingHidden ||
  67. uploadsHidden ||
  68. watchedHidden) &&
  69. !!document.querySelectorAll(unprocessedVideosSelectors).length;
  70.  
  71. const videosShouldBeShown =
  72. !(
  73. liveHidden &&
  74. shortsHidden &&
  75. upcomingHidden &&
  76. uploadsHidden &&
  77. watchedHidden
  78. ) && !!document.querySelectorAll(processedVideosSelectors).length;
  79.  
  80. const shouldIt =
  81. shouldRenderButton() &&
  82. (locationChanged ||
  83. videosCountChanged ||
  84. videosShouldBeHidden ||
  85. videosShouldBeShown);
  86.  
  87. if (shouldIt) {
  88. debug(`Videos should be processed
  89. locationChanged: ${locationChanged}
  90. videosCountChanged: ${videosCountChanged}
  91. videosShouldBeHidden: ${videosShouldBeHidden}
  92. videosShouldBeShown: ${videosShouldBeShown}`);
  93. }
  94.  
  95. return shouldIt;
  96. };
  97.  
  98. const runButtonTask = () => {
  99. if (shouldRenderButton()) {
  100. const buttonsDestinationContainer = jQuery(
  101. buttonDestinationContainerSelector
  102. );
  103.  
  104. if (
  105. buttonsDestinationContainer.length &&
  106. !buttonsDestinationContainer.find(buttonsContainer).length
  107. ) {
  108. insertButtons(buttonsDestinationContainer);
  109. }
  110. } else {
  111. buttonsContainer.remove();
  112. toggleButtonsButton.remove();
  113. }
  114. };
  115.  
  116. const runVideosTask = () => {
  117. if (shouldRunScript()) {
  118. setTimeout(processAllVideos, 150);
  119. }
  120. };
  121.  
  122. const insertButtons = (buttonDestinationContainer) => {
  123. toggleLiveButton.off('click').on('click', toggleLiveVideos);
  124. toggleShortsButton.off('click').on('click', toggleShortsVideos);
  125. toggleUpcomingButton.off('click').on('click', toggleUpcomingVideos);
  126. toggleUploadsButton.off('click').on('click', toggleUploadsVideos);
  127. toggleWatchedButton.off('click').on('click', toggleWatchedVideos);
  128.  
  129. setButtonState(toggleLiveButton, liveHidden);
  130. setButtonState(toggleShortsButton, shortsHidden);
  131. setButtonState(toggleUpcomingButton, upcomingHidden);
  132. setButtonState(toggleUploadsButton, uploadsHidden);
  133. setButtonState(toggleWatchedButton, watchedHidden);
  134.  
  135. buttonDestinationContainer.prepend(buttonsContainer);
  136.  
  137. toggleButtonsButton.off('click').on('click', toggleButtons);
  138. jQuery(buttonsToggleDestinationSelector).prepend(toggleButtonsButton);
  139. };
  140.  
  141. const processAllVideos = () => {
  142. debug(`Processing videos...`);
  143. if (liveHidden) processLiveVideos();
  144. if (shortsHidden) processShortsVideos();
  145. if (upcomingHidden) processUpcomingVideos();
  146. if (uploadsHidden) processUploadsVideos();
  147. if (watchedHidden) processWatchedVideos();
  148. debug(`All videos processed`);
  149. };
  150.  
  151. const toggleLiveVideos = () => {
  152. liveHidden = !liveHidden;
  153. GM.setValue('liveHidden', liveHidden);
  154. processLiveVideos();
  155. };
  156.  
  157. const toggleShortsVideos = () => {
  158. shortsHidden = !shortsHidden;
  159. GM.setValue('shortsHidden', shortsHidden);
  160. processShortsVideos();
  161. };
  162.  
  163. const toggleUpcomingVideos = () => {
  164. upcomingHidden = !upcomingHidden;
  165. GM.setValue('upcomingHidden', upcomingHidden);
  166. processUpcomingVideos();
  167. };
  168.  
  169. const toggleUploadsVideos = () => {
  170. uploadsHidden = !uploadsHidden;
  171. GM.setValue('uploadsHidden', uploadsHidden);
  172. processUploadsVideos();
  173. };
  174.  
  175. const toggleWatchedVideos = () => {
  176. watchedHidden = !watchedHidden;
  177. GM.setValue('watchedHidden', watchedHidden);
  178. processWatchedVideos();
  179. };
  180.  
  181. const toggleButtons = (newValue) => {
  182. buttonsHidden = typeof newValue == 'boolean' ? newValue : !buttonsHidden;
  183. GM.setValue('buttonsHidden', buttonsHidden);
  184. buttonsHidden
  185. ? buttonsContainer.addClass('hide-buttons')
  186. : buttonsContainer.removeClass('hide-buttons');
  187. };
  188.  
  189. const processLiveVideos = () => {
  190. if (shouldFilterByTypeButton()) {
  191. processVideos(toggleLiveButton, liveHidden, liveVideosSelector);
  192. }
  193. };
  194.  
  195. const processShortsVideos = () => {
  196. if (shouldFilterByTypeButton()) {
  197. processVideos(toggleShortsButton, shortsHidden, shortsVideosSelector);
  198. }
  199. };
  200.  
  201. const processUpcomingVideos = () => {
  202. if (shouldFilterByStatus()) {
  203. processVideos(
  204. toggleUpcomingButton,
  205. upcomingHidden,
  206. upcomingVideosSelector
  207. );
  208. }
  209. };
  210.  
  211. const processUploadsVideos = () => {
  212. if (shouldFilterByTypeButton()) {
  213. processVideos(toggleUploadsButton, uploadsHidden, uploadsVideosSelector);
  214. }
  215. };
  216.  
  217. const processWatchedVideos = () => {
  218. if (shouldFilterByStatus()) {
  219. processVideos(toggleWatchedButton, watchedHidden, watchedVideosSelector);
  220. }
  221. };
  222.  
  223. const processVideos = (button, hidden, matchingSelector) => {
  224. const matchingVideos = jQuery(matchingSelector).parents(videosSelector);
  225. matchingVideos.toggleClass('mt-hidden', hidden);
  226.  
  227. setButtonState(button, hidden);
  228. };
  229.  
  230. const setButtonState = (button, hidden) => {
  231. button.toggleClass('on', !hidden);
  232. };
  233.  
  234. const debug = enableDebug
  235. ? (message) => console.debug(`${scriptPrefix} ${message}`)
  236. : () => {};
  237.  
  238. const initialize = () => {
  239. jQuery('head').append(baseStyle);
  240.  
  241. toggleLiveButton = jQuery(toggleVideosButtonTemplate)
  242. .addClass(`${i18n.live} type`)
  243. .text(i18n.live);
  244.  
  245. toggleShortsButton = jQuery(toggleVideosButtonTemplate)
  246. .addClass(`${i18n.shorts} type`)
  247. .text(i18n.shorts);
  248.  
  249. toggleUpcomingButton = jQuery(toggleVideosButtonTemplate)
  250. .addClass(`${i18n.upcoming} status`)
  251. .text(i18n.upcoming);
  252.  
  253. toggleUploadsButton = jQuery(toggleVideosButtonTemplate)
  254. .addClass(`${i18n.uploads} type`)
  255. .text(i18n.uploads);
  256.  
  257. toggleWatchedButton = jQuery(toggleVideosButtonTemplate)
  258. .addClass(`${i18n.watched} status`)
  259. .text(i18n.watched);
  260.  
  261. buttonsContainer = jQuery(buttonsContainerTemplate);
  262. buttonsContainer.append(toggleUpcomingButton);
  263. buttonsContainer.append(toggleLiveButton);
  264. buttonsContainer.append(toggleUploadsButton);
  265. buttonsContainer.append(toggleShortsButton);
  266. buttonsContainer.append(toggleWatchedButton);
  267.  
  268. toggleButtonsButton = jQuery(toggleButtonsButtonTemplate);
  269. toggleButtons(buttonsHidden);
  270.  
  271. setInterval(runButtonTask, 150);
  272. setInterval(runVideosTask, 1000);
  273.  
  274. console.info(`${scriptPrefix} Script initialized.`);
  275. };
  276.  
  277. const scriptPrefix = `[Toggle videos buttons]`;
  278.  
  279. const urlPattern =
  280. /youtube.com(\/?$|\/((channel\/|c\/|@)(\w*)(\/(featured|videos|shorts|streams)|\/?$)|feed\/subscriptions|results|playlist))/;
  281.  
  282. const urlWithTypesPattern =
  283. /youtube.com(\/?$|\/((channel\/|c\/|@)(\w*)(\/(featured)|\/?$)|feed\/subscriptions|results|playlist))/;
  284.  
  285. // texts
  286. const i18n = {
  287. toggleButtons: 'Toggle video filter buttons',
  288.  
  289. live: 'live',
  290. shorts: 'shorts',
  291. upcoming: 'upcoming',
  292. uploads: 'videos',
  293. watched: 'watched',
  294. };
  295.  
  296. // selectors
  297. const liveVideosSelector = `
  298. [role="main"] .badge-style-type-live-now-alternate
  299. `;
  300. const shortsVideosSelector = `
  301. [role="main"] ytd-thumbnail-overlay-time-status-renderer[overlay-style="SHORTS"],
  302. [role="main"] .ytd-thumbnail[href^="/shorts/"]
  303. `;
  304. const upcomingVideosSelector = `
  305. [role="main"] ytd-thumbnail-overlay-time-status-renderer[overlay-style="UPCOMING"]
  306. `;
  307. const uploadsVideosSelector = `
  308. [role="main"] ytd-thumbnail-overlay-time-status-renderer:not([overlay-style="SHORTS"])
  309. `;
  310. const watchedVideosSelector = `
  311. [role="main"] [id="progress"]
  312. `;
  313.  
  314. const buttonDestinationContainerSelector = `
  315. [page-subtype="channels"][role="main"] #primary > ytd-section-list-renderer,
  316. [page-subtype="channels"][role="main"] ytd-rich-grid-renderer,
  317. [page-subtype="home"][role="main"] #primary > ytd-rich-grid-renderer,
  318. [page-subtype="playlist"][role="main"] ytd-item-section-renderer,
  319. [page-subtype="subscriptions"][role="main"] ytd-shelf-renderer,
  320. ytd-search[role="main"] ytd-section-list-renderer
  321. `;
  322.  
  323. const buttonsToggleDestinationSelector = `#masthead #end`;
  324.  
  325. const videosSelector = `
  326. [role="main"] ytd-grid-video-renderer,
  327. [role="main"] ytd-playlist-video-renderer,
  328. [role="main"] ytd-rich-item-renderer,
  329. [role="main"] ytd-video-renderer,
  330.  
  331. [role="main"] .ytd-rich-section-renderer[is-shorts],
  332. [role="main"] ytd-reel-shelf-renderer,
  333. [role="main"] ytd-reel-item-renderer
  334. `;
  335.  
  336. const unprocessedVideosSelectors = videosSelector
  337. .replace(/\n\s*/g, '')
  338. .split(',')
  339. .map(
  340. (selector) =>
  341. `${selector}:not(.mt-hidden) ${watchedVideosSelector}, ${selector}:not(.mt-hidden) ${upcomingVideosSelector}`
  342. )
  343. .join(',');
  344.  
  345. const processedVideosSelectors = videosSelector
  346. .replace(/\n\s*/g, '')
  347. .split(',')
  348. .map(
  349. (selector) =>
  350. `${selector}.mt-hidden ${watchedVideosSelector}, ${selector}.mt-hidden ${upcomingVideosSelector}`
  351. )
  352. .join(',');
  353.  
  354. // templates
  355. const toggleVideosButtonTemplate = `
  356. <tp-yt-paper-button class="ytd-subscribe-button-renderer mt-button mt-toggle-videos-button" />
  357. `;
  358.  
  359. const toggleButtonsButtonTemplate = `
  360. <tp-yt-paper-button class="mt-button mt-toggle-buttons-button">
  361. <svg viewBox="0 0 24 24">
  362. <g>
  363. <path fill="#FFF" d="M20,7H4V6h16V7z M22,9v12H2V9H22z M15,15l-5-3v6L15,15z M17,3H7v1h10V3z"></path>
  364. </g>
  365. </svg>
  366. <tp-yt-paper-tooltip class="ytd-topbar-menu-button-renderer">
  367. ${i18n.toggleButtons}
  368. </tp-yt-paper-tooltip>
  369. </tp-yt-paper-button>
  370. `;
  371.  
  372. const buttonsContainerTemplate = `
  373. <div class="mt-toggle-videos-container"></div>
  374. `;
  375.  
  376. // style
  377. const baseStyle = `
  378. <style>
  379. .mt-toggle-videos-container {
  380. display: flex;
  381. justify-content: center;
  382. margin: 0 auto;
  383. }
  384.  
  385. .mt-toggle-videos-container.hide-buttons {
  386. display: none;
  387. }
  388.  
  389. .mt-button {
  390. border-radius: 20px !important;
  391. }
  392.  
  393. .mt-toggle-videos-button {
  394. border-radius: 0 !important;
  395. margin: 0 !important;
  396. text-align: center;
  397. min-width: 112px;
  398. background: var(--yt-spec-additive-background) !important;
  399. }
  400. .mt-toggle-videos-button.on {
  401. background: var(--yt-spec-10-percent-layer) !important;
  402. }
  403. .mt-toggle-videos-button:first-child {
  404. border-radius: 20px 0 0 20px !important;
  405. }
  406. .mt-toggle-videos-button:last-child {
  407. border-radius: 0 20px 20px 0 !important;
  408. }
  409.  
  410. .mt-toggle-buttons-button {
  411. background: transparent !important;
  412. height: 40px;
  413. margin: 0 8px 0 0;
  414. min-width: 40px;
  415. padding: 0 !important;
  416. }
  417. .mt-toggle-buttons-button:hover {
  418. background: var(--yt-spec-10-percent-layer) !important;
  419. }
  420. .mt-toggle-buttons-button svg {
  421. width: 24px;
  422. }
  423.  
  424. .mt-hidden {
  425. display: none !important;
  426. }
  427.  
  428. [page-subtype="channels"] .mt-toggle-videos-container {
  429. margin-top: 24px;
  430. }
  431. [page-subtype="channels"] ytd-rich-grid-renderer .mt-button.type,
  432. ytd-rich-grid-renderer[is-shorts-grid] .mt-button {
  433. background: transparent !important;
  434. opacity: .1;
  435. pointer-events: none;
  436. }
  437.  
  438. [page-subtype="playlist"] .mt-toggle-videos-container {
  439. box-sizing: border-box;
  440. padding: 0 24px;
  441. }
  442.  
  443. .ytd-search ytd-section-list-renderer .mt-toggle-videos-container {
  444. margin: 12px 0;
  445. }
  446. </style>
  447. `;
  448.  
  449. initialize();
  450. })();