YouTube: Hide "Members Only" Videos

Hides all YouTube videos with the "Members only" badge.

// ==UserScript==
// @name         YouTube: Hide "Members Only" Videos
// @namespace    https://github.com/krrrrrrk/youtube-hide-members-only
// @version      1.0
// @description  Hides all YouTube videos with the "Members only" badge.
// @author       krrrrrrk
// @match        https://www.youtube.com/*
// @run-at       document-idle
// @grant        none
// @license      MIT
// @homepageURL  https://github.com/Krrrrrrk/YouTube-Hide-Members-only-videos
// ==/UserScript==

(function () {
  "use strict";

  // --- Config --------------------------------------------------------------
  // Badge text to match (case-insensitive). Add translations if you need them.
  const BADGE_TEXTS = [
    "Members only",
    // "Miembros solamente", "Nur für Mitglieder", etc.
  ].map(s => s.toLowerCase());

  // Which container tags we consider a "video tile"
  const VIDEO_TILE_SELECTORS = [
    // Classic & search
    "ytd-video-renderer",
    "ytd-grid-video-renderer",
    "ytd-compact-video-renderer",
    "ytd-playlist-video-renderer",
    "ytd-rich-item-renderer",
    "ytd-reel-item-renderer",         // Shorts in some contexts
    "ytd-radio-renderer",
    // New(er) lockup model you pasted
    ".yt-lockup-view-model",
  ].join(",");

  // A few known badge containers/leafs that may contain the text
  const BADGE_SELECTORS = [
    ".yt-badge-shape__text",
    ".yt-content-metadata-view-model__badge",
    "yt-badge-view-model",
    "badge-shape",
    // Fallback: anything in content metadata blocks
    ".yt-content-metadata-view-model",
  ].join(",");

  // --- Helpers -------------------------------------------------------------
  const lc = s => (s || "").toLowerCase();

  function isMembersOnlyBadge(el) {
    const txt = lc(el.textContent || "");
    return BADGE_TEXTS.some(t => txt.includes(t));
  }

  function findVideoTile(start) {
    if (!start) return null;
    // climb up to a recognized tile container
    return start.closest(VIDEO_TILE_SELECTORS);
  }

  function hideTile(tile) {
    if (!tile || tile.dataset._tmMembersHidden === "1") return;
    tile.style.display = "none";
    tile.dataset._tmMembersHidden = "1";
  }

  function processNode(root) {
    if (!root || root.nodeType !== 1) return;

    // Fast path: if the node itself is a badge, check it
    if (root.matches && root.matches(BADGE_SELECTORS) && isMembersOnlyBadge(root)) {
      const tile = findVideoTile(root);
      if (tile) hideTile(tile);
      return;
    }

    // Otherwise, look inside for any badges
    const badges = root.querySelectorAll(BADGE_SELECTORS);
    for (const b of badges) {
      if (isMembersOnlyBadge(b)) {
        const tile = findVideoTile(b);
        if (tile) hideTile(tile);
      }
    }
  }

  // Debounced rescans help when Polymer reflows a lot at once.
  let pending = false;
  function rescanSoon() {
    if (pending) return;
    pending = true;
    requestAnimationFrame(() => {
      pending = false;
      processNode(document.body);
    });
  }

  // Initial pass
  processNode(document.body);

  // Watch for dynamic additions
  const mo = new MutationObserver(mutations => {
    for (const m of mutations) {
      // Process added nodes (tiles/badges often arrive as fragments)
      m.addedNodes && m.addedNodes.forEach(node => processNode(node));
      // If text changed inside a badge, rescan
      if (m.type === "characterData") rescanSoon();
    }
  });

  mo.observe(document.documentElement || document, {
    childList: true,
    subtree: true,
    characterData: true,
  });

  // Also rescan on route changes (YouTube SPA navigation)
  const pushState = history.pushState;
  const replaceState = history.replaceState;
  function onNav() { setTimeout(() => processNode(document.body), 300); }
  history.pushState = function () { pushState.apply(this, arguments); onNav(); };
  history.replaceState = function () { replaceState.apply(this, arguments); onNav(); };
  window.addEventListener("yt-navigate-finish", onNav);
  window.addEventListener("popstate", onNav);
})();