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.12
  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. const REGEX_CHANNEL = /.*\/(user|channel|c)\/.+\/videos/u;
  26. const REGEX_USER = /.*\/@.*/u;
  27.  
  28. ((_undefined) => {
  29. // Enable for debugging
  30. const DEBUG = false;
  31.  
  32. // Needed to bypass YouTube's Trusted Types restrictions, ie.
  33. // Uncaught TypeError: Failed to set the 'innerHTML' property on 'Element': This document requires 'TrustedHTML' assignment.
  34. if (
  35. typeof trustedTypes !== 'undefined' &&
  36. trustedTypes.defaultPolicy === null
  37. ) {
  38. const s = (s) => s;
  39. trustedTypes.createPolicy('default', {
  40. createHTML: s,
  41. createScript: s,
  42. createScriptURL: s,
  43. });
  44. }
  45.  
  46. // GM_config setup
  47. const title = document.createElement('a');
  48. title.textContent = 'YouTube: Hide Watched Videos Settings';
  49. title.href = 'https://github.com/EvHaus/youtube-hide-watched';
  50. title.target = '_blank';
  51. const gmc = new GM_config({
  52. events: {
  53. save() {
  54. this.close();
  55. },
  56. },
  57. fields: {
  58. HIDDEN_THRESHOLD_PERCENT: {
  59. default: 10,
  60. label: 'Hide/Dim Videos Above Percent',
  61. max: 100,
  62. min: 0,
  63. type: 'int',
  64. },
  65. },
  66. id: 'YouTubeHideWatchedVideos',
  67. title,
  68. });
  69.  
  70. // Set defaults
  71. localStorage.YTHWV_WATCHED = localStorage.YTHWV_WATCHED || 'false';
  72.  
  73. const logDebug = (...msgs) => {
  74. if (DEBUG) console.debug('[YT-HWV]', msgs);
  75. };
  76.  
  77. // GreaseMonkey no longer supports GM_addStyle. So we have to define
  78. // our own polyfill here
  79. const addStyle = (aCss) => {
  80. const head = document.getElementsByTagName('head')[0];
  81. if (head) {
  82. const style = document.createElement('style');
  83. style.setAttribute('type', 'text/css');
  84. style.textContent = aCss;
  85. head.appendChild(style);
  86. return style;
  87. }
  88. return null;
  89. };
  90.  
  91. addStyle(`
  92. .YT-HWV-WATCHED-HIDDEN { display: none !important }
  93.  
  94. .YT-HWV-WATCHED-DIMMED { opacity: 0.3 }
  95.  
  96. .YT-HWV-SHORTS-HIDDEN { display: none !important }
  97.  
  98. .YT-HWV-SHORTS-DIMMED { opacity: 0.3 }
  99.  
  100. .YT-HWV-HIDDEN-ROW-PARENT { padding-bottom: 10px }
  101.  
  102. .YT-HWV-BUTTONS {
  103. background: transparent;
  104. border: 1px solid var(--ytd-searchbox-legacy-border-color);
  105. border-radius: 40px;
  106. display: flex;
  107. gap: 5px;
  108. margin: 0 20px;
  109. }
  110.  
  111. .YT-HWV-BUTTON {
  112. align-items: center;
  113. background: transparent;
  114. border: 0;
  115. border-radius: 40px;
  116. color: var(--yt-spec-icon-inactive);
  117. cursor: pointer;
  118. display: flex;
  119. height: 40px;
  120. justify-content: center;
  121. outline: 0;
  122. width: 40px;
  123. }
  124.  
  125. .YT-HWV-BUTTON:focus,
  126. .YT-HWV-BUTTON:hover {
  127. background: var(--yt-spec-badge-chip-background);
  128. }
  129.  
  130. .YT-HWV-BUTTON-DISABLED { color: var(--yt-spec-icon-disabled) }
  131.  
  132. .YT-HWV-MENU {
  133. background: #F8F8F8;
  134. border: 1px solid #D3D3D3;
  135. box-shadow: 0 1px 0 rgba(0, 0, 0, 0.05);
  136. display: none;
  137. font-size: 12px;
  138. margin-top: -1px;
  139. padding: 10px;
  140. position: absolute;
  141. right: 0;
  142. text-align: center;
  143. top: 100%;
  144. white-space: normal;
  145. z-index: 9999;
  146. }
  147.  
  148. .YT-HWV-MENU-ON { display: block; }
  149. .YT-HWV-MENUBUTTON-ON span { transform: rotate(180deg) }
  150. `);
  151.  
  152. const BUTTONS = [
  153. {
  154. 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>',
  155. iconHidden:
  156. '<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>',
  157. name: 'Toggle Watched Videos',
  158. stateKey: 'YTHWV_STATE',
  159. type: 'toggle',
  160. },
  161. {
  162. 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>',
  163. iconHidden:
  164. '<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>',
  165. name: 'Toggle Shorts',
  166. stateKey: 'YTHWV_STATE_SHORTS',
  167. type: 'toggle',
  168. },
  169. {
  170. 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>',
  171. name: 'Settings',
  172. type: 'settings',
  173. },
  174. ];
  175.  
  176. // ===========================================================
  177.  
  178. const debounce = function (func, wait, immediate) {
  179. let timeout;
  180. return (...args) => {
  181. const later = () => {
  182. timeout = null;
  183. if (!immediate) func.apply(this, args);
  184. };
  185. const callNow = immediate && !timeout;
  186. clearTimeout(timeout);
  187. timeout = setTimeout(later, wait);
  188. if (callNow) func.apply(this, args);
  189. };
  190. };
  191.  
  192. // ===========================================================
  193.  
  194. const findWatchedElements = () => {
  195. const watched = document.querySelectorAll(
  196. [
  197. '.ytd-thumbnail-overlay-resume-playback-renderer',
  198. // 2025-02-01 Update
  199. '.ytThumbnailOverlayProgressBarHostWatchedProgressBarSegmentModern',
  200. ].join(','),
  201. );
  202.  
  203. const withThreshold = Array.from(watched).filter((bar) => {
  204. return (
  205. bar.style.width &&
  206. Number.parseInt(bar.style.width, 10) >=
  207. gmc.get('HIDDEN_THRESHOLD_PERCENT')
  208. );
  209. });
  210.  
  211. logDebug(
  212. `Found ${watched.length} watched elements ` +
  213. `(${withThreshold.length} within threshold)`,
  214. );
  215.  
  216. return withThreshold;
  217. };
  218.  
  219. // ===========================================================
  220.  
  221. const findShortsContainers = () => {
  222. const shortsContainers = [
  223. // All pages (2024-09 update)
  224. document.querySelectorAll('[is-shorts]'),
  225. // Subscriptions Page (List View)
  226. document.querySelectorAll(
  227. 'ytd-reel-shelf-renderer ytd-reel-item-renderer',
  228. ),
  229. document.querySelectorAll(
  230. 'ytd-rich-shelf-renderer ytd-rich-grid-slim-media',
  231. ),
  232. // Home Page & Subscriptions Page (Grid View)
  233. document.querySelectorAll('ytd-reel-shelf-renderer ytd-thumbnail'),
  234. // Search results page
  235. document.querySelectorAll(
  236. 'ytd-reel-shelf-renderer .ytd-reel-shelf-renderer',
  237. ),
  238. // Search results apge (2025-06 update)
  239. document.querySelectorAll('ytm-shorts-lockup-view-model-v2'),
  240. ].reduce((acc, matches) => {
  241. matches?.forEach((child) => {
  242. const container =
  243. child.closest('ytd-reel-shelf-renderer') ||
  244. child.closest('ytd-rich-shelf-renderer') ||
  245. child.closest('grid-shelf-view-model');
  246. if (container && !acc.includes(container)) acc.push(container);
  247. });
  248. return acc;
  249. }, []);
  250.  
  251. // Search results sometimes also show Shorts as if they're regular videos with a little "Shorts" badge
  252. document
  253. .querySelectorAll(
  254. '.ytd-thumbnail-overlay-time-status-renderer[aria-label="Shorts"]',
  255. )
  256. .forEach((child) => {
  257. const container = child.closest('ytd-video-renderer');
  258. shortsContainers.push(container);
  259. });
  260.  
  261. logDebug(`Found ${shortsContainers.length} shorts container elements`);
  262.  
  263. return shortsContainers;
  264. };
  265.  
  266. // ===========================================================
  267.  
  268. const findButtonAreaTarget = () => {
  269. // Button will be injected into the main header menu
  270. return document.querySelector('#container #end #buttons');
  271. };
  272.  
  273. // ===========================================================
  274.  
  275. const determineYoutubeSection = () => {
  276. const { href } = window.location;
  277.  
  278. let youtubeSection = 'misc';
  279. if (href.includes('/watch?')) {
  280. youtubeSection = 'watch';
  281. } else if (href.match(REGEX_CHANNEL) || href.match(REGEX_USER)) {
  282. youtubeSection = 'channel';
  283. } else if (href.includes('/feed/subscriptions')) {
  284. youtubeSection = 'subscriptions';
  285. } else if (href.includes('/feed/trending')) {
  286. youtubeSection = 'trending';
  287. } else if (href.includes('/playlist?')) {
  288. youtubeSection = 'playlist';
  289. } else if (href.includes('/results?')) {
  290. youtubeSection = 'search';
  291. }
  292.  
  293. return youtubeSection;
  294. };
  295.  
  296. // ===========================================================
  297.  
  298. const updateClassOnWatchedItems = () => {
  299. // Remove existing classes
  300. document
  301. .querySelectorAll('.YT-HWV-WATCHED-DIMMED')
  302. .forEach((el) => el.classList.remove('YT-HWV-WATCHED-DIMMED'));
  303. document
  304. .querySelectorAll('.YT-HWV-WATCHED-HIDDEN')
  305. .forEach((el) => el.classList.remove('YT-HWV-WATCHED-HIDDEN'));
  306.  
  307. // If we're on the History page -- do nothing. We don't want to hide
  308. // watched videos here.
  309. if (window.location.href.indexOf('/feed/history') >= 0) return;
  310.  
  311. const section = determineYoutubeSection();
  312. const state = localStorage[`YTHWV_STATE_${section}`];
  313.  
  314. findWatchedElements().forEach((item, _i) => {
  315. let watchedItem;
  316. let dimmedItem;
  317.  
  318. // "Subscription" section needs us to hide the "#contents",
  319. // but in the "Trending" section, that class will hide everything.
  320. // So there, we need to hide the "ytd-video-renderer"
  321. if (section === 'subscriptions') {
  322. // For rows, hide the row and the header too. We can't hide
  323. // their entire parent because then we'll get the infinite
  324. // page loader to load forever.
  325. watchedItem =
  326. // Grid item
  327. item.closest('.ytd-grid-renderer') ||
  328. item.closest('.ytd-item-section-renderer') ||
  329. item.closest('.ytd-rich-grid-row') ||
  330. item.closest('.ytd-rich-grid-renderer') ||
  331. // List item
  332. item.closest('#grid-container');
  333.  
  334. // If we're hiding the .ytd-item-section-renderer element, we need to give it
  335. // some extra spacing otherwise we'll get stuck in infinite page loading
  336. if (watchedItem?.classList.contains('ytd-item-section-renderer')) {
  337. watchedItem
  338. .closest('ytd-item-section-renderer')
  339. .classList.add('YT-HWV-HIDDEN-ROW-PARENT');
  340. }
  341. } else if (section === 'playlist') {
  342. watchedItem = item.closest('ytd-playlist-video-renderer');
  343. } else if (section === 'watch') {
  344. watchedItem = item.closest('ytd-compact-video-renderer');
  345.  
  346. // Don't hide video if it's going to play next.
  347. //
  348. // If there is no watchedItem - we probably got
  349. // `ytd-playlist-panel-video-renderer`:
  350. // let's also ignore it as in case of shuffle enabled
  351. // we could accidentially hide the item which gonna play next.
  352. if (watchedItem?.closest('ytd-compact-autoplay-renderer')) {
  353. watchedItem = null;
  354. }
  355.  
  356. // For playlist items, we never hide them, but we will dim
  357. // them even if current mode is to hide rather than dim.
  358. const watchedItemInPlaylist = item.closest(
  359. 'ytd-playlist-panel-video-renderer',
  360. );
  361. if (!watchedItem && watchedItemInPlaylist) {
  362. dimmedItem = watchedItemInPlaylist;
  363. }
  364. } else {
  365. // For home page and other areas
  366. watchedItem =
  367. item.closest('ytd-rich-item-renderer') ||
  368. item.closest('ytd-video-renderer') ||
  369. item.closest('ytd-grid-video-renderer');
  370. }
  371.  
  372. if (watchedItem) {
  373. // Add current class
  374. if (state === 'dimmed') {
  375. watchedItem.classList.add('YT-HWV-WATCHED-DIMMED');
  376. } else if (state === 'hidden') {
  377. watchedItem.classList.add('YT-HWV-WATCHED-HIDDEN');
  378. }
  379. }
  380.  
  381. if (dimmedItem && (state === 'dimmed' || state === 'hidden')) {
  382. dimmedItem.classList.add('YT-HWV-WATCHED-DIMMED');
  383. }
  384. });
  385. };
  386.  
  387. // ===========================================================
  388.  
  389. const updateClassOnShortsItems = () => {
  390. const section = determineYoutubeSection();
  391.  
  392. document
  393. .querySelectorAll('.YT-HWV-SHORTS-DIMMED')
  394. .forEach((el) => el.classList.remove('YT-HWV-SHORTS-DIMMED'));
  395. document
  396. .querySelectorAll('.YT-HWV-SHORTS-HIDDEN')
  397. .forEach((el) => el.classList.remove('YT-HWV-SHORTS-HIDDEN'));
  398.  
  399. const state = localStorage[`YTHWV_STATE_SHORTS_${section}`];
  400.  
  401. const shortsContainers = findShortsContainers();
  402.  
  403. shortsContainers.forEach((item) => {
  404. // Add current class
  405. if (state === 'dimmed') {
  406. item.classList.add('YT-HWV-SHORTS-DIMMED');
  407. } else if (state === 'hidden') {
  408. item.classList.add('YT-HWV-SHORTS-HIDDEN');
  409. }
  410. });
  411. };
  412.  
  413. // ===========================================================
  414.  
  415. const renderButtons = () => {
  416. // Find button area target
  417. const target = findButtonAreaTarget();
  418. if (!target) return;
  419.  
  420. // Did we already render the buttons?
  421. const existingButtons = document.querySelector('.YT-HWV-BUTTONS');
  422.  
  423. // Generate buttons area DOM
  424. const buttonArea = document.createElement('div');
  425. buttonArea.classList.add('YT-HWV-BUTTONS');
  426.  
  427. // Render buttons
  428. BUTTONS.forEach(({ icon, iconHidden, name, stateKey, type }) => {
  429. // For toggle buttons, determine where in localStorage they track state
  430. const section = determineYoutubeSection();
  431. const storageKey = [stateKey, section].join('_');
  432. const toggleButtonState = localStorage.getItem(storageKey) || 'normal';
  433.  
  434. // Generate button DOM
  435. const button = document.createElement('button');
  436. button.title =
  437. type === 'toggle'
  438. ? `${name} : currently "${toggleButtonState}" for section "${section}"`
  439. : `${name}`;
  440. button.classList.add('YT-HWV-BUTTON');
  441. if (toggleButtonState !== 'normal')
  442. button.classList.add('YT-HWV-BUTTON-DISABLED');
  443. button.innerHTML = toggleButtonState === 'hidden' ? iconHidden : icon;
  444. buttonArea.appendChild(button);
  445.  
  446. // Attach events for toggle buttons
  447. switch (type) {
  448. case 'toggle':
  449. button.addEventListener('click', () => {
  450. logDebug(`Button ${name} clicked. State: ${toggleButtonState}`);
  451.  
  452. let newState = 'dimmed';
  453. if (toggleButtonState === 'dimmed') {
  454. newState = 'hidden';
  455. } else if (toggleButtonState === 'hidden') {
  456. newState = 'normal';
  457. }
  458.  
  459. localStorage.setItem(storageKey, newState);
  460.  
  461. updateClassOnWatchedItems();
  462. updateClassOnShortsItems();
  463. renderButtons();
  464. });
  465. break;
  466. case 'settings':
  467. button.addEventListener('click', () => {
  468. gmc.open();
  469. renderButtons();
  470. });
  471. break;
  472. }
  473. });
  474.  
  475. // Insert buttons into DOM
  476. if (existingButtons) {
  477. target.parentNode.replaceChild(buttonArea, existingButtons);
  478. logDebug('Re-rendered menu buttons');
  479. } else {
  480. target.parentNode.insertBefore(buttonArea, target);
  481. logDebug('Rendered menu buttons');
  482. }
  483. };
  484.  
  485. const run = debounce((mutations) => {
  486. // Don't react if only our own buttons changed state
  487. // to avoid running an endless loop
  488. if (
  489. mutations &&
  490. mutations.length === 1 &&
  491. (mutations[0].target.classList.contains('YT-HWV-BUTTON') ||
  492. mutations[0].target.classList.contains('YT-HWV-BUTTON-SHORTS'))
  493. ) {
  494. return;
  495. }
  496.  
  497. logDebug('Running check for watched videos, and shorts');
  498. updateClassOnWatchedItems();
  499. updateClassOnShortsItems();
  500. renderButtons();
  501. }, 250);
  502.  
  503. // ===========================================================
  504.  
  505. // Hijack all XHR calls
  506. const send = XMLHttpRequest.prototype.send;
  507. XMLHttpRequest.prototype.send = function (data) {
  508. this.addEventListener(
  509. 'readystatechange',
  510. function () {
  511. if (
  512. // Anytime more videos are fetched -- re-run script
  513. this.responseURL.indexOf('browse_ajax?action_continuation') > 0
  514. ) {
  515. setTimeout(() => {
  516. run();
  517. }, 0);
  518. }
  519. },
  520. false,
  521. );
  522. send.call(this, data);
  523. };
  524.  
  525. // ===========================================================
  526.  
  527. const observeDOM = (() => {
  528. const MutationObserver =
  529. window.MutationObserver || window.WebKitMutationObserver;
  530. const eventListenerSupported = window.addEventListener;
  531.  
  532. return (obj, callback) => {
  533. logDebug('Attaching DOM listener');
  534.  
  535. // Invalid `obj` given
  536. if (!obj) return;
  537.  
  538. if (MutationObserver) {
  539. const obs = new MutationObserver((mutations, _observer) => {
  540. // If the mutation is the script's own buttons being injected, ignore the event
  541. if (
  542. mutations.length === 1 &&
  543. mutations[0].addedNodes?.length === 1 &&
  544. mutations[0].addedNodes[0].classList.contains('YT-HWV-BUTTONS')
  545. ) {
  546. return;
  547. }
  548.  
  549. if (
  550. mutations[0].addedNodes.length ||
  551. mutations[0].removedNodes.length
  552. ) {
  553. callback(mutations);
  554. }
  555. });
  556.  
  557. obs.observe(obj, { childList: true, subtree: true });
  558. } else if (eventListenerSupported) {
  559. obj.addEventListener('DOMNodeInserted', callback, false);
  560. obj.addEventListener('DOMNodeRemoved', callback, false);
  561. }
  562. };
  563. })();
  564.  
  565. // ===========================================================
  566.  
  567. logDebug('Starting Script');
  568.  
  569. // YouTube does navigation via history and also does a bunch
  570. // of AJAX video loading. In order to ensure we're always up
  571. // to date, we have to listen for ANY DOM change event, and
  572. // re-run our script.
  573. observeDOM(document.body, run);
  574.  
  575. run();
  576. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址