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