// ==UserScript== // @name Chzzk Auto Quality & 광고 팝업 제거 // @namespace http://tampermonkey.net/ // @version 2.1 // @icon https://play-lh.googleusercontent.com/wvo3IB5dTKr6EeffXNDX9kzYZyr5KsyfSB1v9GuZYx-EVzISMz9tTaIYoRdZm1phL_8 // @description Chzzk 자동 선호 화질 설정, 광고 팝업 제거 및 스크롤 잠금 해제 // @match https://chzzk.naver.com/* // @grant none // @require https://unpkg.com/xhook@latest/dist/xhook.min.js // @license MIT // @downloadURL none // ==/UserScript== (function () { 'use strict'; const CONFIG = { styles: { bold: 'font-weight:bold', 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 { styles, minTimeout, defaultTimeout, storageKey, selectors } = CONFIG; console.log(`%c🔔 [Chzzk] 스크립트 로드 완료`, styles.info); console.log(`%c⚠️ [Guide] 최소 timeout은 ${minTimeout}ms 이상이어야 합니다.`, styles.warn); // 공백 정리 유틸 function cleanQualityText(raw) { return raw .trim() .split(/\s+/) // 공백류로 분리 .filter(Boolean) // 빈 문자열 제거 .join(', '); // 쉼표로 조합 } // 광고 팝업 제거 function handleAdBlockPopup() { const popup = document.querySelector(selectors.popup); if (popup && popup.textContent.includes('광고 차단 프로그램을 사용 중이신가요')) { popup.remove(); document.body.removeAttribute('style'); console.log(`%c✅ [AdBlockPopup] 팝업 제거됨`, styles.success); } } // 요소 대기 function waitFor(selector, timeout = defaultTimeout) { const effective = Math.max(timeout, minTimeout); if (timeout < minTimeout) { console.warn(`%c⚠️ [waitFor] timeout이 ${minTimeout}ms로 보정됨`, styles.warn); } return new Promise((resolve, reject) => { const el = document.querySelector(selector); if (el) return resolve(el); const mo = new MutationObserver(() => { const found = document.querySelector(selector); if (found) { mo.disconnect(); resolve(found); } }); mo.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { mo.disconnect(); reject(new Error('Timeout')); }, effective); }); } // 수동 화질 선택 저장 function observeManualQualitySelect() { document.body.addEventListener('click', e => { const li = e.target.closest('li[class*="quality"]'); if (!li) return; const raw = li.textContent.trim(); const core = extractResolution(raw); if (core) { localStorage.setItem(storageKey, core); console.groupCollapsed('%c💾 [Quality] 수동 화질 선택 저장됨', styles.success); console.table([{ '선택한 해상도': core, '원본 텍스트': cleanQualityText(raw), '저장 키': storageKey }]); console.groupEnd(); } }, { capture: true }); } function extractResolution(text) { const match = text.match(/\d{3,4}p/); return match ? match[0] : null; } // 저장된 화질 불러오기 function getPreferredQuality() { const pref = localStorage.getItem(storageKey); if (pref) { console.groupCollapsed('%c🔍 [Quality] 저장된 선호 화질 불러오기', styles.info); console.table([{ '선호 화질': pref, '저장 위치': 'localStorage' }]); console.groupEnd(); return pref; } return '1080p'; } // 화질 자동 선택 async function selectPreferredQuality() { const target = getPreferredQuality(); console.groupCollapsed('%c⚙️ [Quality] 자동 화질 선택 시작', styles.info); console.table([{ '대상 화질': target }]); try { const btn = await waitFor(selectors.qualityBtn); btn.click(); const menu = await waitFor(selectors.qualityMenu); menu.click(); await new Promise(r => setTimeout(r, minTimeout)); const items = Array.from(document.querySelectorAll(selectors.qualityItems)); let pick = items.find(i => extractResolution(i.textContent) === target); if (!pick) pick = items.find(i => /\d+p/.test(i.textContent)); if (!pick && items.length) pick = items[0]; if (pick) { const cleaned = cleanQualityText(pick.textContent); pick.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); console.table([{ '선택된 화질': cleaned, '선택 방식': '자동 (Enter 이벤트)' }]); } else { console.warn(`%c⚠️ [Quality] 품질 항목을 찾지 못함`, styles.warn); } } catch (e) { console.error(`%c❌ [Quality] 자동 선택 중 에러: ${e.message}`, styles.error); } console.groupEnd(); } // xhook: 화질 제한 해제 + 자동 선택 트리거 xhook.after((req, res) => { if (req.url.includes('live-detail')) { try { const data = JSON.parse(res.text); if (data.content?.p2pQuality) { data.content.p2pQuality = []; Object.defineProperty(data.content, 'p2pQuality', { writable: false }); } res.text = JSON.stringify(data); } catch (err) { console.error(`%c❌ [xhook] JSON 처리 오류: ${err.message}`, styles.error); } setTimeout(selectPreferredQuality, minTimeout); } }); // URL 변경 감지 (SPA) (function watchUrlChange() { let lastUrl = location.href; let lastVideoId = null; function getVideoIdFromUrl(url) { const match = url.match(/live\/([\w-]+)/); return match ? match[1] : null; } const onChange = () => { if (location.href !== lastUrl) { console.log(`%c🔄 [URLChange] ${lastUrl} → ${location.href}`, styles.info); const newVideoId = getVideoIdFromUrl(location.href); lastUrl = location.href; if (newVideoId) { if (newVideoId !== lastVideoId) { lastVideoId = newVideoId; setTimeout(selectPreferredQuality, minTimeout); } else { console.log(`%c⏩ [URLChange] 같은 방송(${newVideoId}), 품질 재설정 생략`, styles.warn); } } else { console.log(`%cℹ️ [URLChange] 방송 ID 없음, 건너뜀`, styles.info); } } }; const _push = history.pushState; history.pushState = function () { _push.apply(this, arguments); onChange(); }; const _replace = history.replaceState; history.replaceState = function () { _replace.apply(this, arguments); onChange(); }; window.addEventListener('popstate', onChange); })(); // 광고 팝업 감시 let adPopupObserver; function startObserver() { if (adPopupObserver) adPopupObserver.disconnect(); adPopupObserver = new MutationObserver(handleAdBlockPopup); adPopupObserver.observe(document.body, { childList: true, subtree: true }); console.log(`%c🔍 [Observer] 광고 팝업 감시 시작`, styles.bold); } // body.style 감시 → 스크롤 잠금 해제 function observeBodyStyleChanges() { const observer = new MutationObserver(mutations => { for (const m of mutations) { if (m.type === 'attributes' && m.attributeName === 'style' && document.body.style.overflow === 'hidden') { document.body.removeAttribute('style'); console.log(`%c♻️ [BodyStyle] overflow:hidden 감지, style 제거`, styles.info); } } }); observer.observe(document.body, { attributes: true, attributeFilter: ['style'] }); console.log(`%c👀 [Observer] body.style 감시 시작`, styles.bold); } // 초기화 observeManualQualitySelect(); startObserver(); observeBodyStyleChanges(); })();