Created
September 7, 2025 04:39
-
-
Save IceBotYT/6bbda6f52f5d18f11450ab6764326e92 to your computer and use it in GitHub Desktop.
Hides all auto-dubbed YouTube videos from recommended feeds
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name YouTube: Hide "Auto-dubbed" video cards | |
| // @namespace https://github.com/IceBotYT | |
| // @version 1.5 | |
| // @description Hides auto-dubbed YouTube videos | |
| // @author IceBotYT | |
| // @match https://www.youtube.com/* | |
| // @match https://youtube.com/* | |
| // @match https://youtu.be/* | |
| // @run-at document-end | |
| // @grant none | |
| // @license MIT | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| // --- DEBUG CONFIG --- | |
| const DEBUG = false; | |
| const PREFIX = '[AutoDub]'; | |
| function ts() { return (new Date()).toISOString(); } | |
| function info(...args) { if (DEBUG) console.info(PREFIX, ts(), ...args); } | |
| function debug(...args) { if (DEBUG) console.debug(PREFIX, ts(), ...args); } | |
| function warn(...args) { if (DEBUG) console.warn(PREFIX, ts(), ...args); } | |
| function error(...args) { if (DEBUG) console.error(PREFIX, ts(), ...args); } | |
| // Toggle behavior: | |
| // If MAKE_RED is true => mark matching cards with a red overlay (non-destructive) | |
| // If MAKE_RED is false => hide matching cards entirely (display: none) | |
| // Change this constant to switch modes. | |
| const MAKE_RED = false; | |
| // Expose basic stats for inspection in the console | |
| window.AutoDubDebug = window.AutoDubDebug || { | |
| scans: 0, | |
| badgesSeen: 0, | |
| badgesMatched: 0, | |
| cardsMarked: 0, | |
| cardsHidden: 0, | |
| cardsRed: 0, | |
| lastScanAt: null | |
| }; | |
| const BADGE_SELECTOR = '.yt-content-metadata-view-model__badge'; | |
| const BADGE_PROCESSED_ATTR = 'data-autodub-badge-checked'; | |
| const CARD_MARKED_ATTR = 'data-autodub-card'; | |
| const STYLE_ID = 'autodub-card-styles-v1'; | |
| const OVERLAY_CLASS = 'autodub-card-overlay-v1'; | |
| const REL_CLASS = 'autodub-card-relative-v1'; | |
| const HIDE_CLASS = 'autodub-card-hidden-v1'; | |
| const CARD_SELECTORS = [ | |
| '.ytd-item-section-renderer', | |
| 'ytd-video-renderer', | |
| 'ytd-rich-item-renderer', | |
| 'ytd-grid-video-renderer', | |
| 'ytd-compact-video-renderer', | |
| 'ytd-rich-grid-media', | |
| 'ytd-playlist-video-renderer', | |
| 'ytd-reel-video-renderer', | |
| 'ytd-compact-movie-renderer', | |
| 'ytd-channel-video-renderer', | |
| '[role="listitem"]' | |
| ]; | |
| function ensureStyles() { | |
| if (document.getElementById(STYLE_ID)) return; | |
| const css = ` | |
| .${REL_CLASS} { position: relative !important; } | |
| .${OVERLAY_CLASS} { | |
| position: absolute; | |
| inset: 0; | |
| background: rgba(255, 0, 0, 0.85); | |
| pointer-events: none; | |
| z-index: 11; | |
| border-radius: 6px; | |
| } | |
| .${HIDE_CLASS} { display: none !important; } | |
| `; | |
| const s = document.createElement('style'); | |
| s.id = STYLE_ID; | |
| s.textContent = css; | |
| document.head.appendChild(s); | |
| debug('Inserted stylesheet', STYLE_ID); | |
| } | |
| function findCardAncestor(el) { | |
| if (!el) return null; | |
| for (const sel of CARD_SELECTORS) { | |
| const anc = el.closest(sel); | |
| if (anc) { | |
| debug('findCardAncestor: matched selector', sel, '->', describeNode(anc)); | |
| return anc; | |
| } | |
| } | |
| const maybe = el.closest('a#thumbnail, a[href*="/watch"], ytd-thumbnail, ytd-rich-item-renderer, ytd-video-renderer'); | |
| if (maybe) debug('findCardAncestor: fallback matched', describeNode(maybe)); | |
| else debug('findCardAncestor: no ancestor found for', describeNode(el)); | |
| return maybe; | |
| } | |
| function describeNode(node) { | |
| if (!node) return 'null'; | |
| try { | |
| const tag = node.tagName || node.nodeName; | |
| const id = node.id ? `#${node.id}` : ''; | |
| const classes = node.classList && node.classList.length ? `.${Array.from(node.classList).slice(0,5).join('.')}` : ''; | |
| const short = `${tag}${id}${classes}`; | |
| return short; | |
| } catch (e) { | |
| return String(node); | |
| } | |
| } | |
| function markOrHideCard(card) { | |
| if (!card) { | |
| warn('markOrHideCard called with falsy card'); | |
| return false; | |
| } | |
| if (card.hasAttribute && card.hasAttribute(CARD_MARKED_ATTR)) { | |
| debug('markOrHideCard: card already processed, skipping', describeNode(card)); | |
| return false; | |
| } | |
| try { | |
| card.setAttribute(CARD_MARKED_ATTR, MAKE_RED ? 'red' : 'hidden'); | |
| ensureStyles(); | |
| if (MAKE_RED) { | |
| // RED MODE: add relative class and insert overlay (non-destructive) | |
| card.classList.add(REL_CLASS); | |
| // don't insert multiple overlays | |
| if (card.querySelector && card.querySelector(`.${OVERLAY_CLASS}`)) { | |
| debug('markOrHideCard: overlay already present on', describeNode(card)); | |
| window.AutoDubDebug.cardsMarked += 1; | |
| window.AutoDubDebug.cardsRed += 1; | |
| return true; | |
| } | |
| const overlay = document.createElement('div'); | |
| overlay.className = OVERLAY_CLASS; | |
| overlay.setAttribute('aria-hidden', 'true'); | |
| overlay.dataset.autodubOverlay = '1'; | |
| card.insertBefore(overlay, card.firstChild); | |
| // slight dim underlying images/videos to make overlay clear (non-destructive) | |
| try { | |
| const thumbs = card.querySelectorAll('img, video, yt-img-shadow'); | |
| for (const t of thumbs) { | |
| t.style.filter = (t.style.filter ? t.style.filter + ' ' : '') + 'brightness(0.65)'; | |
| } | |
| } catch (e) { | |
| // ignore | |
| } | |
| window.AutoDubDebug.cardsMarked += 1; | |
| window.AutoDubDebug.cardsRed += 1; | |
| info('Marked card red:', describeNode(card)); | |
| return true; | |
| } else { | |
| // HIDE MODE: add hide class to collapse the card from the layout | |
| card.classList.add(HIDE_CLASS); | |
| // Also set aria-hidden and inert-like safety | |
| try { | |
| card.setAttribute('aria-hidden', 'true'); | |
| // try to make non-focusable | |
| const anchors = card.querySelectorAll('a, button, input, [tabindex]'); | |
| for (const a of anchors) { | |
| try { | |
| if (a.hasAttribute('tabindex')) a.dataset._autodub_old_tabindex = a.getAttribute('tabindex'); | |
| a.setAttribute('tabindex', '-1'); | |
| } catch (e) { /* ignore */ } | |
| } | |
| } catch (e) { /* ignore */ } | |
| window.AutoDubDebug.cardsMarked += 1; | |
| window.AutoDubDebug.cardsHidden += 1; | |
| info('Hidden card:', describeNode(card)); | |
| return true; | |
| } | |
| } catch (err) { | |
| error('markOrHideCard error', err); | |
| return false; | |
| } | |
| } | |
| function processBadge(badgeEl, index) { | |
| if (!badgeEl) return; | |
| if (badgeEl.hasAttribute && badgeEl.hasAttribute(BADGE_PROCESSED_ATTR)) { | |
| debug(`processBadge[${index}]: already processed`, describeNode(badgeEl)); | |
| return; | |
| } | |
| badgeEl.setAttribute(BADGE_PROCESSED_ATTR, '1'); | |
| window.AutoDubDebug.badgesSeen += 1; | |
| const text = (badgeEl.innerText || badgeEl.textContent || '').trim(); | |
| debug(`processBadge[${index}]: text="${text}"`, describeNode(badgeEl)); | |
| if (!text) return; | |
| if (text.includes("Auto-dubbed")) { | |
| window.AutoDubDebug.badgesMatched += 1; | |
| info(`processBadge[${index}]: MATCH -> "${text}"`); | |
| const card = findCardAncestor(badgeEl); | |
| if (card) { | |
| const ok = markOrHideCard(card); | |
| if (!ok) warn('processBadge: failed to process card', describeNode(card)); | |
| } else { | |
| warn('processBadge: matched badge but no card ancestor found for', describeNode(badgeEl)); | |
| } | |
| } else { | |
| debug(`processBadge[${index}]: not an auto-dubbed badge`); | |
| } | |
| } | |
| function scanBadges() { | |
| try { | |
| window.AutoDubDebug.scans += 1; | |
| window.AutoDubDebug.lastScanAt = new Date().toISOString(); | |
| const badges = document.querySelectorAll(BADGE_SELECTOR); | |
| debug('scanBadges: found badge elements count=', badges.length); | |
| let i = 0; | |
| for (const b of badges) { | |
| processBadge(b, i++); | |
| } | |
| info(`scanBadges: badgesSeen=${window.AutoDubDebug.badgesSeen} badgesMatched=${window.AutoDubDebug.badgesMatched} cardsMarked=${window.AutoDubDebug.cardsMarked} cardsHidden=${window.AutoDubDebug.cardsHidden} cardsRed=${window.AutoDubDebug.cardsRed}`); | |
| } catch (err) { | |
| error('scanBadges error', err); | |
| } | |
| } | |
| let scanTimeout = null; | |
| function scheduleScan(delay = 150) { | |
| if (scanTimeout) return; | |
| debug('scheduleScan: scheduling in', delay, 'ms'); | |
| scanTimeout = setTimeout(() => { | |
| scanTimeout = null; | |
| scanBadges(); | |
| }, delay); | |
| } | |
| const observer = new MutationObserver((mutations) => { | |
| try { | |
| debug('observer: mutation batch', mutations.length); | |
| let added = false; | |
| for (const m of mutations) { | |
| if (m.addedNodes && m.addedNodes.length) { added = true; break; } | |
| if (m.type === 'characterData' || m.type === 'attributes') { added = true; break; } | |
| } | |
| if (added) scheduleScan(100); | |
| } catch (err) { | |
| error('observer callback error', err); | |
| } | |
| }); | |
| let pollHandle = null; | |
| function startPolling() { | |
| if (pollHandle) return; | |
| pollHandle = setInterval(() => { | |
| debug('polling: running scanBadges'); | |
| scanBadges(); | |
| }, 3000); | |
| debug('startPolling: started with interval 3000ms'); | |
| } | |
| function init() { | |
| info('Initializing AutoDub debug script (MAKE_RED=' + MAKE_RED + ')'); | |
| setTimeout(scanBadges, 300); | |
| try { | |
| observer.observe(document.body, { childList: true, subtree: true, characterData: true, attributes: true }); | |
| debug('observer attached to document.body'); | |
| } catch (e) { | |
| try { | |
| observer.observe(document.documentElement, { childList: true, subtree: true, characterData: true, attributes: true }); | |
| debug('observer attached to document.documentElement'); | |
| } catch (err) { | |
| error('Failed to attach observer', err); | |
| } | |
| } | |
| // detect SPA navigation | |
| const origPush = history.pushState; | |
| history.pushState = function () { | |
| const res = origPush.apply(this, arguments); | |
| debug('history.pushState called -> scheduling scan'); | |
| scheduleScan(300); | |
| return res; | |
| }; | |
| window.addEventListener('yt-navigate-finish', () => { | |
| debug('yt-navigate-finish event -> scheduling scan'); | |
| scheduleScan(200); | |
| }, { passive: true }); | |
| startPolling(); | |
| } | |
| // Start after short delay so initial DOM can settle | |
| setTimeout(init, 500); | |
| // Helpful note printed once | |
| info('AutoDub debug script loaded. To inspect runtime stats: window.AutoDubDebug. Filter console for', PREFIX); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment