Chzzk Auto Quality & 광고 팝업 제거

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

目前為 2025-05-03 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Chzzk Auto Quality & 광고 팝업 제거
// @namespace    http://tampermonkey.net/
// @version      2.0
// @icon         https://play-lh.googleusercontent.com/wvo3IB5dTJHyjpIHvkdzpgbFnG3LoVsqKdQ7W3IoRm-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';

	// ----- 설정(JSON) -----
	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);
	const minTimeoutSec = (minTimeout / 1000);
	console.log(
		`%c⚠️ [Guide] timeout은 최소 ${minTimeoutSec}초 (${minTimeout}ms) 이상이어야 하며, ` +
		`이보다 작으면 자동 조정됩니다. 이 경우 재생이 멈추거나 품질 목록을 찾지 못할 수 있습니다.`,
		styles.warn
	);


	// ----- 팝업 제거 -----
	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) 미만이어서 ${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 chosen = li.textContent.trim();
			localStorage.setItem(storageKey, chosen);
			console.log(`%c💾 [Quality] 수동 화질 선택 저장: ${chosen}`, styles.success);
		}, {
			capture: true
		});
	}

	// ----- 저장된 선호 화질 불러오기 -----
	function getPreferredQuality() {
		const pref = localStorage.getItem(storageKey);
		if (pref) {
			console.log(`%c🔍 [Quality] 로컬 저장 화질 불러옴: ${pref}`, styles.info);
			return pref;
		}
		return '1080p';
	}

	// ----- 자동 화질 선택 -----
	async function selectPreferredQuality() {
		const target = getPreferredQuality();
		console.log(`%c⚙️ [Quality] '${target}' 자동 선택 시도`, styles.info);
		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 => i.textContent.includes(target));

			if (!pick) {
				const regex = /\d+p/;
				pick = items.find(i => regex.test(i.textContent));
			}

			if (!pick && items.length) {
				pick = items[0];
			}

			if (pick) {
				pick.dispatchEvent(new KeyboardEvent('keydown', {
					key: 'Enter'
				}));
				console.log(`%c✅ [Quality] '${pick.textContent.trim()}' 선택 완료`, styles.success);
			} else {
				console.warn(`%c⚠️ [Quality] 품질 목록을 찾지 못했습니다. 셀렉터나 timeout을 확인하세요.`, styles.warn);
			}
		} catch (e) {
			console.error(`%c❌ [Quality] 자동 선택 중 에러: ${e.message}`, styles.error);
		}
	}

	// ----- xhook 후크: P2P 제한 해제 & 화질 선택 -----
	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);
		}
	});

	// ----- 방송 ID 추출 및 비교 -----
	let lastVideoId = null;

	function getVideoIdFromUrl(url) {
		const match = url.match(/live\/([\w-]+)/);
		return match ? match[1] : null;
	}

	// ----- SPA URL 변경 감지 -----
	(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;

				// 방송 ID가 있을 경우에만 비교
				if (newVideoId) {
					if (newVideoId !== lastVideoId) {
						lastVideoId = newVideoId;
						setTimeout(selectPreferredQuality, minTimeout);
					} else {
						console.log(`%c⏩ [URLChange] 같은 방송(${newVideoId}), 품질 재설정 생략`, styles.warn);
					}
				} else {
					console.log(`%cℹ️ [URLChange] 방송 ID 없음(${location.href}), 품질 설정 건너뜀`, 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 observer;

	function startObserver() {
		if (observer) observer.disconnect();
		observer = new MutationObserver(handleAdBlockPopup);
		observer.observe(document.body, {
			childList: true,
			subtree: true
		});
		console.log(`%c🔍 [Observer] 팝업 감시 시작`, styles.bold);
	}

	// ----- 초기화 -----
	observeManualQualitySelect();
	startObserver();

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址