Members-Only Remover

Filters Members-only entries out of YouTube API responses, and removes the members-only shelf.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         Members-Only Remover 
// @namespace    https://example.com/memonly
// @version      1.0
// @description  Filters Members-only entries out of YouTube API responses, and removes the members-only shelf.
// @match        https://www.youtube.com/*
// @match        https://youtube.com/*
// @grant        none
// @author       Mr005k 
// @license      MIT
// @run-at       document-start
// ==/UserScript==

(() => {
  'use strict';

  // ---------- Detection ----------
  const MEM_RE = /\bmembers\s*[- ]?\s*only\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;
    // Direct style flags used by YT JSON
    if (typeof o.style === 'string' && o.style.includes('MEMBERS_ONLY')) return true;
    if (typeof o.badgeStyle === 'string' && o.badgeStyle.includes('MEMBERS_ONLY')) return true;

    // Textual labels
    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) {
        // Skip huge binary-ish fields
        if (k === 'playerResponse' || k === 'responseContext') continue;
        if (deepHasMembersOnly(o[k], depth + 1)) return true;
      }
    }
    return false;
  }

  // Remove any array item whose subtree advertises "Members only"
  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)) 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();
      const scrubbed = scrubJSON(data);
      // If nothing changed, pass original response
      if (JSON.stringify(data) === JSON.stringify(scrubbed)) 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 (some pages still use it)
  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;
        const text = this.responseText;
        const json = JSON.parse(text);
        const scrubbed = scrubJSON(json);
        const newText = JSON.stringify(scrubbed);
        if (newText !== text) {
          Object.defineProperty(this, 'responseText', { value: newText });
          Object.defineProperty(this, 'response', { value: newText });
        }
      } catch (_) {}
    });
    return _send.apply(this, arguments);
  };

  // ---------- DOM fallback (strict) ----------
  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'
  ].join(',');

  const POLYMER_BADGE = '.badge.badge-style-type-members-only';
  const VM_BADGE_TEXT = '.yt-badge-shape--commerce .yt-badge-shape__text';

  function isStrictBadge(el) {
    if (!(el instanceof Element)) return false;
    if (el.matches(POLYMER_BADGE)) return true;
    if (el.matches(VM_BADGE_TEXT) && MEM_RE.test(el.textContent || '')) return true;
    return false;
  }

  function dropTileFromBadge(badge) {
    const item = badge.closest(ITEM_SEL);
    if (item) item.remove();
  }

  function pruneMembersShelf(root = document) {
    // Remove the channel home shelf titled "Members-only videos"
    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 scanDOM(root = document) {
    root.querySelectorAll(POLYMER_BADGE).forEach(dropTileFromBadge);
    root.querySelectorAll(VM_BADGE_TEXT).forEach(n => {
      if (MEM_RE.test(n.textContent || '')) dropTileFromBadge(n);
    });
    pruneMembersShelf(root);
  }

  function observeDOM() {
    const mo = new MutationObserver(muts => {
      for (const m of muts) {
        if (m.type === 'childList') {
          for (const n of m.addedNodes) {
            if (!(n instanceof Element)) continue;
            if (isStrictBadge(n)) dropTileFromBadge(n);
            else 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();
  }
})();