Focused YouTube

Remove ads, shorts, and algorithmic suggestions on YouTube (EN/NL/DE/FR)

// ==UserScript==
// @name         Focused YouTube
// @version      34
// @author       Richard B
// @namespace    https://www.365devnet.eu/focusedyoutube
// @description  Remove ads, shorts, and algorithmic suggestions on YouTube (EN/NL/DE/FR)
// @match        *://*.youtube.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @run-at       document-start
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==

/*
MIT License

Copyright (c) 2025 Richard B
Permission is hereby granted, free of charge, to any person obtaining a copy...
(license text unchanged)
*/

// -----------------------------
// Config
// -----------------------------
const DEFAULT_SETTINGS = {
  /// homepage redirect ///
  redirectHomepage: false, // Options: 'wl', 'subs', 'lib', false
  hideHomepageButton: false,
  /// homepage suggestions ///
  hideAllSuggestions: false,
  hideAllButOneRow: false,
  hideInfiniteScroll: false,
  /// video player ///
  skipAds: true,
  hideLiveChat: true,
  hideRelatedVideos: true,
  hideMiniPlayerButton: true,
  hidePlayNextButton: true,
  forceCinemaMode: true,
  /// shorts ///
  hideShorts: true,
  redirectShortsPlayer: true,
  /// misc ///
  hideSearchButton: false,
  cleanSearchResults: true,
  hideSponsoredContent: true,
  hideFilterBar: true,
  forceAudioTrack: true,
  preferredAudioLanguage: 'en', // 'en','nl','de','fr','es','it','pt','ja','ko','zh',...
  /// video buttons ///
  hideThanksButton: true,
  hideClipButton: true,
  hideSponsorButton: true,
  /// audio control ///
  blockMultiAudio: true,
  aggressiveAudioControl: true,
  /// layout fixes ///
  removeFirstColumnClass: true,
};

const SETTINGS = DEFAULT_SETTINGS;

// Mark settings in HTML
const HTML = document.documentElement;
Object.keys(SETTINGS).forEach(key => {
  HTML.setAttribute(key, SETTINGS[key]);
});

// -----------------------------
// CSS Blocklists
// -----------------------------
const DESKTOP_BLOCK_LIST = [
  // Ads
  '#masthead-ad',
  'ytd-mealbar-promo-renderer',
  'ytd-carousel-ad-renderer',
  '.ytd-display-ad-renderer',
  'ytd-ad-slot-renderer',
  'div.ytp-ad-overlay-image',
  '.iv-branding.annotation-type-custom.annotation',

  // Shorts
  'html[hideShorts="true"] ytd-rich-section-renderer',
  'html[hideShorts="true"] ytd-reel-shelf-renderer',
  'html[hideShorts="true"] ytd-shelf-renderer',

  // Left Bar Navigation
  'a[href="/feed/trending"]',
  'a[href="/feed/explore"]',
  'html[hideShorts="true"] ytd-guide-section-renderer a[title="Shorts"]',
  'html[hideShorts="true"] ytd-mini-guide-entry-renderer[aria-label="Shorts"]',
  'ytd-guide-section-renderer.ytd-guide-renderer.style-scope:nth-of-type(4)',
  'ytd-guide-section-renderer.ytd-guide-renderer.style-scope:nth-of-type(3)',

  // Homepage
  'html[hideHomepageButton="true"] a:not(#logo)[href="/"]',
  'html[hideAllSuggestions="true"] ytd-browse[page-subtype="home"]',
  'html[hideAllButOneRow="true"] ytd-browse[page-subtype="home"] #header',
  'html[hideAllButOneRow="true"] ytd-browse[page-subtype="home"] ytd-rich-grid-renderer>#contents>ytd-rich-grid-row:nth-child(n+2)',
  'html[hideInfiniteScroll="true"] ytd-browse[page-subtype="home"] ytd-rich-grid-renderer>#contents>ytd-continuation-item-renderer',

  // Video Player
  'html[hideRelatedVideos="true"] #secondary>div.circle',
  'html[hideRelatedVideos="true"] #related',
  'html[hideRelatedVideos="true"] .html5-endscreen',
  'html[hidePlayNextButton="true"] a.ytp-next-button.ytp-button',
  'html[hidePlayNextButton="true"] a.ytp-prev-button.ytp-button',
  'html[hideLiveChat="true"] #chat',
  'html[hideMiniPlayerButton="true"] .ytp-button.ytp-miniplayer-button',
  '.ytd-download-button-renderer.style-scope',

  // Video Action Buttons
  'html[hideThanksButton="true"] ytd-menu-renderer button[aria-label="Thanks"]',
  'html[hideThanksButton="true"] ytd-menu-renderer button[title="Show support with Super Thanks"]',
  'html[hideThanksButton="true"] ytd-menu-renderer yt-button-view-model:has(button[aria-label="Thanks"])',
  'html[hideClipButton="true"] ytd-menu-renderer button[aria-label="Clip"]',
  'html[hideClipButton="true"] ytd-menu-renderer button[title="Clip"]',
  'html[hideClipButton="true"] ytd-menu-renderer yt-button-view-model:has(button[aria-label="Clip"])',
  'html[hideSponsorButton="true"] #sponsor-button',
  'html[hideSponsorButton="true"] ytd-video-owner-renderer #sponsor-button',
  'html[hideSponsorButton="true"] ytd-video-owner-renderer timed-animation-button-renderer',
  'html[hideSponsorButton="true"] ytd-video-owner-renderer button[aria-label="Subscribe Plus"]',

  // Search
  'div.sbdd_a',
  '#container.ytd-search ytd-search-pyv-renderer',
  'html[hideSearchButton="true"] div.ytd-masthead>ytd-searchbox',
  'html[hideSearchButton="true"] div.ytd-masthead>#voice-search-button',

  // Filter Bar (Chips)
  'html[hideFilterBar="true"] ytd-feed-filter-chip-bar-renderer',
  'html[hideFilterBar="true"] #chips-wrapper',
];

const MOBILE_BLOCK_LIST = [
  // Ads
  'ytm-companion-ad-renderer',
  'ytm-promoted-sparkles-web-renderer',

  // Homepage
  'html[hideHomepageButton="true"] div[tab-identifier="FEwhat_to_watch"]',
  'html[hideSearchButton="true"] #header-bar > header > div > button',
  'html[hideSearchButton="true"] #center.style-scope.ytd-masthead',

  // Shorts in search results
  'html[hideShorts="true"] ytm-reel-shelf-renderer.item',

  // Video Player
  'html[hideRelatedVideos="true"] ytm-item-section-renderer[section-identifier="related-items"]>lazy-list',
  'html[hidePlayNextButton="true"] .player-controls-middle-core-buttons > div:nth-child(1)',
  'html[hidePlayNextButton="true"] .player-controls-middle-core-buttons > div:nth-child(5)',

  // Video Action Buttons - Mobile
  'html[hideThanksButton="true"] ytm-menu-renderer button[aria-label="Thanks"]',
  'html[hideThanksButton="true"] ytm-menu-renderer yt-button-view-model:has(button[aria-label="Thanks"])',
  'html[hideClipButton="true"] ytm-menu-renderer button[aria-label="Clip"]',
  'html[hideClipButton="true"] ytm-menu-renderer yt-button-view-model:has(button[aria-label="Clip"])',
  'html[hideSponsorButton="true"] ytm-video-owner-renderer #sponsor-button',
  'html[hideSponsorButton="true"] ytm-video-owner-renderer timed-animation-button-renderer',
  'html[hideSponsorButton="true"] ytm-video-owner-renderer button[aria-label="Subscribe Plus"]',

  // Navigation Bar
  'html[hideHomepageButton="true"] ytm-pivot-bar-item-renderer:nth-child(1)',
  'html[hideShorts="true"] ytm-pivot-bar-item-renderer:nth-child(2)',
  'ytm-chip-cloud-chip-renderer[chip-style="STYLE_EXPLORE_LAUNCHER_CHIP"]',

  // Filter Bar (Chips) - Mobile
  'html[hideFilterBar="true"] ytm-feed-filter-chip-bar-renderer',
];

// -----------------------------
// Utilities
// -----------------------------
function addStyle(css) {
  const style = document.createElement('style');
  style.textContent = css;
  document.head.appendChild(style);
}

function addFilterBarSpacingFix() {
  const spacingCSS = `
    /* Fix spacing when filter bar is hidden */
    html[hideFilterBar="true"] ytd-browse[page-subtype="home"] #primary #contents,
    html[hideFilterBar="true"] ytd-browse[page-subtype="home"] ytd-rich-grid-renderer,
    html[hideFilterBar="true"] ytd-browse[page-subtype="home"] #contents.ytd-rich-grid-renderer {
      padding-top: 32px !important;
      margin-top: 16px !important;
    }
    html[hideFilterBar="true"] ytd-browse[page-subtype="home"] ytd-rich-grid-row:first-child {
      margin-top: 24px !important;
      padding-top: 16px !important;
    }
    html[hideFilterBar="true"] ytd-browse[page-subtype="home"] #primary > #contents {
      padding-top: 32px !important;
    }
    /* Mobile */
    html[hideFilterBar="true"] ytm-browse[page-subtype="home"] #contents,
    html[hideFilterBar="true"] ytm-rich-grid-renderer #contents {
      padding-top: 24px !important;
      margin-top: 12px !important;
    }
    html[hideFilterBar="true"] ytd-browse[page-subtype="subscriptions"] #primary #contents {
      padding-top: 32px !important;
    }
  `;
  addStyle(spacingCSS);
}

// -----------------------------
// Robust remover for first-column marker (class OR attribute, Shadow DOM aware)
// -----------------------------

// Walk document + nested shadow roots
function* deepElements(root = document) {
  const stack = [root];
  while (stack.length) {
    const node = stack.pop();
    if (!node) continue;

    if (node.nodeType === 1) yield node; // element

    const sr = node.shadowRoot;
    if (sr) for (let i = sr.children.length - 1; i >= 0; i--) stack.push(sr.children[i]);
    if (node.children) for (let i = node.children.length - 1; i >= 0; i--) stack.push(node.children[i]);
  }
}

function clearFirstColumnFlagOn(el) {
  // Remove attribute form (homepage uses this)
  if (el.hasAttribute && el.hasAttribute('is-in-first-column')) {
    el.removeAttribute('is-in-first-column');
  }
  // Remove class form (some pages still use this)
  if (el.classList?.contains('is-in-first-column')) {
    el.classList.remove('is-in-first-column');
  }
}

function stripFirstColumnFlagDeep(root = document) {
  for (const el of deepElements(root)) {
    clearFirstColumnFlagOn(el);
  }
}

function observeDeep(target, cb) {
  const observers = new Set();

  const attach = (rootNode) => {
    if (!rootNode) return;
    const obs = new MutationObserver(cb);
    obs.observe(rootNode, {
      childList: true,
      subtree: true,
      attributes: true,
      // Watch both class and the boolean attribute
      attributeFilter: ['class', 'is-in-first-column'],
    });
    observers.add(obs);
  };

  attach(target);
  if (document.body) attach(document.body);

  // Attach to existing shadow roots
  for (const el of deepElements(document)) {
    if (el.shadowRoot) attach(el.shadowRoot);
  }

  // Discover future shadow roots
  const discover = new MutationObserver(() => {
    for (const el of deepElements(document)) {
      if (el.shadowRoot) attach(el.shadowRoot);
    }
  });
  discover.observe(document.documentElement, { childList: true, subtree: true });

  return () => {
    discover.disconnect();
    observers.forEach(o => o.disconnect());
  };
}

function setupFirstColumnStripping() {
  if (!SETTINGS.removeFirstColumnClass) return;

  const run = () => stripFirstColumnFlagDeep(document);

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', run, { once: true });
  } else {
    run();
  }

  observeDeep(document, (mutations) => {
    for (const m of mutations) {
      if (m.type === 'attributes') {
        const el = m.target;
        clearFirstColumnFlagOn(el);
      } else if (m.type === 'childList') {
        m.addedNodes.forEach(n => {
          if (n.nodeType === 1) stripFirstColumnFlagDeep(n);
        });
      }
    }
  });

  // Periodic safety sweep
  setInterval(run, 1200);
}

// -----------------------------
// Audio track control
// -----------------------------
function initializeAudioBlocking() {
  function hookYouTubePlayer() {
    if (!location.hostname.includes('youtube.com')) return;
    const playerElement = document.querySelector('#movie_player');
    if (!playerElement) return;

    try {
      if (playerElement.setAudioTrack && !playerElement.setAudioTrackHooked) {
        playerElement.setAudioTrackHooked = true;
        const original = playerElement.setAudioTrack;
        playerElement.setAudioTrack = function (trackId) {
          if (SETTINGS.aggressiveAudioControl && trackId !== 0) return;
          return original.call(this, trackId);
        };
      }
      if (playerElement.selectAudioTrack && !playerElement.selectAudioTrackHooked) {
        playerElement.selectAudioTrackHooked = true;
        const original = playerElement.selectAudioTrack;
        playerElement.selectAudioTrack = function (trackId) {
          if (SETTINGS.aggressiveAudioControl && trackId !== 0) return;
          return original.call(this, trackId);
        };
      }
      if (playerElement.changeAudioTrack && !playerElement.changeAudioTrackHooked) {
        playerElement.changeAudioTrackHooked = true;
        const original = playerElement.changeAudioTrack;
        playerElement.changeAudioTrack = function (trackId) {
          if (SETTINGS.aggressiveAudioControl && trackId !== 0) return;
          return original.call(this, trackId);
        };
      }
    } catch (_) { /* silent */ }
  }

  const originalCreateElement = document.createElement;
  document.createElement = function (tagName) {
    const element = originalCreateElement.call(this, tagName);
    if (String(tagName).toLowerCase() === 'video') {
      const checkForTracks = setInterval(() => {
        try {
          if (element.audioTracks && element.audioTracks.length > 0) {
            for (let i = 0; i < element.audioTracks.length; i++) {
              element.audioTracks[i].enabled = (i === 0);
            }
            const monitorInterval = setInterval(() => {
              if (!document.contains(element)) return clearInterval(monitorInterval);
              if (element.audioTracks && element.audioTracks.length > 1) {
                const active = Array.from(element.audioTracks).findIndex(t => t.enabled);
                if (active !== 0) {
                  for (let i = 0; i < element.audioTracks.length; i++) {
                    element.audioTracks[i].enabled = (i === 0);
                  }
                }
              }
            }, 2000);
            clearInterval(checkForTracks);
          }
        } catch (_) { /* silent */ }
      }, 500);
      setTimeout(() => clearInterval(checkForTracks), 30000);
    }
    return element;
  };

  hookYouTubePlayer();
  setInterval(hookYouTubePlayer, 2000);

  if (SETTINGS.blockMultiAudio) {
    const audioBlockCSS = `
      html[blockMultiAudio="true"] .ytp-menuitem[role="menuitem"]:has([aria-label*="audio" i]),
      html[blockMultiAudio="true"] .ytp-menuitem[role="menuitem"]:has([aria-label*="sprache" i]),
      html[blockMultiAudio="true"] .ytp-menuitem[role="menuitem"]:has([aria-label*="langue" i]),
      html[blockMultiAudio="true"] .ytp-menuitem[role="menuitem"]:has([aria-label*="track" i]) { display: none !important; }
      html[blockMultiAudio="true"] .ytp-chrome-controls .ytp-button[aria-label*="audio" i] { display: none !important; }
      html[blockMultiAudio="true"] .ytp-panel-menu .ytp-menuitem:has([class*="audio"]) { display: none !important; }
    `;
    addStyle(audioBlockCSS);
  }
}

// -----------------------------
// Dynamic behaviors
// -----------------------------
function redirectHomepage() {
  if (location.pathname === '/') {
    if (SETTINGS.redirectHomepage === 'wl')   location.replace('/playlist/?list=WL');
    if (SETTINGS.redirectHomepage === 'subs') location.replace('/feed/subscriptions');
    if (SETTINGS.redirectHomepage === 'lib')  location.replace('/feed/library');
  }
}

function redirectShortsPlayer() {
  if (location.pathname.startsWith('/shorts')) {
    const redirPath = location.pathname.replace('shorts', 'watch');
    location.replace(redirPath);
  }
}

function disableRelatedAutoPlay() {
  document.querySelectorAll('.ytp-autonav-toggle-button[aria-checked=true]').forEach(e => e.offsetParent && e.click());
  document.querySelectorAll('.ytm-autonav-toggle-button-container[aria-pressed=true]').forEach(e => e.offsetParent && e.click());
}

function forceAudioTrack() {
  if (!location.pathname.startsWith('/watch')) return;
  const video = document.querySelector('.html5-main-video');
  if (!video || !video.audioTracks) return;

  if (video.audioTracks.length > 1) {
    const preferred = String(SETTINGS.preferredAudioLanguage || '').toLowerCase();
    let target = 0;
    for (let i = 0; i < video.audioTracks.length; i++) {
      const lang = (video.audioTracks[i].language || '').toLowerCase();
      if (lang === preferred || lang.startsWith(preferred + '-')) { target = i; break; }
    }
    for (let i = 0; i < video.audioTracks.length; i++) video.audioTracks[i].enabled = (i === target);
  }
}

function forceCinemaMode() {
  if (!location.pathname.startsWith('/watch')) return;
  const pageManager = document.querySelector('ytd-watch-flexy');
  if (pageManager && pageManager.hasAttribute('theater')) return;

  const clickIfVisible = sel => {
    const el = document.querySelector(sel);
    if (el && el.offsetParent) el.click();
  };
  clickIfVisible('.ytp-size-button');
  clickIfVisible('button[aria-keyshortcuts="t"]');
  clickIfVisible('.ytp-button[data-tooltip-target-id*="theater"]');
}

function skipVideoAds() {
  if (!location.pathname.startsWith('/watch')) return;

  document.querySelector(".ytp-ad-skip-button-slot button,.ytp-ad-overlay-close-button")?.click();

  const adShowing = document.querySelector('.ad-showing');
  if (adShowing) {
    const video = document.querySelector('.html5-main-video');
    if (video && !isNaN(video.duration)) {
      video.play();
      video.currentTime = video.duration;
    }
  }
}

function cleanSearchResults() {
  if (!location.pathname.startsWith('/results')) return;
  const badges = document.querySelectorAll('ytm-badge');
  badges.forEach(badge => {
    if (badge.innerText === '相關影片' || badge.innerText === '相关视频' || badge.innerText === 'Related') {
      badge.closest('ytm-video-with-context-renderer')?.remove();
    }
  });
}

function runDynamicSettings() {
  if (SETTINGS.redirectHomepage) redirectHomepage();
  if (SETTINGS.redirectShortsPlayer) redirectShortsPlayer();
  if (SETTINGS.cleanSearchResults) cleanSearchResults();
  if (SETTINGS.skipAds) skipVideoAds();
  if (SETTINGS.hideRelatedVideos) disableRelatedAutoPlay();
  if (SETTINGS.forceCinemaMode) forceCinemaMode();
  if (SETTINGS.forceAudioTrack) forceAudioTrack();
  setTimeout(runDynamicSettings, 500);
}

// -----------------------------
// Boot
// -----------------------------
(function init() {
  if (location.hostname.startsWith('www.')) {
    addStyle(DESKTOP_BLOCK_LIST.map(e => `${e} {display: none !important}`).join('\n'));
    addFilterBarSpacingFix();
  }
  if (location.hostname.startsWith('m.')) {
    addStyle(MOBILE_BLOCK_LIST.map(e => `${e} {display: none !important}`).join('\n'));
    addFilterBarSpacingFix();
  }

  if (SETTINGS.blockMultiAudio) {
    initializeAudioBlocking();
  }

  setupFirstColumnStripping();     // <— robust removal for .is-in-first-column
  runDynamicSettings();
})();