Members-Only Remover

Filters Members-only entries out of YouTube API responses, and hides member promo UI.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Members-Only Remover
// @namespace    https://example.com/memonly
// @version      1.4.0
// @description  Filters Members-only entries out of YouTube API responses, and hides member promo UI.
// @match        https://www.youtube.com/*
// @match        https://youtube.com/*
// @grant        none
// @license      MIT
// @run-at       document-start
// ==/UserScript==
 
(() => {
  'use strict';
 
  // ---------- Detection ----------
  const MEM_RE = /\bmembers?\s*[- ]?\s*only\b/i;
  const JOIN_THIS_CHANNEL_RE = /\bjoin\s+this\s+channel\b/i;
 
  function extractText(obj) {
    if (!obj) return '';
    if (typeof obj === 'string') return obj;
    if (obj.simpleText) return String(obj.simpleText);
    if (Array.isArray(obj.runs)) return obj.runs.map(r => (r && r.text) || '').join('');
    if (obj.text) return extractText(obj.text);
    if (obj.label) return String(obj.label);
    return '';
  }
 
  function nodeLooksMembersOnly(o) {
    if (!o || typeof o !== 'object') return false;
 
    if (typeof o.style === 'string' && o.style.includes('MEMBERS_ONLY')) return true;
    if (typeof o.badgeStyle === 'string' && o.badgeStyle.includes('MEMBERS_ONLY')) return true;
 
    if (MEM_RE.test(extractText(o))) return true;
 
    return false;
  }
 
  function deepHasMembersOnly(o, depth = 0) {
    if (depth > 6 || !o) return false;
    if (nodeLooksMembersOnly(o)) return true;
 
    if (Array.isArray(o)) {
      for (const it of o) if (deepHasMembersOnly(it, depth + 1)) return true;
      return false;
    }
    if (typeof o === 'object') {
      for (const k in o) {
        if (k === 'playerResponse' || k === 'responseContext') continue;
        if (deepHasMembersOnly(o[k], depth + 1)) return true;
      }
    }
    return false;
  }
 
  let didScrub = false;
 
  function scrubJSON(x, depth = 0) {
    if (depth > 8 || x == null) return x;
 
    if (Array.isArray(x)) {
      const out = [];
      for (const it of x) {
        if (deepHasMembersOnly(it)) {
          didScrub = true;
          continue;
        }
        out.push(scrubJSON(it, depth + 1));
      }
      return out;
    }
 
    if (typeof x === 'object') {
      for (const k in x) x[k] = scrubJSON(x[k], depth + 1);
    }
    return x;
  }
 
  // ---------- Network interception (fetch + XHR) ----------
  const shouldFilterURL = url =>
    typeof url === 'string' &&
    /\/youtubei\/v1\/(browse|search|next|reel|guide)/.test(url);
 
  // fetch
  const _fetch = window.fetch;
  window.fetch = async function(input, init) {
    const res = await _fetch(input, init);
    try {
      const url = (typeof input === 'string' ? input : input.url) || res.url || '';
      if (!shouldFilterURL(url)) return res;
 
      const clone = res.clone();
      const data = await clone.json();
 
      didScrub = false;
      const scrubbed = scrubJSON(data);
      if (!didScrub) return res;
 
      const body = JSON.stringify(scrubbed);
      const headers = new Headers(res.headers);
      headers.set('content-type', 'application/json; charset=UTF-8');
      return new Response(body, { status: res.status, statusText: res.statusText, headers });
    } catch (_) {
      return res; // fail open
    }
  };
 
  // XHR
  const _open = XMLHttpRequest.prototype.open;
  const _send = XMLHttpRequest.prototype.send;
  XMLHttpRequest.prototype.open = function(method, url, async, user, pass) {
    this.__yt_url = url;
    return _open.apply(this, arguments);
  };
  XMLHttpRequest.prototype.send = function() {
    this.addEventListener('readystatechange', function() {
      if (this.readyState !== 4) return;
      try {
        if (!shouldFilterURL(this.__yt_url)) return;
 
        if (this.responseType === 'json' && this.response && typeof this.response === 'object') {
          didScrub = false;
          const scrubbed = scrubJSON(this.response);
          if (didScrub) Object.defineProperty(this, 'response', { value: scrubbed });
          return;
        }
 
        const text = this.responseText;
        if (!text || (text[0] !== '{' && text[0] !== '[')) return;
 
        const json = JSON.parse(text);
        didScrub = false;
        const scrubbed = scrubJSON(json);
        if (!didScrub) return;
 
        const newText = JSON.stringify(scrubbed);
        Object.defineProperty(this, 'responseText', { value: newText });
        Object.defineProperty(this, 'response', { value: newText });
      } catch (_) {}
    });
    return _send.apply(this, arguments);
  };
 
  // ---------- DOM fallback ----------
  const ITEM_SEL = [
    'ytd-rich-item-renderer',
    'yt-lockup-view-model',
    'ytd-video-renderer',
    'ytd-compact-video-renderer',
    'ytd-grid-video-renderer',
    'ytd-playlist-video-renderer',
    'ytd-playlist-panel-video-renderer',
    'ytd-radio-renderer',
    'ytd-reel-item-renderer',
    'ytd-reel-video-renderer',
    'ytd-rich-grid-media',
    'ytd-rich-grid-slim-media',
    'ytd-item-section-renderer'
  ].join(',');
 
  const POLYMER_BADGE = [
    '.badge.badge-style-type-members-only',
    'badge-shape[aria-label*="Members only" i]'
  ].join(',');
 
  const VM_BADGE_TEXT = '.yt-badge-shape__text, .yt-badge-shape_text, .yt-badge-shapetext';
 
  const OVERLAY_BADGE_SEL = [
    'ytd-thumbnail-overlay-time-status-renderer',
    'ytd-thumbnail-overlay-badge-renderer',
    'ytd-thumbnail-overlay-badge-view-model',
    'ytd-badge-supported-renderer',
    'yt-badge-shape',
    'badge-shape'
  ].join(',');
 
  // Join / members promo selectors
  const JOIN_BUTTON_SEL = [
    'button[aria-label*="Join this channel" i]',
    'a[aria-label*="Join this channel" i]'
  ].join(',');
 
  function softHide(el) {
    if (!(el instanceof Element)) return;
    if (el.dataset.memonlyHidden === '1') return;
    el.dataset.memonlyHidden = '1';
    el.style.setProperty('display', 'none', 'important');
  }
 
  function badgeSaysMembersOnly(el) {
    if (!(el instanceof Element)) return false;
    const aria = el.getAttribute?.('aria-label') || '';
    const txt = el.textContent || '';
    return MEM_RE.test(`${aria} ${txt}`);
  }
 
  function dropTileFromBadge(badge) {
    const item = badge.closest(ITEM_SEL);
    if (item) item.remove();
  }
 
  function pruneMembersShelf() {
    document.querySelectorAll('ytd-shelf-renderer').forEach(shelf => {
      const title = (shelf.querySelector('#title')?.textContent || '').trim();
      const subtitle = (shelf.querySelector('#subtitle')?.textContent || '').trim();
      if (MEM_RE.test(title) || /videos available to members/i.test(subtitle)) {
        shelf.remove();
      }
    });
  }
 
  function hideJoinPromos(root = document) {
    // The "Our members" recognition shelf
    root.querySelectorAll('ytd-recognition-shelf-renderer').forEach(softHide);
 
    // Watch-page sponsor/join container
    root.querySelectorAll('#sponsor-button').forEach(softHide);
 
    // Any "Join this channel" button variants:
    root.querySelectorAll(JOIN_BUTTON_SEL).forEach(btn => {
      const host =
        btn.closest('#sponsor-button') ||
        btn.closest('ytd-recognition-shelf-renderer') ||
        btn.closest('timed-animation-button-renderer') ||
        btn.closest('ytd-button-renderer') ||
        btn.closest('button-view-model') ||
        btn.closest('yt-button-shape') ||
        btn;
      softHide(host);
    });
 
    // The specific flexible-actions wrapper
    root.querySelectorAll('.ytFlexibleActionsViewModelAction').forEach(w => {
      const hasJoin = w.querySelector('button[aria-label*="Join this channel" i], a[aria-label*="Join this channel" i]');
      if (hasJoin) softHide(w);
    });
  }
 
  function scanDOM(root = document) {
    // Additional lockup-view-model filters with commerce badges
    root.querySelectorAll('.yt-lockup-view-model.yt-lockup-view-model--horizontal.yt-lockup-view-model--compact').forEach(lockup => {
      const commerceBadge = lockup.querySelector('.yt-badge-shape.yt-badge-shape--commerce');
      if (commerceBadge) {
        const badgeText = lockup.querySelector('.yt-badge-shapetext, .yt-badge-shape_text');
        if (badgeText && MEM_RE.test(badgeText.textContent || '')) {
          lockup.remove();
        }
      }
    });
 
    // Generic yt-lockup-view-model with members-only badges
    root.querySelectorAll('yt-lockup-view-model').forEach(lockup => {
      const badgeText = lockup.querySelector('.yt-badge-shape_text, .yt-badge-shapetext');
      if (badgeText && MEM_RE.test(badgeText.textContent || '')) {
        lockup.remove();
      }
    });
 
    // Item section renderers with members-only badges
    root.querySelectorAll('ytd-item-section-renderer').forEach(section => {
      const lockup = section.querySelector('yt-lockup-view-model');
      if (lockup) {
        const badgeText = lockup.querySelector('.yt-badge-shapetext');
        if (badgeText && MEM_RE.test(badgeText.textContent || '')) {
          section.remove();
        }
      }
    });
 
    // Existing filters
    root.querySelectorAll(POLYMER_BADGE).forEach(badge => {
      if (badgeSaysMembersOnly(badge)) dropTileFromBadge(badge);
    });
 
    root.querySelectorAll(VM_BADGE_TEXT).forEach(n => {
      if (MEM_RE.test(n.textContent || '')) dropTileFromBadge(n);
    });
 
    root.querySelectorAll(OVERLAY_BADGE_SEL).forEach(n => {
      if (badgeSaysMembersOnly(n)) dropTileFromBadge(n);
    });
 
    root.querySelectorAll('[aria-label*="Members only" i]').forEach(n => {
      if (badgeSaysMembersOnly(n)) dropTileFromBadge(n);
    });
 
    pruneMembersShelf();
    hideJoinPromos(root);
  }
 
  function observeDOM() {
    const mo = new MutationObserver(muts => {
      for (const m of muts) {
        if (m.type !== 'childList') continue;
 
        for (const n of m.addedNodes) {
          if (!(n instanceof Element)) continue;
 
          const aria = n.getAttribute?.('aria-label') || '';
          if (JOIN_THIS_CHANNEL_RE.test(aria)) hideJoinPromos(n);
 
          scanDOM(n);
        }
      }
    });
 
    mo.observe(document.documentElement, { childList: true, subtree: true });
 
    const rescan = () => setTimeout(() => scanDOM(document), 50);
    window.addEventListener('yt-navigate-finish', rescan);
    window.addEventListener('yt-page-data-updated', rescan);
  }
 
  // Boot
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => { scanDOM(); observeDOM(); });
  } else {
    scanDOM(); observeDOM();
  }
})();