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