Members-Only Remover

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

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.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

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