Chzzk Auto Refresh

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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 로드됨 (리방 멈춤 해결)");

})();