Members-Only Remover

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

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

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

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

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.

(Zateb bir user-style yöneticim var, yükleyeyim!)

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