Chzzk Auto Refresh

방송 중임에도 영상이 10초 이상 멈춰있거나(리방 오류 등), 새로 시작될 때 알림을 띄웁니다.

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

You will need to install an extension such as Tampermonkey to install this script.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==UserScript==
// @name         Chzzk Auto Refresh
// @namespace    http://tampermonkey.net/
// @version      3.2
// @description  방송 중임에도 영상이 10초 이상 멈춰있거나(리방 오류 등), 새로 시작될 때 알림을 띄웁니다.
// @author       You
// @match        https://chzzk.naver.com/live/*
// @icon         https://ssl.pstatic.net/static/nng/glive/icon/favicon.png
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // [설정 구역]
    const CHECK_INTERVAL = 3000;      // 3초마다 상태 확인
    const COOLDOWN_TIME = 120000;     // 방송 종료 후 2분간 알림 금지 (쿨다운)
    const AUTO_REFRESH_SECONDS = 5;   // 알림창 뜬 후 자동 새로고침까지 대기 시간

    // [중요] 3초 간격 x 4회 = 약 12초 동안 영상이 멈춰있으면 알림 발생
    // 리방 후 로딩이 꼬여서 10초 이상 멍하니 있는 경우를 잡기 위함입니다.
    const STUCK_THRESHOLD = 4;

    let isPageLoaded = false;
    let hasAlerted = false;
    let lastPlayingTime = 0;
    let cooldownUntil = 0;
    let consecutiveStuckCount = 0;
    let previousApiStatus = null;
    let worker = null;
    let fallbackIntervalId = null;

    // 페이지 로드 후 5초 대기 (안정화)
    setTimeout(() => {
        isPageLoaded = true;
        tryStartEngine();
    }, 5000);

    // --- 엔진 시동 (AdGuard/Tampermonkey 호환) ---
    function tryStartEngine() {
        try {
            const workerScript = `
                self.onmessage = function(e) {
                    if (e.data === 'start') {
                        setInterval(function() {
                            self.postMessage('tick');
                        }, ${CHECK_INTERVAL});
                    }
                };
            `;
            const blob = new Blob([workerScript], { type: 'application/javascript' });
            worker = new Worker(URL.createObjectURL(blob));

            worker.onmessage = function(e) {
                if (e.data === 'tick') checkLiveStatus();
            };

            worker.onerror = function() {
                console.warn("⚠️ [Auto Refresh] Worker 에러. 타이머 전환.");
                startFallbackTimer();
            };

            worker.postMessage('start');
            console.log("🟢 [Auto Refresh] Web Worker 모드로 감시 시작");

        } catch (error) {
            console.warn("⚠️ [Auto Refresh] Worker 차단됨. 일반 타이머 사용.");
            startFallbackTimer();
        }
    }

    function startFallbackTimer() {
        if (fallbackIntervalId) clearInterval(fallbackIntervalId);
        if (worker) { worker.terminate(); worker = null; }
        fallbackIntervalId = setInterval(checkLiveStatus, CHECK_INTERVAL);
    }

    // --- 유틸리티 ---
    function isValidLiveUrl() {
        return /^\/live\/[^/]+$/.test(window.location.pathname);
    }

    function getChannelId() {
        const path = window.location.pathname.split('/');
        const liveIndex = path.indexOf('live');
        if (liveIndex !== -1 && path[liveIndex + 1]) return path[liveIndex + 1];
        return null;
    }

    // 영상 재생 여부 판별 (재생 중이면 true, 멈춤/오류면 false)
    function isVideoPlaying() {
        const video = document.querySelector('video');
        if (!video) return false;
        // paused가 아니고, 데이터가 충분하며, 시간이 흐르고 있어야 함
        return !video.paused && video.readyState > 2 && video.currentTime > 0;
    }

    function forceReload() {
        const currentUrl = new URL(window.location.href);
        currentUrl.searchParams.set('refresh', Date.now());
        window.location.href = currentUrl.toString();
    }

    // --- 커스텀 알림창 ---
    function showCustomModal(reason) {
        const modalStyle = `
            position: fixed; top: 20%; left: 50%; transform: translate(-50%, -50%);
            background: #1e1e1e; color: white; padding: 25px; border-radius: 12px;
            box-shadow: 0 10px 30px rgba(0,0,0,0.7); z-index: 999999;
            text-align: center; font-family: 'Pretendard', sans-serif; min-width: 350px;
            border: 1px solid #444;
        `;
        const btnBaseStyle = `
            padding: 10px 20px; border: none; border-radius: 6px; cursor: pointer;
            font-weight: bold; margin: 0 5px; font-size: 14px;
        `;

        const modal = document.createElement('div');
        modal.style.cssText = modalStyle;
        modal.innerHTML = `
            <h2 style="margin: 0 0 10px; font-size: 20px; color: #00ffa3;">📢 방송 상태 확인</h2>
            <p style="margin: 5px 0; font-size: 15px; font-weight: bold;">${reason}</p>
            <p style="margin: 5px 0; font-size: 13px; color: #ccc;">오류 해결을 위해 새로고침이 필요할 수 있습니다.</p>
            <p id="czk_timer_msg" style="margin: 15px 0; font-size: 14px; color: #ffcc00;">${AUTO_REFRESH_SECONDS}초 뒤 자동으로 새로고침됩니다.</p>
            <div style="margin-top: 20px;">
                <button id="czk_refresh_btn" style="${btnBaseStyle} background: #00ffa3; color: #000;">새로고침</button>
                <button id="czk_cancel_btn" style="${btnBaseStyle} background: #555; color: #fff;">취소</button>
            </div>
        `;
        document.body.appendChild(modal);

        let timeLeft = AUTO_REFRESH_SECONDS;
        const countdownInterval = setInterval(() => {
            timeLeft--;
            const msgEl = document.getElementById('czk_timer_msg');
            if (msgEl) msgEl.innerText = `${timeLeft}초 뒤 자동으로 새로고침됩니다.`;
            if (timeLeft <= 0) {
                clearInterval(countdownInterval);
                forceReload();
            }
        }, 1000);

        document.getElementById('czk_refresh_btn').onclick = () => {
            clearInterval(countdownInterval);
            forceReload();
        };

        document.getElementById('czk_cancel_btn').onclick = () => {
            clearInterval(countdownInterval);
            if (worker) worker.terminate();
            if (fallbackIntervalId) clearInterval(fallbackIntervalId);
            modal.remove();
            alert("자동 감지가 취소되었습니다. 다시 켜려면 페이지를 수동으로 새로고침하세요.");
        };
    }

    // --- 메인 감지 로직 ---
    async function checkLiveStatus() {
        if (!isValidLiveUrl()) return;
        if (!isPageLoaded || hasAlerted) return;
        if (Date.now() < cooldownUntil) return;

        // 1. 영상이 정상 재생 중인 경우
        if (isVideoPlaying()) {
            lastPlayingTime = Date.now();
            consecutiveStuckCount = 0; // 카운트 초기화
            previousApiStatus = 'OPEN';
            return;
        }

        // 2. 방송 종료 판단 (방금 전까지 보다가 끊긴 경우)
        if (lastPlayingTime > 0 && (Date.now() - lastPlayingTime < 30000)) {
            console.warn("🛑 방송 종료 감지. 2분 쿨다운.");
            cooldownUntil = Date.now() + COOLDOWN_TIME;
            lastPlayingTime = 0;
            previousApiStatus = 'CLOSE';
            return;
        }

        const channelId = getChannelId();
        if (!channelId) return;

        try {
            const response = await fetch(`https://api.chzzk.naver.com/polling/v2/channels/${channelId}/live-status`);
            const data = await response.json();
            const currentStatus = data.content?.status; // 'OPEN' or 'CLOSE'

            if (currentStatus === 'OPEN') {
                // A. 완전한 방송 시작 (CLOSE -> OPEN)
                if (previousApiStatus === 'CLOSE') {
                    console.warn("🚨 [EVENT] 방송 시작 감지 (즉시)");
                    hasAlerted = true;
                    document.title = "🔴 방송 시작!!";
                    showCustomModal("방송이 시작되었습니다!");
                    return;
                }

                // B. 방송 중인데 영상이 안 나오는 경우 (리방 오류, 로딩 지연 등)
                consecutiveStuckCount++;
                console.log(`⚠️ 방송 중 / 영상 멈춤 감지 중... (${consecutiveStuckCount}/${STUCK_THRESHOLD})`);

                // 4회 연속(약 12초) 멈춰있으면 알림
                if (consecutiveStuckCount >= STUCK_THRESHOLD) {
                    console.warn("🚨 [EVENT] 장시간 멈춤(리방 오류 등) 감지");
                    hasAlerted = true;
                    showCustomModal("방송 중이나 영상 재생이 멈춰있습니다.");
                }

            } else {
                // 방송이 꺼져있으면 카운트 초기화
                consecutiveStuckCount = 0;
            }

            previousApiStatus = currentStatus;

        } catch (error) {
            console.error("❌ 에러:", error);
        }
    }

    document.addEventListener("visibilitychange", () => {
        if (document.visibilityState === 'visible' && !hasAlerted) {
            checkLiveStatus();
        }
    });

    console.log("🟢 [Auto Refresh] v3.2 로드됨 (리방 멈춤 해결)");

})();