Chzzk Auto Refresh

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

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         Chzzk Auto Refresh
// @namespace    http://tampermonkey.net/
// @version      3.4
// @description  방송 중임에도 영상이 10초 이상 멈춰있거나(리방 오류 등), 새로 시작될 때 알림을 띄웁니다.
// @author       떱_
// @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;
    const COOLDOWN_TIME = 120000;
    const AUTO_REFRESH_SECONDS = 5;
    const STUCK_THRESHOLD = 4; // 약 12초

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

    let worker = null;

    // 페이지 로드 후 5초 대기
    setTimeout(() => {
        isPageLoaded = true;
        currentChannelId = getChannelId(); // 초기 채널 ID 세팅
        startWorkerEngine();
    }, 5000);

    // --- Web Worker 엔진 시동 (백그라운드 생존용) ---
    function startWorkerEngine() {
        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.postMessage('start');
        console.log("🟢 [Auto Refresh] Web Worker 모드 시작 (Tampermonkey 최적화)");
    }

    // --- 유틸리티 ---
    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;
    }

    function isVideoPlaying() {
        const video = document.querySelector('video');
        if (!video) return false;
        return !video.paused && video.readyState > 2 && video.currentTime > 0;
    }

    // 초강력 새로고침 (Form Submit 방식)
    function forceReload() {
        console.warn("🔄 강제 새로고침(하드 리프레시)을 실행합니다.");
        const url = new URL(window.location.href);
        url.searchParams.set('_t', Date.now());

        const form = document.createElement('form');
        form.method = 'GET';
        form.action = url.toString();
        document.body.appendChild(form);
        form.submit();
    }

    function clearCustomModal() {
        const existingModal = document.getElementById('czk_custom_modal');
        if (existingModal) existingModal.remove();
    }

    // --- 커스텀 알림창 ---
    function showCustomModal(reason) {
        clearCustomModal();

        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; font-size: 16px;
        `;
        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.id = 'czk_custom_modal';
        modal.style.cssText = modalStyle;
        modal.innerHTML = `
            <h2 style="margin: 0 0 10px; font-size: 20px; color: #00ffa3;">📢 방송 상태 확인</h2>
            <p style="margin: 5px 0; 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(); // Web Worker 중단
            modal.remove();
            alert("자동 감지가 취소되었습니다. 다시 켜려면 페이지를 수동으로 새로고침하세요.");
        };
    }

    // --- 메인 감지 로직 ---
    async function checkLiveStatus() {
        if (!isValidLiveUrl()) return;

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

        // 채널 이동 감지 로직 (SPA 라우팅 오탐 방지)
        if (currentChannelId && currentChannelId !== channelId) {
            console.log(`🔄 [Auto Refresh] 채널 이동 감지됨 (${currentChannelId} -> ${channelId}). 스크립트 상태 리셋.`);

            currentChannelId = channelId;
            hasAlerted = false;
            lastPlayingTime = 0;
            cooldownUntil = 0;
            consecutiveStuckCount = 0;
            previousApiStatus = null;
            clearCustomModal();

            isPageLoaded = false;
            setTimeout(() => { isPageLoaded = true; }, 4000);
            return;
        }

        if (!isPageLoaded || hasAlerted) return;
        if (Date.now() < cooldownUntil) return;

        if (isVideoPlaying()) {
            lastPlayingTime = Date.now();
            consecutiveStuckCount = 0;
            previousApiStatus = 'OPEN';
            return;
        }

        if (lastPlayingTime > 0 && (Date.now() - lastPlayingTime < 30000)) {
            console.warn("🛑 방송 종료 감지. 2분 쿨다운.");
            cooldownUntil = Date.now() + COOLDOWN_TIME;
            lastPlayingTime = 0;
            previousApiStatus = 'CLOSE';
            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;

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

                consecutiveStuckCount++;
                if (consecutiveStuckCount >= STUCK_THRESHOLD) {
                    console.warn("🚨 [EVENT] 장시간 멈춤 감지");
                    hasAlerted = true;
                    showCustomModal("방송 중이나 영상 재생이 멈춰있습니다.");
                }

            } else {
                consecutiveStuckCount = 0;
            }

            previousApiStatus = currentStatus;

        } catch (error) {
            // 에러 무시
        }
    }

    // 탭 복귀 시 즉각 확인
    document.addEventListener("visibilitychange", () => {
        if (document.visibilityState === 'visible' && !hasAlerted) {
            checkLiveStatus();
        }
    });

    console.log("🟢 [Auto Refresh] v3.4 로드됨 (Tampermonkey 최적화 버전)");

})();