YouTube: Hide Watched Videos

Hides watched videos (and shorts) from your YouTube subscriptions page.

  1. // ==UserScript==
  2. // @name YouTube: Hide Watched Videos
  3. // @namespace https://www.haus.gg/
  4. // @version 6.11
  5. // @license MIT
  6. // @description Hides watched videos (and shorts) from your YouTube subscriptions page.
  7. // @author Ev Haus
  8. // @author netjeff
  9. // @author actionless
  10. // @match http://*.youtube.com/*
  11. // @match http://youtube.com/*
  12. // @match https://*.youtube.com/*
  13. // @match https://youtube.com/*
  14. // @noframes
  15. // @require https://openuserjs.org/src/libs/sizzle/GM_config.js
  16. // @grant GM_getValue
  17. // @grant GM_setValue
  18. // ==/UserScript==
  19.  
  20. // To submit bugs or submit revisions please see visit the repository at:
  21. // https://github.com/EvHaus/youtube-hide-watched
  22. // You can open new issues at:
  23. // https://github.com/EvHaus/youtube-hide-watched/issues
  24.  
  25. ((_undefined) => {
  26. // Enable for debugging
  27. const DEBUG = false;
  28.  
  29. // Needed to bypass YouTube's Trusted Types restrictions, ie.
  30. // Uncaught TypeError: Failed to set the 'innerHTML' property on 'Element': This document requires 'TrustedHTML' assignment.
  31. if (
  32. typeof trustedTypes !== 'undefined' &&
  33. trustedTypes.defaultPolicy === null
  34. ) {
  35. const s = (s) => s;
  36. trustedTypes.createPolicy('default', {
  37. createHTML: s,
  38. createScriptURL: s,
  39. createScript: s,
  40. });
  41. }
  42.  
  43. // GM_config setup
  44. const title = document.createElement('a');
  45. title.textContent = 'YouTube: Hide Watched Videos Settings';
  46. title.href = 'https://github.com/EvHaus/youtube-hide-watched';
  47. title.target = '_blank';
  48. const gmc = new GM_config({
  49. events: {
  50. save() {
  51. this.close();
  52. },
  53. },
  54. fields: {
  55. HIDDEN_THRESHOLD_PERCENT: {
  56. default: 10,
  57. label: 'Hide/Dim Videos Above Percent',
  58. max: 100,
  59. min: 0,
  60. type: 'int',
  61. },
  62. },
  63. id: 'YouTubeHideWatchedVideos',
  64. title,
  65. });
  66.  
  67. // Set defaults
  68. localStorage.YTHWV_WATCHED = localStorage.YTHWV_WATCHED || 'false';
  69.  
  70. const logDebug = (...msgs) => {
  71. if (DEBUG) console.debug('[YT-HWV]', msgs);
  72. };
  73.  
  74. // GreaseMonkey no longer supports GM_addStyle. So we have to define
  75. // our own polyfill here
  76. const addStyle = (aCss) => {
  77. const head = document.getElementsByTagName('head')[0];
  78. if (head) {
  79. const style = document.createElement('style');
  80. style.setAttribute('type', 'text/css');
  81. style.textContent = aCss;
  82. head.appendChild(style);
  83. return style;
  84. }
  85. return null;
  86. };
  87.  
  88. addStyle(`
  89. .YT-HWV-WATCHED-HIDDEN { display: none !important }
  90.  
  91. .YT-HWV-WATCHED-DIMMED { opacity: 0.3 }
  92.  
  93. .YT-HWV-SHORTS-HIDDEN { display: none !important }
  94.  
  95. .YT-HWV-SHORTS-DIMMED { opacity: 0.3 }
  96.  
  97. .YT-HWV-HIDDEN-ROW-PARENT { padding-bottom: 10px }
  98.  
  99. .YT-HWV-BUTTONS {
  100. background: transparent;
  101. border: 1px solid var(--ytd-searchbox-legacy-border-color);
  102. border-radius: 40px;
  103. display: flex;
  104. gap: 5px;
  105. margin: 0 20px;
  106. }
  107.  
  108. .YT-HWV-BUTTON {
  109. align-items: center;
  110. background: transparent;
  111. border: 0;
  112. border-radius: 40px;
  113. color: var(--yt-spec-icon-inactive);
  114. cursor: pointer;
  115. display: flex;
  116. height: 40px;
  117. justify-content: center;
  118. outline: 0;
  119. width: 40px;
  120. }
  121.  
  122. .YT-HWV-BUTTON:focus,
  123. .YT-HWV-BUTTON:hover {
  124. background: var(--yt-spec-badge-chip-background);
  125. }
  126.  
  127. .YT-HWV-BUTTON-DISABLED { color: var(--yt-spec-icon-disabled) }
  128.  
  129. .YT-HWV-MENU {
  130. background: #F8F8F8;
  131. border: 1px solid #D3D3D3;
  132. box-shadow: 0 1px 0 rgba(0, 0, 0, 0.05);
  133. display: none;
  134. font-size: 12px;
  135. margin-top: -1px;
  136. padding: 10px;
  137. position: absolute;
  138. right: 0;
  139. text-align: center;
  140. top: 100%;
  141. white-space: normal;
  142. z-index: 9999;
  143. }
  144.  
  145. .YT-HWV-MENU-ON { display: block; }
  146. .YT-HWV-MENUBUTTON-ON span { transform: rotate(180deg) }
  147. `);
  148.  
  149. const BUTTONS = [
  150. {
  151. icon: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 48 48"><path fill="currentColor" d="M24 9C14 9 5.46 15.22 2 24c3.46 8.78 12 15 22 15 10.01 0 18.54-6.22 22-15-3.46-8.78-11.99-15-22-15zm0 25c-5.52 0-10-4.48-10-10s4.48-10 10-10 10 4.48 10 10-4.48 10-10 10zm0-16c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6z"/></svg>',
  152. iconHidden:
  153. '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 48 48"><path fill="currentColor" d="M24 14c5.52 0 10 4.48 10 10 0 1.29-.26 2.52-.71 3.65l5.85 5.85c3.02-2.52 5.4-5.78 6.87-9.5-3.47-8.78-12-15-22.01-15-2.8 0-5.48.5-7.97 1.4l4.32 4.31c1.13-.44 2.36-.71 3.65-.71zM4 8.55l4.56 4.56.91.91C6.17 16.6 3.56 20.03 2 24c3.46 8.78 12 15 22 15 3.1 0 6.06-.6 8.77-1.69l.85.85L39.45 44 42 41.46 6.55 6 4 8.55zM15.06 19.6l3.09 3.09c-.09.43-.15.86-.15 1.31 0 3.31 2.69 6 6 6 .45 0 .88-.06 1.3-.15l3.09 3.09C27.06 33.6 25.58 34 24 34c-5.52 0-10-4.48-10-10 0-1.58.4-3.06 1.06-4.4zm8.61-1.57 6.3 6.3L30 24c0-3.31-2.69-6-6-6l-.33.03z"/></svg>',
  154. name: 'Toggle Watched Videos',
  155. stateKey: 'YTHWV_STATE',
  156. type: 'toggle',
  157. },
  158. {
  159. icon: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 48 48"><path fill="currentColor" d="M31.95 3c-1.11 0-2.25.3-3.27.93l-15.93 9.45C10.32 14.79 8.88 17.67 9 20.7c.15 3 1.74 5.61 4.17 6.84.06.03 2.25 1.05 2.25 1.05l-2.7 1.59c-3.42 2.04-4.74 6.81-2.94 10.65C11.07 43.47 13.5 45 16.05 45c1.11 0 2.22-.3 3.27-.93l15.93-9.45c2.4-1.44 3.87-4.29 3.72-7.35-.12-2.97-1.74-5.61-4.17-6.81-.06-.03-2.25-1.05-2.25-1.05l2.7-1.59c3.42-2.04 4.74-6.81 2.91-10.65C36.93 4.53 34.47 3 31.95 3z"/></svg>',
  160. iconHidden:
  161. '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 48 48"><g fill="currentColor"><g clip-path="url(#slashGap)"><path d="M31.97 3c-1.11 0-2.25.3-3.27.93l-15.93 9.45c-2.43 1.41-3.87 4.29-3.75 7.32.15 3 1.74 5.61 4.17 6.84.06.03 2.25 1.05 2.25 1.05l-2.7 1.59C9.32 32.22 8 36.99 9.8 40.83c1.29 2.64 3.72 4.17 6.27 4.17 1.11 0 2.22-.3 3.27-.93l15.93-9.45c2.4-1.44 3.87-4.29 3.72-7.35-.12-2.97-1.74-5.61-4.17-6.81-.06-.03-2.25-1.05-2.25-1.05l2.7-1.59c3.42-2.04 4.74-6.81 2.91-10.65C36.95 4.53 34.49 3 31.97 3z"/></g><path d="m7.501 5.55 4.066-2.42 24.26 40.78-4.065 2.418z"/></g></svg>',
  162. name: 'Toggle Shorts',
  163. stateKey: 'YTHWV_STATE_SHORTS',
  164. type: 'toggle',
  165. },
  166. {
  167. icon: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path fill="currentColor" d="M12 9.5a2.5 2.5 0 0 1 0 5 2.5 2.5 0 0 1 0-5m0-1c-1.93 0-3.5 1.57-3.5 3.5s1.57 3.5 3.5 3.5 3.5-1.57 3.5-3.5-1.57-3.5-3.5-3.5zM13.22 3l.55 2.2.13.51.5.18c.61.23 1.19.56 1.72.98l.4.32.5-.14 2.17-.62 1.22 2.11-1.63 1.59-.37.36.08.51c.05.32.08.64.08.98s-.03.66-.08.98l-.08.51.37.36 1.63 1.59-1.22 2.11-2.17-.62-.5-.14-.4.32c-.53.43-1.11.76-1.72.98l-.5.18-.13.51-.55 2.24h-2.44l-.55-2.2-.13-.51-.5-.18c-.6-.23-1.18-.56-1.72-.99l-.4-.32-.5.14-2.17.62-1.21-2.12 1.63-1.59.37-.36-.08-.51c-.05-.32-.08-.65-.08-.98s.03-.66.08-.98l.08-.51-.37-.36L3.6 8.56l1.22-2.11 2.17.62.5.14.4-.32c.53-.44 1.11-.77 1.72-.99l.5-.18.13-.51.54-2.21h2.44M14 2h-4l-.74 2.96c-.73.27-1.4.66-2 1.14l-2.92-.83-2 3.46 2.19 2.13c-.06.37-.09.75-.09 1.14s.03.77.09 1.14l-2.19 2.13 2 3.46 2.92-.83c.6.48 1.27.87 2 1.14L10 22h4l.74-2.96c.73-.27 1.4-.66 2-1.14l2.92.83 2-3.46-2.19-2.13c.06-.37.09-.75.09-1.14s-.03-.77-.09-1.14l2.19-2.13-2-3.46-2.92.83c-.6-.48-1.27-.87-2-1.14L14 2z"/></svg>',
  168. name: 'Settings',
  169. type: 'settings',
  170. },
  171. ];
  172.  
  173. // ===========================================================
  174.  
  175. const debounce = function (func, wait, immediate) {
  176. let timeout;
  177. return (...args) => {
  178. const later = () => {
  179. timeout = null;
  180. if (!immediate) func.apply(this, args);
  181. };
  182. const callNow = immediate && !timeout;
  183. clearTimeout(timeout);
  184. timeout = setTimeout(later, wait);
  185. if (callNow) func.apply(this, args);
  186. };
  187. };
  188.  
  189. // ===========================================================
  190.  
  191. const findWatchedElements = () => {
  192. const watched = document.querySelectorAll(
  193. [
  194. '.ytd-thumbnail-overlay-resume-playback-renderer',
  195. // 2025-02-01 Update
  196. '.ytThumbnailOverlayProgressBarHostWatchedProgressBarSegmentModern',
  197. ].join(','),
  198. );
  199.  
  200. const withThreshold = Array.from(watched).filter((bar) => {
  201. return (
  202. bar.style.width &&
  203. Number.parseInt(bar.style.width, 10) >=
  204. gmc.get('HIDDEN_THRESHOLD_PERCENT')
  205. );
  206. });
  207.  
  208. logDebug(
  209. `Found ${watched.length} watched elements ` +
  210. `(${withThreshold.length} within threshold)`,
  211. );
  212.  
  213. return withThreshold;
  214. };
  215.  
  216. // ===========================================================
  217.  
  218. const findShortsContainers = () => {
  219. const shortsContainers = [
  220. // All pages (2024-09 update)
  221. document.querySelectorAll('[is-shorts]'),
  222. // Subscriptions Page (List View)
  223. document.querySelectorAll(
  224. 'ytd-reel-shelf-renderer ytd-reel-item-renderer',
  225. ),
  226. document.querySelectorAll(
  227. 'ytd-rich-shelf-renderer ytd-rich-grid-slim-media',
  228. ),
  229. // Home Page & Subscriptions Page (Grid View)
  230. document.querySelectorAll('ytd-reel-shelf-renderer ytd-thumbnail'),
  231. // Search results page
  232. document.querySelectorAll(
  233. 'ytd-reel-shelf-renderer .ytd-reel-shelf-renderer',
  234. ),
  235. ].reduce((acc, matches) => {
  236. matches?.forEach((child) => {
  237. const container =
  238. child.closest('ytd-reel-shelf-renderer') ||
  239. child.closest('ytd-rich-shelf-renderer');
  240. if (container && !acc.includes(container)) acc.push(container);
  241. });
  242. return acc;
  243. }, []);
  244.  
  245. // Search results sometimes also show Shorts as if they're regular videos with a little "Shorts" badge
  246. document
  247. .querySelectorAll(
  248. '.ytd-thumbnail-overlay-time-status-renderer[aria-label="Shorts"]',
  249. )
  250. .forEach((child) => {
  251. const container = child.closest('ytd-video-renderer');
  252. shortsContainers.push(container);
  253. });
  254.  
  255. logDebug(`Found ${shortsContainers.length} shorts container elements`);
  256.  
  257. return shortsContainers;
  258. };
  259.  
  260. // ===========================================================
  261.  
  262. const findButtonAreaTarget = () => {
  263. // Button will be injected into the main header menu
  264. return document.querySelector('#container #end #buttons');
  265. };
  266.  
  267. // ===========================================================
  268.  
  269. const determineYoutubeSection = () => {
  270. const { href } = window.location;
  271.  
  272. let youtubeSection = 'misc';
  273. if (href.includes('/watch?')) {
  274. youtubeSection = 'watch';
  275. } else if (
  276. href.match(/.*\/(user|channel|c)\/.+\/videos/u) ||
  277. href.match(/.*\/@.*/u)
  278. ) {
  279. youtubeSection = 'channel';
  280. } else if (href.includes('/feed/subscriptions')) {
  281. youtubeSection = 'subscriptions';
  282. } else if (href.includes('/feed/trending')) {
  283. youtubeSection = 'trending';
  284. } else if (href.includes('/playlist?')) {
  285. youtubeSection = 'playlist';
  286. } else if (href.includes('/results?')) {
  287. youtubeSection = 'search';
  288. }
  289.  
  290. return youtubeSection;
  291. };
  292.  
  293. // ===========================================================
  294.  
  295. const updateClassOnWatchedItems = () => {
  296. // Remove existing classes
  297. document
  298. .querySelectorAll('.YT-HWV-WATCHED-DIMMED')
  299. .forEach((el) => el.classList.remove('YT-HWV-WATCHED-DIMMED'));
  300. document
  301. .querySelectorAll('.YT-HWV-WATCHED-HIDDEN')
  302. .forEach((el) => el.classList.remove('YT-HWV-WATCHED-HIDDEN'));
  303.  
  304. // If we're on the History page -- do nothing. We don't want to hide
  305. // watched videos here.
  306. if (window.location.href.indexOf('/feed/history') >= 0) return;
  307.  
  308. const section = determineYoutubeSection();
  309. const state = localStorage[`YTHWV_STATE_${section}`];
  310.  
  311. findWatchedElements().forEach((item, _i) => {
  312. let watchedItem;
  313. let dimmedItem;
  314.  
  315. // "Subscription" section needs us to hide the "#contents",
  316. // but in the "Trending" section, that class will hide everything.
  317. // So there, we need to hide the "ytd-video-renderer"
  318. if (section === 'subscriptions') {
  319. // For rows, hide the row and the header too. We can't hide
  320. // their entire parent because then we'll get the infinite
  321. // page loader to load forever.
  322. watchedItem =
  323. // Grid item
  324. item.closest('.ytd-grid-renderer') ||
  325. item.closest('.ytd-item-section-renderer') ||
  326. item.closest('.ytd-rich-grid-row') ||
  327. item.closest('.ytd-rich-grid-renderer') ||
  328. // List item
  329. item.closest('#grid-container');
  330.  
  331. // If we're hiding the .ytd-item-section-renderer element, we need to give it
  332. // some extra spacing otherwise we'll get stuck in infinite page loading
  333. if (watchedItem?.classList.contains('ytd-item-section-renderer')) {
  334. watchedItem
  335. .closest('ytd-item-section-renderer')
  336. .classList.add('YT-HWV-HIDDEN-ROW-PARENT');
  337. }
  338. } else if (section === 'playlist') {
  339. watchedItem = item.closest('ytd-playlist-video-renderer');
  340. } else if (section === 'watch') {
  341. watchedItem = item.closest('ytd-compact-video-renderer');
  342.  
  343. // Don't hide video if it's going to play next.
  344. //
  345. // If there is no watchedItem - we probably got
  346. // `ytd-playlist-panel-video-renderer`:
  347. // let's also ignore it as in case of shuffle enabled
  348. // we could accidentially hide the item which gonna play next.
  349. if (watchedItem?.closest('ytd-compact-autoplay-renderer')) {
  350. watchedItem = null;
  351. }
  352.  
  353. // For playlist items, we never hide them, but we will dim
  354. // them even if current mode is to hide rather than dim.
  355. const watchedItemInPlaylist = item.closest(
  356. 'ytd-playlist-panel-video-renderer',
  357. );
  358. if (!watchedItem && watchedItemInPlaylist) {
  359. dimmedItem = watchedItemInPlaylist;
  360. }
  361. } else {
  362. // For home page and other areas
  363. watchedItem =
  364. item.closest('ytd-rich-item-renderer') ||
  365. item.closest('ytd-video-renderer') ||
  366. item.closest('ytd-grid-video-renderer');
  367. }
  368.  
  369. if (watchedItem) {
  370. // Add current class
  371. if (state === 'dimmed') {
  372. watchedItem.classList.add('YT-HWV-WATCHED-DIMMED');
  373. } else if (state === 'hidden') {
  374. watchedItem.classList.add('YT-HWV-WATCHED-HIDDEN');
  375. }
  376. }
  377.  
  378. if (dimmedItem && (state === 'dimmed' || state === 'hidden')) {
  379. dimmedItem.classList.add('YT-HWV-WATCHED-DIMMED');
  380. }
  381. });
  382. };
  383.  
  384. // ===========================================================
  385.  
  386. const updateClassOnShortsItems = () => {
  387. const section = determineYoutubeSection();
  388.  
  389. document
  390. .querySelectorAll('.YT-HWV-SHORTS-DIMMED')
  391. .forEach((el) => el.classList.remove('YT-HWV-SHORTS-DIMMED'));
  392. document
  393. .querySelectorAll('.YT-HWV-SHORTS-HIDDEN')
  394. .forEach((el) => el.classList.remove('YT-HWV-SHORTS-HIDDEN'));
  395.  
  396. const state = localStorage[`YTHWV_STATE_SHORTS_${section}`];
  397.  
  398. const shortsContainers = findShortsContainers();
  399.  
  400. shortsContainers.forEach((item) => {
  401. // Add current class
  402. if (state === 'dimmed') {
  403. item.classList.add('YT-HWV-SHORTS-DIMMED');
  404. } else if (state === 'hidden') {
  405. item.classList.add('YT-HWV-SHORTS-HIDDEN');
  406. }
  407. });
  408. };
  409.  
  410. // ===========================================================
  411.  
  412. const renderButtons = () => {
  413. // Find button area target
  414. const target = findButtonAreaTarget();
  415. if (!target) return;
  416.  
  417. // Did we already render the buttons?
  418. const existingButtons = document.querySelector('.YT-HWV-BUTTONS');
  419.  
  420. // Generate buttons area DOM
  421. const buttonArea = document.createElement('div');
  422. buttonArea.classList.add('YT-HWV-BUTTONS');
  423.  
  424. // Render buttons
  425. BUTTONS.forEach(({ icon, iconHidden, name, stateKey, type }) => {
  426. // For toggle buttons, determine where in localStorage they track state
  427. const section = determineYoutubeSection();
  428. const storageKey = [stateKey, section].join('_');
  429. const toggleButtonState = localStorage.getItem(storageKey) || 'normal';
  430.  
  431. // Generate button DOM
  432. const button = document.createElement('button');
  433. button.title =
  434. type === 'toggle'
  435. ? `${name} : currently "${toggleButtonState}" for section "${section}"`
  436. : `${name}`;
  437. button.classList.add('YT-HWV-BUTTON');
  438. if (toggleButtonState !== 'normal')
  439. button.classList.add('YT-HWV-BUTTON-DISABLED');
  440. button.innerHTML = toggleButtonState === 'hidden' ? iconHidden : icon;
  441. buttonArea.appendChild(button);
  442.  
  443. // Attach events for toggle buttons
  444. switch (type) {
  445. case 'toggle':
  446. button.addEventListener('click', () => {
  447. logDebug(`Button ${name} clicked. State: ${toggleButtonState}`);
  448.  
  449. let newState = 'dimmed';
  450. if (toggleButtonState === 'dimmed') {
  451. newState = 'hidden';
  452. } else if (toggleButtonState === 'hidden') {
  453. newState = 'normal';
  454. }
  455.  
  456. localStorage.setItem(storageKey, newState);
  457.  
  458. updateClassOnWatchedItems();
  459. updateClassOnShortsItems();
  460. renderButtons();
  461. });
  462. break;
  463. case 'settings':
  464. button.addEventListener('click', () => {
  465. gmc.open();
  466. renderButtons();
  467. });
  468. break;
  469. }
  470. });
  471.  
  472. // Insert buttons into DOM
  473. if (existingButtons) {
  474. target.parentNode.replaceChild(buttonArea, existingButtons);
  475. logDebug('Re-rendered menu buttons');
  476. } else {
  477. target.parentNode.insertBefore(buttonArea, target);
  478. logDebug('Rendered menu buttons');
  479. }
  480. };
  481.  
  482. const run = debounce((mutations) => {
  483. // Don't react if only our own buttons changed state
  484. // to avoid running an endless loop
  485. if (
  486. mutations &&
  487. mutations.length === 1 &&
  488. (mutations[0].target.classList.contains('YT-HWV-BUTTON') ||
  489. mutations[0].target.classList.contains('YT-HWV-BUTTON-SHORTS'))
  490. ) {
  491. return;
  492. }
  493.  
  494. logDebug('Running check for watched videos, and shorts');
  495. updateClassOnWatchedItems();
  496. updateClassOnShortsItems();
  497. renderButtons();
  498. }, 250);
  499.  
  500. // ===========================================================
  501.  
  502. // Hijack all XHR calls
  503. const send = XMLHttpRequest.prototype.send;
  504. XMLHttpRequest.prototype.send = function (data) {
  505. this.addEventListener(
  506. 'readystatechange',
  507. function () {
  508. if (
  509. // Anytime more videos are fetched -- re-run script
  510. this.responseURL.indexOf('browse_ajax?action_continuation') > 0
  511. ) {
  512. setTimeout(() => {
  513. run();
  514. }, 0);
  515. }
  516. },
  517. false,
  518. );
  519. send.call(this, data);
  520. };
  521.  
  522. // ===========================================================
  523.  
  524. const observeDOM = (() => {
  525. const MutationObserver =
  526. window.MutationObserver || window.WebKitMutationObserver;
  527. const eventListenerSupported = window.addEventListener;
  528.  
  529. return (obj, callback) => {
  530. logDebug('Attaching DOM listener');
  531.  
  532. // Invalid `obj` given
  533. if (!obj) return;
  534.  
  535. if (MutationObserver) {
  536. const obs = new MutationObserver((mutations, _observer) => {
  537. // If the mutation is the script's own buttons being injected, ignore the event
  538. if (
  539. mutations.length === 1 &&
  540. mutations[0].addedNodes?.length === 1 &&
  541. mutations[0].addedNodes[0].classList.contains('YT-HWV-BUTTONS')
  542. ) {
  543. return;
  544. }
  545.  
  546. if (
  547. mutations[0].addedNodes.length ||
  548. mutations[0].removedNodes.length
  549. ) {
  550. callback(mutations);
  551. }
  552. });
  553.  
  554. obs.observe(obj, { childList: true, subtree: true });
  555. } else if (eventListenerSupported) {
  556. obj.addEventListener('DOMNodeInserted', callback, false);
  557. obj.addEventListener('DOMNodeRemoved', callback, false);
  558. }
  559. };
  560. })();
  561.  
  562. // ===========================================================
  563.  
  564. logDebug('Starting Script');
  565.  
  566. // YouTube does navigation via history and also does a bunch
  567. // of AJAX video loading. In order to ensure we're always up
  568. // to date, we have to listen for ANY DOM change event, and
  569. // re-run our script.
  570. observeDOM(document.body, run);
  571.  
  572. run();
  573. })();