您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Chzzk 방송에서 자동 화질 설정, 광고 팝업 차단, 음소거 자동 해제, 스크롤 잠금 해제
当前为
// ==UserScript== // @name Chzzk 올인원 스크립트 (Auto Quality + Ad Popup Removal + Unmute) // @namespace http://tampermonkey.net/ // @version 3.6 // @description Chzzk 방송에서 자동 화질 설정, 광고 팝업 차단, 음소거 자동 해제, 스크롤 잠금 해제 // @match 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 // ==/UserScript== ;(function(){ const originalRemoveChild = Node.prototype.removeChild; Node.prototype.removeChild = function(child) { if (!child || child.parentNode !== this) return child; return originalRemoveChild.call(this, child); }; })(); (async () => { 'use strict'; const APPLY_COOLDOWN = 1000; const CONFIG = { minTimeout: 500, defaultTimeout: 2000, storageKeys: { quality: 'chzzkPreferredQuality', autoUnmute: 'chzzkAutoUnmute', debugLog: 'chzzkDebugLog', screenSharpness: 'chzzkScreenSharp' }, 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"]', headerMenu: '.header_service__DyG7M' }, 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' } }; const common = { regex: { adBlockDetect: /광고\s*차단\s*프로그램.*사용\s*중/i }, async: { sleep: ms => new Promise(r => setTimeout(r, ms)), waitFor: (selector, timeout = CONFIG.defaultTimeout) => { const effective = Math.max(timeout, CONFIG.minTimeout); 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 waiting for ' + selector)); }, effective); }); } }, text: { clean: txt => txt.trim().split(/\s+/).filter(Boolean).join(', '), extractResolution: txt => { const m = txt.match(/(\d{3,4})p/); return m ? parseInt(m[1], 10) : null; } }, dom: { remove: el => el?.remove(), clearStyle: el => el?.removeAttribute('style') }, log: { DEBUG: true, info: (...args) => common.log.DEBUG && console.log(...args), success: (...args) => common.log.DEBUG && console.log(...args), warn: (...args) => common.log.DEBUG && console.warn(...args), error: (...args) => common.log.DEBUG && console.error(...args), groupCollapsed: (...args) => common.log.DEBUG && console.groupCollapsed(...args), table: (...args) => common.log.DEBUG && console.table(...args), groupEnd: (...args) => common.log.DEBUG && console.groupEnd(...args), }, observeElement: (selector, callback, once = true) => { const mo = new MutationObserver(() => { const el = document.querySelector(selector); if (el) callback(el); if (once) mo.disconnect(); }); mo.observe(document.body, { childList: true, subtree: true }); const initial = document.querySelector(selector); if (initial) { callback(initial); if (once) mo.disconnect(); } } }; const TOGGLE_CLASS = 'chzzk-helper-toggle'; async function addHeaderMenu() { const header = await common.async.waitFor(CONFIG.selectors.headerMenu); if (header.querySelector(`.${TOGGLE_CLASS}`)) return; const separator = document.createElement('div'); separator.classList.add(TOGGLE_CLASS); separator.style.cssText = 'width:100%; height:1px; margin:4px 0; background-color:currentColor; opacity:0.2;'; header.appendChild(separator); const items = header.querySelectorAll('a.header_item__MFv39'); if (!items.length) return; const template = items[items.length - 1]; const debugItem = template.cloneNode(true); debugItem.classList.add(TOGGLE_CLASS); debugItem.removeAttribute('aria-current'); const debugSvg = ` <svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="header_icon__8SHkt"> <path stroke-linecap="round" stroke-linejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 0 1 .865-.501 48.172 48.172 0 0 0 3.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0 0 12 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018Z"/> </svg>`; debugItem.querySelector('svg').outerHTML = debugSvg; const debugState = common.log.DEBUG; debugItem.href = '#'; debugItem.querySelector('.header_text__SNWKj').textContent = `디버그 로그 ${debugState ? 'ON' : 'OFF'}`; debugItem.addEventListener('click', async e => { e.preventDefault(); const newState = !await GM.getValue(CONFIG.storageKeys.debugLog, false); await GM.setValue(CONFIG.storageKeys.debugLog, newState); alert(`디버그 로그: ${newState ? 'ON' : 'OFF'}\n페이지를 새로고침합니다.`); location.reload(); }); header.appendChild(debugItem); const unmuteItem = template.cloneNode(true); unmuteItem.classList.add(TOGGLE_CLASS); unmuteItem.removeAttribute('aria-current'); const unmuteState = await GM.getValue(CONFIG.storageKeys.autoUnmute, false); const unmuteSvgOff = ` <svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="header_icon__8SHkt"> <path stroke-linecap="round" stroke-linejoin="round" d="M17.25 9.75 19.5 12m0 0 2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6 4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z"/> </svg>`; const unmuteSvgOn = ` <svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="header_icon__8SHkt"> <path stroke-linecap="round" stroke-linejoin="round" d="M19.114 5.636a9 9 0 0 1 0 12.728M16.463 8.288a5.25 5.25 0 0 1 0 7.424M6.75 8.25l4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z"/> </svg>`; unmuteItem.querySelector('svg').outerHTML = unmuteState ? unmuteSvgOn : unmuteSvgOff; unmuteItem.href = '#'; unmuteItem.querySelector('.header_text__SNWKj').textContent = `음소거 해제 ${unmuteState ? 'ON' : 'OFF'}`; unmuteItem.addEventListener('click', async e => { e.preventDefault(); const newState = !await GM.getValue(CONFIG.storageKeys.autoUnmute, false); await GM.setValue(CONFIG.storageKeys.autoUnmute, newState); alert(`음소거 자동 해제: ${newState ? 'ON' : 'OFF'}\n페이지를 새로고침합니다.`); location.reload(); }); header.appendChild(unmuteItem); const sharpItem = template.cloneNode(true); sharpItem.classList.add(TOGGLE_CLASS); sharpItem.removeAttribute('aria-current'); const sharpState = await GM.getValue(CONFIG.storageKeys.screenSharpness, false); const sharpSvg = ` <svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="header_icon__8SHkt"> <path stroke-linecap="round" stroke-linejoin="round" d="M6 20.25h12m-7.5-3v3m3-3v3m-10.125-3h17.25c.621 0 1.125-.504 1.125-1.125V4.875c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125Z" /> </svg>`; sharpItem.querySelector('svg').outerHTML = sharpSvg; sharpItem.href = '#'; sharpItem.querySelector('.header_text__SNWKj').textContent = `선명한 화면 ${sharpState ? 'ON' : 'OFF'}`; sharpItem.addEventListener('click', async e => { e.preventDefault(); const newState = !await GM.getValue(CONFIG.storageKeys.screenSharpness, false); await GM.setValue(CONFIG.storageKeys.screenSharpness, newState); alert(`선명한 화면: ${newState ? 'ON' : 'OFF'}\n페이지를 새로고침합니다.`); location.reload(); }); header.appendChild(sharpItem); } 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 = common.text.extractResolution(raw); if (res) { await GM.setValue(CONFIG.storageKeys.quality, res); common.log.groupCollapsed('%c💾 [Quality] 수동 화질 저장됨', CONFIG.styles.success); common.log.table([{ '선택 해상도': res, '원본': common.text.clean(raw) }]); common.log.groupEnd(); } }, { capture: true }); }, async getPreferred() { const stored = await GM.getValue(CONFIG.storageKeys.quality, 1080); return parseInt(stored, 10); }, async applyPreferred() { const now = Date.now(); if (this._applying || now - this._lastApply < APPLY_COOLDOWN) return; this._applying = true; this._lastApply = now; const target = await this.getPreferred(); let cleaned = '(선택 실패)', pick = null; try { const btn = await common.async.waitFor(CONFIG.selectors.qualityBtn); btn.click(); const menu = await common.async.waitFor(CONFIG.selectors.qualityMenu); menu.click(); await common.async.sleep(CONFIG.minTimeout); const items = Array.from(document.querySelectorAll(CONFIG.selectors.qualityItems)); pick = items.find(i => common.text.extractResolution(i.textContent) === target) || items.find(i => /\d+p/.test(i.textContent)) || items[0]; cleaned = pick ? common.text.clean(pick.textContent) : cleaned; if (pick) pick.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); else common.log.warn('[Quality] 화질 항목을 찾지 못함'); } catch (e) { common.log.error(`[Quality] 선택 실패: ${e.message}`); } common.log.groupCollapsed('%c⚙️ [Quality] 자동 화질 적용', CONFIG.styles.info); common.log.table([{ '대상 해상도': target }]); common.log.table([{ '선택 화질': cleaned, '선택 방식': pick ? '자동' : '없음' }]); common.log.groupEnd(); this._applying = false; } }; const handler = { interceptXHR() { const oOpen = XMLHttpRequest.prototype.open; const oSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(m, u, ...a) { this._url = u; return oOpen.call(this, m, u, ...a); }; XMLHttpRequest.prototype.send = function(body) { 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) { common.log.error(`[XHR] JSON 파싱 오류: ${e.message}`); } } }); } return oSend.call(this, body); }; common.log.info('[XHR] live-detail 요청 감시 시작'); }, trackURLChange () { let lastUrl = location.href, lastId = null; const getId = url => (url.match(/live\/([\w-]+)/) ?? [])[1] || null; const onChange = () => { if (location.href === lastUrl) return; common.log.info(`[URLChange] ${lastUrl} → ${location.href}`); lastUrl = location.href; const id = getId(location.href); if (!id) return common.log.info('[URLChange] 방송 ID 없음'); if (id !== lastId) { lastId = id; setTimeout(() => { quality.applyPreferred(); injectSharpnessScript(); }, CONFIG.minTimeout); } else { common.log.warn(`[URLChange] 같은 방송(${id}), 스킵`); } }; ['pushState', 'replaceState'].forEach(m => { const orig = history[m]; history[m] = function() { const res = orig.apply(this, arguments); onChange(); return res; }; }); window.addEventListener('popstate', onChange); } }; const observer = { start() { const mo = new MutationObserver(muts => { for (const mut of muts) { for (const node of mut.addedNodes) { if (node.nodeType !== 1) continue; this.tryRemoveAdPopup(node); let vid = null; if (node.tagName === 'VIDEO') vid = node; else if (node.querySelector?.('video')) vid = node.querySelector('video'); if (vid) { this.unmuteAll(vid); checkAndFixLowQuality(vid); } } } if (document.body.style.overflow === 'hidden') { common.dom.clearStyle(document.body); common.log.info('[BodyStyle] overflow:hidden 제거됨'); } }); mo.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'] }); common.log.info('[Observer] 통합 감시 시작'); }, async unmuteAll(video) { const autoUnmute = await GM.getValue(CONFIG.storageKeys.autoUnmute, true); if (!autoUnmute) return common.log.info('[Unmute] 설정에 따라 스킵'); if (video.muted) { video.muted = false; common.log.success('[Unmute] video.muted 해제'); } const btn = document.querySelector('button.pzp-pc-volume-button[aria-label*="음소거 해제"]'); if (btn) { btn.click(); common.log.success('[Unmute] 버튼 클릭'); } }, async tryRemoveAdPopup(node) { try { const txt = node.innerText || ''; if (common.regex.adBlockDetect.test(txt)) { const cont = node.closest(CONFIG.selectors.popup) || node; cont.remove(); common.dom.clearStyle(document.body); common.log.groupCollapsed('%c✅ [AdPopup] 제거 성공', CONFIG.styles.success); common.log.table([{ '제거된 텍스트': txt.slice(0,100), '클래스': cont.className }]); common.log.groupEnd(); } } catch (e) { common.log.error(`[AdPopup] 제거 실패: ${e.message}`); } } }; async function checkAndFixLowQuality(video) { if (!video || video.__checkedAlready) return; video.__checkedAlready = true; await common.async.sleep(CONFIG.defaultTimeout); let height = video.videoHeight || 0; if (height === 0) { await common.async.sleep(1000); height = video.videoHeight || 0; } if (height === 0) { return; } if (height <= 360) { const preferred = await quality.getPreferred(); if (preferred !== height) { common.log.warn(`[QualityCheck] 저화질(${height}p) 감지, ${preferred}p로 복구`); await quality.applyPreferred(); } else { common.log.warn('[QualityCheck] 현재 해상도가 사용자 선호값과 동일하여 복구 생략'); } } } async function setDebugLogging() { common.log.DEBUG = await GM.getValue(CONFIG.storageKeys.debugLog, false); } async function injectSharpnessScript() { const enabled = await GM.getValue(CONFIG.storageKeys.screenSharpness, false); if (!enabled) return; const script = document.createElement('script'); script.src = 'https://update.greasyfork.org/scripts/534918/Chzzk%20%EC%84%A0%EB%AA%85%ED%95%9C%20%ED%99%94%EB%A9%B4%20%EC%97%85%EA%B7%B8%EB%A0%88%EC%9D%B4%EB%93%9C.user.js'; script.async = true; document.head.appendChild(script); common.log.success('%c[Sharpness] 외부 스크립트 삽입 완료', CONFIG.styles.info); } async function init() { await setDebugLogging(); if (document.body.style.overflow === 'hidden') { common.dom.clearStyle(document.body); common.log.success('[Init] overflow 잠금 해제'); } if ((await GM.getValue(CONFIG.storageKeys.quality)) === undefined) { await GM.setValue(CONFIG.storageKeys.quality, 1080); common.log.success('[Init] 기본 화질 1080 저장'); } if ((await GM.getValue(CONFIG.storageKeys.autoUnmute)) === undefined) { await GM.setValue(CONFIG.storageKeys.autoUnmute, true); common.log.success('[Init] 기본 언뮤트 ON 저장'); } await addHeaderMenu(); common.observeElement(CONFIG.selectors.headerMenu, () => { addHeaderMenu().catch(console.error); }, false); await quality.applyPreferred(); await injectSharpnessScript(); } function onDomReady() { console.log('%c🔔 [ChzzkHelper] 스크립트 시작', CONFIG.styles.info); quality.observeManualSelect(); observer.start(); init().catch(console.error); } handler.interceptXHR(); handler.trackURLChange(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', onDomReady); } else { onDomReady(); } })(); (function() { const skip = t => ['INPUT', 'TEXTAREA'].includes(t.tagName) || t.isContentEditable; const getBtn = () => document.querySelector('button[aria-label="넓은 화면"],button[aria-label="좁은 화면"]'); document.addEventListener('keydown', e => { if (skip(e.target) || e.ctrlKey || e.altKey || e.metaKey) return; const v = document.querySelector('video'); if (!v) return; const k = e.key.toLowerCase(); const actions = { ' ': () => v.paused ? v.play() : v.pause(), 'k': () => v.paused ? v.play() : v.pause(), 'm': () => v.muted = !v.muted, 't': () => { const b = getBtn(); b && b.click(); }, 'f': () => document.fullscreenElement ? document.exitFullscreen() : v.requestFullscreen && v.requestFullscreen() }; if (actions[k]) { actions[k](); e.preventDefault(); e.stopPropagation(); } }, true); })();