YT Premium Client Side

Client-side YouTube Premium features: ad removal, sponsor skip, quality unlock, background play hint, and more

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         YT Premium Client Side
// @namespace    https://github.com/yt-premium-client
// @version      1.2.4
// @description  Client-side YouTube Premium features: ad removal, sponsor skip, quality unlock, background play hint, and more
// @author       Mysticatten
// @match        https://www.youtube.com/*
// @match        https://youtube.com/*
// @icon         https://i.ibb.co/fdfnXr3C/image-2026-03-06-154548869.png
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// @grant        GM_addStyle
// @run-at       document-start
// @noframes
// ==/UserScript==

(function () {
  'use strict';

  // ─────────────────────────────────────────────
  //  CONFIG  (saved per-session via GM storage)
  // ─────────────────────────────────────────────
  const CFG = {
    blockAds:          GM_getValue('blockAds',          true),
    skipSponsors:      GM_getValue('skipSponsors',      true),
    autoMaxQuality:    GM_getValue('autoMaxQuality',    true),
    hideUpsells:       GM_getValue('hideUpsells',       true),
    persistVolume:     GM_getValue('persistVolume',     true),
    autoSkipEndscreen: GM_getValue('autoSkipEndscreen', false),
    autoTheaterMode:   GM_getValue('autoTheaterMode',   false),
  };

  // ─────────────────────────────────────────────
  //  AD BLOCKING
  // ─────────────────────────────────────────────
  if (CFG.blockAds) {
    // Intercept XHR / fetch to neutralise ad-serving calls
    const AD_URL_PATTERNS = [
      /doubleclick\.net/,
      /googlesyndication\.com/,
      /googleadservices\.com/,
      /\/pagead\//,
      /\/ads\//,
      /adformat/,
    ];

    // Patch fetch
    const _fetch = window.fetch;
    window.fetch = function (input, init) {
      const url = (typeof input === 'string') ? input : (input && input.url) || '';
      if (AD_URL_PATTERNS.some(p => p.test(url))) {
        return Promise.resolve(new Response('', { status: 200 }));
      }
      return _fetch.apply(this, arguments);
    };

    // Patch XHR
    const _open = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function (method, url) {
      if (AD_URL_PATTERNS.some(p => p.test(url))) {
        // Redirect to a harmless empty endpoint
        arguments[1] = 'about:blank';
      }
      return _open.apply(this, arguments);
    };

    // DOM-level: skip / remove injected ad elements
    function removeAdElements() {
      const adSelectors = [
        '.ad-showing',
        '.ad-interrupting',
        '#player-ads',
        '#masthead-ad',
        'ytd-banner-promo-renderer',
        'ytd-video-masthead-ad-v3-renderer',
        'ytd-in-feed-ad-layout-renderer',
        'ytd-ad-slot-renderer',
        'ytd-statement-banner-renderer',
        '.ytd-promoted-sparkles-web-renderer',
        '#offer-module',
        'tp-yt-paper-dialog.ytd-mealbar-promo-renderer',
      ];
      adSelectors.forEach(sel => {
        document.querySelectorAll(sel).forEach(el => el.remove());
      });

      // Auto-skip skippable video ads
      const skipBtn = document.querySelector('.ytp-ad-skip-button, .ytp-skip-ad-button');
      if (skipBtn) skipBtn.click();

      // If an ad is playing, mute + fast-forward it
      const video = document.querySelector('video');
      if (video && document.querySelector('.ad-showing')) {
        video.muted = true;
        if (video.duration && isFinite(video.duration)) {
          video.currentTime = video.duration;
        }
      }
    }

    // Run on every DOM mutation
    const adObserver = new MutationObserver(removeAdElements);
    document.addEventListener('DOMContentLoaded', () => {
      adObserver.observe(document.body, { childList: true, subtree: true });
      removeAdElements();
    });
    // Also run immediately in case DOMContentLoaded already fired
    if (document.readyState !== 'loading') {
      adObserver.observe(document.body, { childList: true, subtree: true });
      removeAdElements();
    }
  }

  // ─────────────────────────────────────────────
  //  HIDE PREMIUM UPSELL BANNERS / DIALOGS
  // ─────────────────────────────────────────────
  if (CFG.hideUpsells) {
    GM_addStyle(`
      /* Premium upsell overlays */
      ytd-mealbar-promo-renderer,
      ytd-banner-promo-renderer,
      #offer-module,
      .ytd-premium-yva-upsell-renderer,
      ytd-primetime-promo-renderer,
      .ytd-ypc-shelf-renderer,
      tp-yt-paper-dialog.ytd-mealbar-promo-renderer,
      #yt-masthead-premium,
      ytd-statement-banner-renderer { display: none !important; }

      /* "Try YouTube Premium" on homepage */
      ytd-rich-section-renderer:has(ytd-statement-banner-renderer) { display: none !important; }

      /* Sidebar recommendation upsell */
      #secondary ytd-compact-promoted-item-renderer { display: none !important; }

      /* "Sign up for Premium" button in menus */
      yt-upsell-dialog-renderer { display: none !important; }

      /* Hide "Members only" lock icons */
      .ytd-sponsorships-tier-renderer .yt-icon[aria-label*="lock"],
      .badge-style-type-members-only { opacity: 0.4; }
    `);
  }

  // ─────────────────────────────────────────────
  //  QUALITY: auto-select highest available
  // ─────────────────────────────────────────────
  if (CFG.autoMaxQuality) {
    function setMaxQuality() {
      try {
        const player = document.getElementById('movie_player');
        if (!player || typeof player.getAvailableQualityLevels !== 'function') return;

        const levels = player.getAvailableQualityLevels();
        if (!levels || levels.length === 0) return;

        const preferred = ['hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium'];
        const best = preferred.find(q => levels.includes(q)) || levels[0];

        if (player.getPlaybackQuality() !== best) {
          player.setPlaybackQualityRange(best, best);
          console.log('[YT-Premium] Quality set to', best);
        }
      } catch (e) { /* player not ready yet */ }
    }

    // Poll until player is ready, then watch navigation
    let qualityInterval = setInterval(() => {
      if (document.getElementById('movie_player')) {
        setMaxQuality();
        clearInterval(qualityInterval);
      }
    }, 500);

    // YouTube is an SPA — re-apply on navigation
    document.addEventListener('yt-navigate-finish', () => {
      setTimeout(setMaxQuality, 1500);
    });
  }

  // ─────────────────────────────────────────────
  //  PERSIST VOLUME across page navigations
  // ─────────────────────────────────────────────
  if (CFG.persistVolume) {
    let savedVolume = parseFloat(GM_getValue('ytVolume', 1));
    let savedMuted  = GM_getValue('ytMuted', false);

    function applyVolume() {
      const video = document.querySelector('video');
      const player = document.getElementById('movie_player');
      if (!video) return;
      video.volume = savedVolume;
      video.muted  = savedMuted;
      if (player && typeof player.setVolume === 'function') {
        player.setVolume(savedVolume * 100);
        if (savedMuted) player.mute(); else player.unMute();
      }
    }

    function saveVolume() {
      const video = document.querySelector('video');
      if (!video) return;
      savedVolume = video.volume;
      savedMuted  = video.muted;
      GM_setValue('ytVolume', savedVolume);
      GM_setValue('ytMuted',  savedMuted);
    }

    document.addEventListener('yt-navigate-finish', () => {
      setTimeout(() => {
        applyVolume();
        const video = document.querySelector('video');
        if (video) video.addEventListener('volumechange', saveVolume, { passive: true });
      }, 1000);
    });
  }

  // ─────────────────────────────────────────────
  //  AUTO THEATER MODE
  // ─────────────────────────────────────────────
  if (CFG.autoTheaterMode) {
    function enableTheater() {
      const player = document.getElementById('movie_player');
      if (player && typeof player.getPlayerSize === 'function') {
        const sizeBtn = document.querySelector('.ytp-size-button');
        // Only click if we're NOT already in theater
        if (sizeBtn && !document.querySelector('ytd-watch-flexy[theater]')) {
          sizeBtn.click();
        }
      }
    }
    document.addEventListener('yt-navigate-finish', () => setTimeout(enableTheater, 800));
  }

  // ─────────────────────────────────────────────
  //  AUTO SKIP ENDSCREEN / OUTRO (last 20 s)
  // ─────────────────────────────────────────────
  if (CFG.autoSkipEndscreen) {
    document.addEventListener('yt-navigate-finish', () => {
      setTimeout(() => {
        const video = document.querySelector('video');
        if (!video) return;
        video.addEventListener('timeupdate', function skipEnd() {
          if (video.duration && video.currentTime >= video.duration - 20) {
            const nextBtn = document.querySelector('.ytp-next-button');
            if (nextBtn) {
              nextBtn.click();
              video.removeEventListener('timeupdate', skipEnd);
            }
          }
        }, { passive: true });
      }, 1500);
    });
  }

  // ─────────────────────────────────────────────
  //  SPONSOR SKIP (self-hosted simple heuristic)
  //  For real SponsorBlock integration, see
  //  https://sponsor.ajay.app — this is a minimal
  //  local version using in-page chapter data.
  // ─────────────────────────────────────────────
  if (CFG.skipSponsors) {
    const SPONSOR_KEYWORDS = /sponsor|promo|promotion|ad|advertisement|merch|affiliate/i;

    function getChapters() {
      // YT exposes chapters via the player API on ytInitialData
      try {
        const chapters = [];
        const panels = document.querySelectorAll('ytd-macro-markers-list-item-renderer');
        panels.forEach(panel => {
          const label = panel.querySelector('#title')?.textContent?.trim() || '';
          const timeEl = panel.querySelector('#time')?.textContent?.trim() || '';
          const [m, s] = timeEl.split(':').map(Number);
          if (!isNaN(m) && !isNaN(s)) {
            chapters.push({ label, start: m * 60 + s });
          }
        });
        return chapters;
      } catch { return []; }
    }

    function watchForSponsors() {
      const video = document.querySelector('video');
      if (!video) return;

      const chapters = getChapters();
      if (chapters.length === 0) return;

      video.addEventListener('timeupdate', function () {
        const ct = video.currentTime;
        for (let i = 0; i < chapters.length; i++) {
          const ch = chapters[i];
          const next = chapters[i + 1];
          const end = next ? next.start : video.duration;
          if (ct >= ch.start && ct < end && SPONSOR_KEYWORDS.test(ch.label)) {
            console.log('[YT-Premium] Skipping sponsor chapter:', ch.label);
            video.currentTime = end;
            showSkipToast(ch.label);
            break;
          }
        }
      }, { passive: true });
    }

    function showSkipToast(label) {
      const existing = document.getElementById('ytp-skip-toast');
      if (existing) existing.remove();
      const toast = document.createElement('div');
      toast.id = 'ytp-skip-toast';
      toast.textContent = `⏭ Skipped: ${label}`;
      Object.assign(toast.style, {
        position: 'fixed', bottom: '80px', right: '24px',
        background: 'rgba(0,0,0,0.82)', color: '#fff',
        padding: '8px 16px', borderRadius: '4px',
        fontFamily: 'Roboto, sans-serif', fontSize: '13px',
        zIndex: 99999, pointerEvents: 'none',
        animation: 'ytpFadeIn .2s ease',
      });
      document.body.appendChild(toast);
      setTimeout(() => toast.remove(), 3000);
    }

    GM_addStyle(`
      @keyframes ytpFadeIn { from { opacity:0; transform:translateY(6px) } to { opacity:1; transform:none } }
    `);

    document.addEventListener('yt-navigate-finish', () => setTimeout(watchForSponsors, 2000));
  }

  // ─────────────────────────────────────────────
  //  SETTINGS PANEL  (press Alt+P to open)
  // ─────────────────────────────────────────────
  GM_addStyle(`
    #ytpc-panel {
      position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%);
      background: #0f0f0f; color: #fff; border: 1px solid #333;
      border-radius: 12px; padding: 24px 28px; width: 340px;
      font-family: Roboto, sans-serif; font-size: 14px;
      z-index: 999999; box-shadow: 0 8px 40px rgba(0,0,0,.7);
      display: none;
    }
    #ytpc-panel.visible { display: block; }
    #ytpc-panel h2 {
      margin: 0 0 18px; font-size: 16px; font-weight: 600;
      display: flex; align-items: center; gap: 8px;
    }
    #ytpc-panel h2 span { color: #ff0000; }
    .ytpc-row {
      display: flex; justify-content: space-between; align-items: center;
      padding: 9px 0; border-bottom: 1px solid #222;
    }
    .ytpc-row:last-of-type { border-bottom: none; }
    .ytpc-label { line-height: 1.3; }
    .ytpc-label small { color: #aaa; font-size: 11px; display: block; }

    /* Toggle switch */
    .ytpc-toggle { position: relative; width: 40px; height: 22px; flex-shrink: 0; }
    .ytpc-toggle input { opacity: 0; width: 0; height: 0; }
    .ytpc-slider {
      position: absolute; cursor: pointer; inset: 0;
      background: #333; border-radius: 22px; transition: .25s;
    }
    .ytpc-slider:before {
      content: ''; position: absolute;
      height: 16px; width: 16px; left: 3px; top: 3px;
      background: #fff; border-radius: 50%; transition: .25s;
    }
    .ytpc-toggle input:checked + .ytpc-slider { background: #ff0000; }
    .ytpc-toggle input:checked + .ytpc-slider:before { transform: translateX(18px); }

    #ytpc-close {
      margin-top: 18px; width: 100%; padding: 9px;
      background: #222; color: #fff; border: none;
      border-radius: 6px; cursor: pointer; font-size: 13px;
    }
    #ytpc-close:hover { background: #333; }
    #ytpc-hint {
      position: fixed; bottom: 12px; right: 16px;
      background: rgba(0,0,0,.6); color: #aaa;
      font-family: Roboto, sans-serif; font-size: 11px;
      padding: 4px 10px; border-radius: 4px; z-index: 99990;
      pointer-events: none;
    }
  `);

  const FEATURES = [
    { key: 'blockAds',          label: 'Block Ads',            desc: 'Remove video & banner ads'          },
    { key: 'skipSponsors',      label: 'Skip Sponsor Chapters', desc: 'Auto-skip labelled sponsor segments' },
    { key: 'autoMaxQuality',    label: 'Max Quality',          desc: 'Always pick highest available'       },
    { key: 'hideUpsells',       label: 'Hide Upsells',         desc: 'Remove Premium promo banners'        },
    { key: 'persistVolume',     label: 'Persist Volume',       desc: 'Remember volume across videos'       },
    { key: 'autoSkipEndscreen', label: 'Skip End Screen',      desc: 'Skip last 20 s outro'                },
    { key: 'autoTheaterMode',   label: 'Theater Mode',         desc: 'Auto-enable theater on watch pages'  },
  ];

  function buildPanel() {
    if (document.getElementById('ytpc-panel')) return;

    const panel = document.createElement('div');
    panel.id = 'ytpc-panel';
    panel.innerHTML = `<h2>▶ <span>YT</span> Premium Client</h2>`;

    FEATURES.forEach(({ key, label, desc }) => {
      const row = document.createElement('div');
      row.className = 'ytpc-row';
      row.innerHTML = `
        <div class="ytpc-label">${label}<small>${desc}</small></div>
        <label class="ytpc-toggle">
          <input type="checkbox" data-key="${key}" ${CFG[key] ? 'checked' : ''}>
          <span class="ytpc-slider"></span>
        </label>`;
      panel.appendChild(row);
    });

    const closeBtn = document.createElement('button');
    closeBtn.id = 'ytpc-close';
    closeBtn.textContent = 'Close  (Alt+G)';
    closeBtn.onclick = () => panel.classList.remove('visible');
    panel.appendChild(closeBtn);

    panel.querySelectorAll('input[data-key]').forEach(input => {
      input.addEventListener('change', () => {
        const k = input.dataset.key;
        CFG[k] = input.checked;
        GM_setValue(k, input.checked);
      });
    });

    // Close on outside click
    document.addEventListener('mousedown', e => {
      if (!panel.contains(e.target)) panel.classList.remove('visible');
    });

    document.body.appendChild(panel);

    // Keyboard hint
    const hint = document.createElement('div');
    hint.id = 'ytpc-hint';
    hint.textContent = 'Alt+G  YT Premium';
    document.body.appendChild(hint);
    setTimeout(() => hint.remove(), 5000);
  }

  document.addEventListener('keydown', e => {
    if (e.altKey && e.key === 'g') {
      buildPanel();
      document.getElementById('ytpc-panel').classList.toggle('visible');
    }
  });

  // Build panel DOM once page is ready
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', buildPanel);
  } else {
    buildPanel();
  }

})();