Twitch Auto Reload on Error #2000 (God Mode)

Reload Twitch stream if Error #2000 appears. Full feature set: GUI, retry counter, audio, notifications, logs, dark mode, draggable panel, and error history tracking.

当前为 2025-07-01 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Twitch Auto Reload on Error #2000 (God Mode)
// @namespace    http://tampermonkey.net/
// @version      5.0
// @description  Reload Twitch stream if Error #2000 appears. Full feature set: GUI, retry counter, audio, notifications, logs, dark mode, draggable panel, and error history tracking.
// @author       You
// @match        https://www.twitch.tv/*
// @grant        GM_registerMenuCommand
// @license MIT
// ==/UserScript==

(function () {
    'use strict';

    const SETTINGS_KEY = 'twitchErrorReloadSettings_v5';
    const LOGS_KEY = 'twitchErrorLogs_v5';

    const DEFAULT_SETTINGS = {
        maxRetries: 5,
        startDelayMs: 5000,
        reloadDelayMs: 2000,
        retryCount: 0,
        darkMode: true,
        enableAudio: true,
        enableNotify: true
    };

    let settings = loadSettings();
    let logs = loadLogs();

    function loadSettings() {
        const saved = localStorage.getItem(SETTINGS_KEY);
        return saved ? JSON.parse(saved) : { ...DEFAULT_SETTINGS };
    }

    function saveSettings() {
        localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
    }

    function loadLogs() {
        const saved = localStorage.getItem(LOGS_KEY);
        return saved ? JSON.parse(saved) : [];
    }

    function saveLogs() {
        localStorage.setItem(LOGS_KEY, JSON.stringify(logs));
    }

    function logEvent(msg) {
        const time = new Date().toLocaleTimeString();
        const entry = `[${time}] ${msg}`;
        logs.push(entry);
        if (logs.length > 50) logs.shift(); // Keep last 50
        saveLogs();
        updateLogPanel();
    }

    function playAlertSound() {
        if (!settings.enableAudio) return;
        const beep = new Audio("https://notificationsounds.com/storage/sounds/file-sounds-1153-pristine.mp3");
        beep.volume = 0.5;
        beep.play();
    }

    function showToast(message, success = true) {
        if (!settings.enableNotify) return;
        const toast = document.createElement('div');
        toast.innerText = message;
        toast.style.position = 'fixed';
        toast.style.bottom = '20px';
        toast.style.left = '50%';
        toast.style.transform = 'translateX(-50%)';
        toast.style.background = success ? '#28a745' : '#dc3545';
        toast.style.color = 'white';
        toast.style.padding = '10px 16px';
        toast.style.borderRadius = '5px';
        toast.style.zIndex = 9999;
        toast.style.boxShadow = '0 4px 10px rgba(0,0,0,0.3)';
        document.body.appendChild(toast);
        setTimeout(() => toast.remove(), 4000);
    }

    function updateLogPanel() {
        const panel = document.getElementById('log-panel');
        if (panel) {
            panel.innerHTML = '<strong>📜 Error History:</strong><br>' + logs.slice().reverse().map(l => `<div style='margin:2px 0'>${l}</div>`).join('');
        }
    }

    function createUI() {
        const container = document.createElement('div');
        container.id = 'twitch-reload-ui';
        container.style.position = 'fixed';
        container.style.top = '20px';
        container.style.right = '20px';
        container.style.zIndex = '9999';
        container.style.padding = '10px';
        container.style.borderRadius = '8px';
        container.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3)';
        container.style.fontFamily = 'Arial, sans-serif';
        container.style.width = '280px';
        container.style.maxWidth = '90vw';
        container.style.cursor = 'move';
        applyTheme(container);

        let offsetX = 0, offsetY = 0, isDragging = false;

        container.addEventListener('mousedown', function(e) {
            isDragging = true;
            offsetX = e.clientX - container.getBoundingClientRect().left;
            offsetY = e.clientY - container.getBoundingClientRect().top;
        });
        document.addEventListener('mouseup', () => isDragging = false);
        document.addEventListener('mousemove', function(e) {
            if (isDragging) {
                container.style.left = (e.clientX - offsetX) + 'px';
                container.style.top = (e.clientY - offsetY) + 'px';
                container.style.right = 'auto';
            }
        });

        const title = document.createElement('div');
        title.innerText = '⚙️ Twitch Auto Reload';
        title.style.fontWeight = 'bold';
        title.style.marginBottom = '10px';

        const counter = document.createElement('div');
        counter.id = 'retry-counter';
        counter.innerText = `Retries: ${settings.retryCount}/${settings.maxRetries}`;
        counter.style.marginBottom = '10px';

        function createSetting(labelText, type, settingKey, extra = {}) {
            const wrapper = document.createElement('div');
            wrapper.style.margin = '5px 0';

            const label = document.createElement('label');
            label.innerText = labelText;
            label.style.display = 'block';
            label.style.marginBottom = '2px';

            let input;
            if (type === 'checkbox') {
                input = document.createElement('input');
                input.type = 'checkbox';
                input.checked = settings[settingKey];
                input.onchange = () => {
                    settings[settingKey] = input.checked;
                    saveSettings();
                    if (settingKey === 'darkMode') applyTheme(container);
                };
            } else {
                input = document.createElement('input');
                input.type = 'number';
                input.value = settings[settingKey];
                input.min = extra.min || 0;
                input.style.width = '100%';
                input.onchange = () => {
                    settings[settingKey] = parseInt(input.value);
                    saveSettings();
                    updateRetryDisplay();
                };
            }

            wrapper.appendChild(label);
            wrapper.appendChild(input);
            return wrapper;
        }

        const retryBtn = document.createElement('button');
        retryBtn.innerText = '🔁 Retry Now';
        retryBtn.onclick = () => {
            showToast('Manual Reload', true);
            logEvent('Manual reload triggered.');
            location.reload();
        };

        const logPanel = document.createElement('div');
        logPanel.id = 'log-panel';
        logPanel.style.marginTop = '10px';
        logPanel.style.maxHeight = '120px';
        logPanel.style.overflowY = 'auto';
        logPanel.style.fontSize = '12px';

        container.appendChild(title);
        container.appendChild(counter);
        container.appendChild(createSetting('Max Retries', 'number', 'maxRetries'));
        container.appendChild(createSetting('Start Delay (ms)', 'number', 'startDelayMs'));
        container.appendChild(createSetting('Reload Delay (ms)', 'number', 'reloadDelayMs'));
        container.appendChild(createSetting('Enable Audio', 'checkbox', 'enableAudio'));
        container.appendChild(createSetting('Enable Notify', 'checkbox', 'enableNotify'));
        container.appendChild(createSetting('Dark Mode', 'checkbox', 'darkMode'));
        container.appendChild(retryBtn);
        container.appendChild(logPanel);
        document.body.appendChild(container);
        updateLogPanel();
    }

    function applyTheme(container) {
        const isDark = settings.darkMode;
        container.style.backgroundColor = isDark ? '#1f1f23' : '#f4f4f4';
        container.style.color = isDark ? '#f4f4f4' : '#1f1f23';

        const inputs = container.querySelectorAll('input');
        inputs.forEach(input => {
            input.style.backgroundColor = isDark ? '#333' : '#fff';
            input.style.color = isDark ? '#f4f4f4' : '#1f1f23';
        });
        const buttons = container.querySelectorAll('button');
        buttons.forEach(btn => {
            btn.style.width = '100%';
            btn.style.padding = '8px';
            btn.style.marginTop = '10px';
            btn.style.backgroundColor = isDark ? '#9147ff' : '#e6e6e6';
            btn.style.color = isDark ? 'white' : 'black';
            btn.style.border = 'none';
            btn.style.borderRadius = '5px';
            btn.style.cursor = 'pointer';
        });
    }

    function updateRetryDisplay() {
        const counter = document.getElementById('retry-counter');
        if (counter) counter.innerText = `Retries: ${settings.retryCount}/${settings.maxRetries}`;
    }

    function observeError() {
        const observer = new MutationObserver(() => {
            const text = document.body.innerText;
            if (text.includes('There was a network error. Please try again. (Error #2000)')) {
                if (settings.retryCount < settings.maxRetries) {
                    settings.retryCount++;
                    saveSettings();
                    updateRetryDisplay();
                    playAlertSound();
                    showToast('Reloading Twitch due to Error #2000...', true);
                    logEvent('Auto reload due to Error #2000');
                    setTimeout(() => location.reload(), settings.reloadDelayMs);
                } else {
                    showToast('Retry limit reached. Not reloading.', false);
                    logEvent('Retry limit reached. No reload.');
                }
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true,
            characterData: true
        });
    }

    
// ==UserScript== additions for Tampermonkey menu command
// Add menu command to show the UI
GM_registerMenuCommand("Show Twitch Reload Panel", () => {
    const existing = document.getElementById('twitch-reload-ui');
    if (existing) {
        existing.style.display = 'block';
    } else {
        createUI();
    }
});

document.addEventListener('keydown', (e) => {
    if (e.key === 'F3') {
        const panel = document.getElementById('twitch-reload-ui');
        if (panel) {
            panel.style.display = (panel.style.display === 'none') ? 'block' : 'none';
        } else {
            createUI();
        }
    }
});

window.addEventListener('load', () => {

        setTimeout(() => {
            createUI();
            observeError();
            logEvent('Observer initialized.');
        }, settings.startDelayMs);
    });
})();