YouTube: Hide "Members Only" Videos

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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);
})();