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.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Twitch Auto Reload on Error #2000 (God Mode)
// @namespace    http://tampermonkey.net/
// @version      5.1
// @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       SNOOKEEE
// @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() {
        // Check if UI already exists to avoid duplicates
        if (document.getElementById('twitch-reload-ui')) return;

        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
        });
    }

    // Tampermonkey 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();
        }
    });

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

    // Initialize observer on page load without creating UI
    window.addEventListener('load', () => {
        setTimeout(() => {
            observeError();
            logEvent('Observer initialized.');
        }, settings.startDelayMs);
    });
})();