Greasy Fork

Greasy Fork is available in English.

Chzzk Auto Quality & 광고 팝업 제거

Chzzk 자동 선호 화질 설정, 광고 팝업 제거 및 스크롤 잠금 해제

当前为 2025-05-04 提交的版本,查看 最新版本

// ==UserScript==
// @name         Chzzk Auto Quality & 광고 팝업 제거
// @version      3.0
// @description  Chzzk 자동 선호 화질 설정, 광고 팝업 제거 및 스크롤 잠금 해제
// @include      https://chzzk.naver.com/*
// @icon         https://chzzk.naver.com/favicon.ico
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        unsafeWindow
// @run-at       document-start
// @license      MIT
// @namespace http://tampermonkey.net/
// ==/UserScript==

(async () => {
  'use strict';

  const CONFIG = {
    styles: { success:'font-weight:bold; color:green', error:'font-weight:bold; color:red',
              info:'font-weight:bold; color:skyblue', warn:'font-weight:bold; color:orange' },
    minTimeout:500, defaultTimeout:2000,
    storageKey:'chzzkPreferredQuality',
    selectors:{
      popup:'div[class^="popup_container"]',
      qualityBtn:'button[class*="pzp-pc-setting-button"]',
      qualityMenu:'div[class*="pzp-pc-setting-intro-quality"]',
      qualityItems:'li[class*="quality-item"], li[class*="quality"]'
    }
  };

  const log = {
    info:    msg => console.log(`%c${msg}`, CONFIG.styles.info),
    success: msg => console.log(`%c${msg}`, CONFIG.styles.success),
    warn:    msg => console.warn(`%c${msg}`, CONFIG.styles.warn),
    error:   msg => console.error(`%c${msg}`, CONFIG.styles.error)
  };

  const utils = {
    sleep: ms => new Promise(r => setTimeout(r, ms)),
    waitFor: (sel, to=CONFIG.defaultTimeout) => {
      const max = Math.max(to, CONFIG.minTimeout);
      return new Promise((res, rej) => {
        const e = document.querySelector(sel);
        if (e) return res(e);
        const mo = new MutationObserver(() => {
          const f = document.querySelector(sel);
          if (f) { mo.disconnect(); res(f); }
        });
        mo.observe(document.body, { childList:true, subtree:true });
        setTimeout(() => { mo.disconnect(); rej(new Error('Timeout')); }, max);
      });
    },
    extractResolution: t => { const m = t.match(/(\d{3,4})p/); return m?+m[1]:null; },
    cleanText: r => r.trim().split(/\s+/).filter(Boolean).join(', ')
  };

  const quality = {
    observeManualSelect() {
      document.body.addEventListener('click', async e => {
        const li = e.target.closest('li[class*="quality"]');
        if (!li) return;
        const raw = li.textContent;
        const res = utils.extractResolution(raw);
        if (res) {
          await GM.setValue(CONFIG.storageKey, res);
          console.groupCollapsed('%c💾 [Quality] 수동 화질 저장됨', CONFIG.styles.success);
          console.table([{ '선택 해상도':res, '원본':utils.cleanText(raw) }]);
          console.groupEnd();
        }
      }, { capture:true });
    },
    async getPreferred() {
      const stored = await GM.getValue(CONFIG.storageKey, 1080);
      return parseInt(stored,10);
    },
    async applyPreferred() {
      const target = await this.getPreferred();
      console.groupCollapsed('%c⚙️ [Quality] 자동 화질 선택 시작', CONFIG.styles.info);
      console.table([{ '대상 해상도':target }]);
      try {
        (await utils.waitFor(CONFIG.selectors.qualityBtn)).click();
        (await utils.waitFor(CONFIG.selectors.qualityMenu)).click();
        await utils.sleep(CONFIG.minTimeout);
        const items = Array.from(document.querySelectorAll(CONFIG.selectors.qualityItems));
        const pick = items.find(i=>utils.extractResolution(i.textContent)===target)
                   || items.find(i=>/\d+p/.test(i.textContent))
                   || items[0];
        if (pick) {
          pick.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter'}));
          console.table([{ '선택 화질':utils.cleanText(pick.textContent) }]);
        } else {
          log.warn('[Quality] 화질 항목을 찾지 못함');
        }
      } catch (e) {
        log.error(`[Quality] 선택 실패: ${e.message}`);
      }
      console.groupEnd();
    }
  };

  const handler = {
    removeAdPopup() {
      const pop = document.querySelector(CONFIG.selectors.popup);
      if (pop?.textContent.includes('광고 차단 프로그램을 사용 중이신가요')) {
        pop.remove();
        document.body.removeAttribute('style');
        log.success('[AdPopup] 팝업 제거됨');
      }
    },
    interceptXHR() {
      const oO = XMLHttpRequest.prototype.open;
      const oS = XMLHttpRequest.prototype.send;
      XMLHttpRequest.prototype.open = function(m,u,...a){ this._url=u; return oO.call(this,m,u,...a); };
      XMLHttpRequest.prototype.send = function(b){
        if (this._url?.includes('live-detail')) {
          this.addEventListener('readystatechange', ()=>{
            if (this.readyState===4 && this.status===200){
              try {
                const data = JSON.parse(this.responseText);
                if (data.content?.p2pQuality) {
                  data.content.p2pQuality=[];
                  const mod = JSON.stringify(data);
                  Object.defineProperty(this,'responseText',{value:mod});
                  Object.defineProperty(this,'response',{value:mod});
                  setTimeout(()=>quality.applyPreferred(),CONFIG.minTimeout);
                }
              } catch(e) {
                log.error(`[XHR] JSON 파싱 오류: ${e.message}`);
              }
            }
          });
        }
        return oS.call(this,b);
      };
      log.info('[XHR] live-detail 요청 감시 시작');
    },
    trackURLChange() {
      let last=location.href, lastId=null;
      const getId=url=>(url.match(/live\/([\w-]+)/)??[])[1]||null;
      const onChange=()=>{
        if(location.href===last) return;
        log.info(`[URLChange] ${last} → ${location.href}`);
        last=location.href;
        const id=getId(location.href);
        if (!id) return log.info('[URLChange] 방송 ID 없음, 설정 생략');
        if (id!==lastId) {
          lastId=id;
          setTimeout(()=>quality.applyPreferred(),CONFIG.minTimeout);
        } else {
          log.warn(`[URLChange] 같은 방송(${id}), 재설정 생략`);
        }
      };
      ['pushState','replaceState'].forEach(m=>{
        const orig=history[m];
        history[m]=function(){ const r=orig.apply(this,arguments); onChange(); return r; };
      });
      window.addEventListener('popstate',onChange);
    }
  };

  const observer = {
    startAdPopupWatcher() {
      new MutationObserver(()=>handler.removeAdPopup())
        .observe(document.body,{childList:true,subtree:true});
      log.info('[Observer] 광고 팝업 감시 시작');
    },
    startBodyStyleWatcher() {
      const clean=()=>{
        if(document.body.style.overflow==='hidden'){
          document.body.removeAttribute('style');
          log.info('[BodyStyle] overflow:hidden 제거됨');
        }
      };
      clean();
      new MutationObserver(clean)
        .observe(document.body,{attributes:true,attributeFilter:['style']});
      log.info('[Observer] body.style 감시 시작');
    }
  };

  async function runStartup() {
    handler.removeAdPopup();
    log.success('[Startup] 팝업 제거 시도');
    if (document.body.style.overflow==='hidden') {
      document.body.removeAttribute('style');
      log.success('[Startup] 초기 overflow 제거');
    }
    log.info('[Startup] 자동 화질 설정 준비');
    const stored = await GM.getValue(CONFIG.storageKey);
    if (stored===undefined) {
      await GM.setValue(CONFIG.storageKey,1080);
      log.success('[Startup] 기본 화질 1080p 저장됨');
    }
    await quality.applyPreferred();
  }

  handler.interceptXHR();
  handler.trackURLChange();

  const onDomReady = () => {
    console.log('%c🔔 [ChzzkHelper] 스크립트 시작', CONFIG.styles.info);
    quality.observeManualSelect();
    observer.startAdPopupWatcher();
    if (location.pathname === '/lives') observer.startBodyStyleWatcher();
    runStartup();
  };
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', onDomReady);
  } else {
    onDomReady();
  }
})();