[DS] Twitch Logged-In Chatters Counter

Добавляет счетчик залогинившихся пользователей рядом с оригинальным счетчиком зрителей на Twitch.

// ==UserScript==
// @name         [DS] Twitch Logged-In Chatters Counter
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  Добавляет счетчик залогинившихся пользователей рядом с оригинальным счетчиком зрителей на Twitch.
// @author       DS2902
// @match        https://www.twitch.tv/*
// @icon         https://www.google.com/s2/favicons?domain=twitch.tv
// @license      MIT
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const CLIENT_ID = "a2QxdW5iNGIzcTR0NThmd2xwY2J6Y2JubTc2YThmcA==";
    const GQL_ENDPOINT = "https://gql.twitch.tv/gql";
    const UPDATE_INTERVAL = 30000; // Интервал обновления в миллисекундах (30 секунд)
    const MAX_FAILED_ATTEMPTS = 5;

    let isUpdating = false;
    let isTabActive = true;
    let failedAttempts = 0;
    let observer;

    // Отслеживание состояния вкладки
    document.addEventListener("visibilitychange", () => {
        if (document.hidden) {
            isTabActive = false;
        } else {
            isTabActive = true;
            forceUpdateCounter();
        }
    });

    async function gqlRequest(query, variables) {
        try {
            const response = await fetch(GQL_ENDPOINT, {
                method: "POST",
                headers: {
                    "Client-ID": atob(CLIENT_ID),
                    "Content-Type": "application/json",
                },
                body: JSON.stringify({ query, variables }),
            });

            if (!response.ok) {
                throw new Error(`GraphQL request failed: ${response.status} ${response.statusText}`);
            }

            return response.json();
        } catch (error) {
            console.error("Ошибка при выполнении GraphQL-запроса:", error);
            return null;
        }
    }

    async function getChattersCount(channelName) {
        const query = `
            query GetChannelChattersCount($name: String!) {
                channel(name: $name) {
                    chatters {
                        count
                    }
                }
            }
        `;

        const variables = { name: channelName };
        const data = await gqlRequest(query, variables);

        return data?.data?.channel?.chatters?.count || 0;
    }

    function createChattersCounterComponent(parentElement) {
        const wrapper = document.createElement("span");
        wrapper.className = "enhancer-chat-counter-wrapper";

        const counter = document.createElement("span");
        counter.className = "logged-in-chatters-count";
        counter.style.cursor = "pointer";
        counter.style.marginLeft = "4px";
        counter.style.color = "#ff8280";
        counter.style.fontWeight = "600";

        counter.addEventListener("click", () => {
            updateCounterManually(counter);
        });

        wrapper.appendChild(counter);
        parentElement.appendChild(wrapper);

        return counter;
    }

    function animateCounter(element, startValue, endValue) {
        const duration = 2000;
        const steps = Math.ceil(duration / 16);
        const increment = (endValue - startValue) / steps;
        let currentStep = 0;

        function update() {
            if (currentStep < steps) {
                const currentValue = Math.round(startValue + increment * currentStep);
                element.textContent = `(${currentValue})`;
                currentStep++;
                requestAnimationFrame(update);
            } else {
                element.textContent = `(${endValue})`;
            }
        }

        update();
    }

    async function updateCounter(originalCounter) {
        if (!isTabActive || isUpdating) return; // Не обновляем, если вкладка неактивна или идёт обновление
        isUpdating = true;

        try {
            const channelName = window.location.pathname.split("/")[1];
            if (!channelName) return;

            const count = await getChattersCount(channelName);
            const counterElement = originalCounter.querySelector(".logged-in-chatters-count");

            if (counterElement) {
                const currentCount = parseInt(counterElement.textContent.trim().slice(1, -1));
                animateCounter(counterElement, currentCount, count);
            } else {
                const counterElement = createChattersCounterComponent(originalCounter);
                counterElement.textContent = `(${count})`;
            }

            failedAttempts = 0;
        } catch (error) {
            console.error("Ошибка при обновлении счетчика:", error);
            failedAttempts++;

            if (failedAttempts >= MAX_FAILED_ATTEMPTS) {
                console.log("Скрипт остановлен из-за отсутствия оригинального счетчика.");
                stopScript();
            }
        } finally {
            isUpdating = false;
        }
    }

    async function updateCounterManually(counterElement) {
        if (!counterElement) return;

        counterElement.style.color = "rgba(255, 255, 255, 0.8)";
        await updateCounter(counterElement.parentElement);
        setTimeout(() => {
            counterElement.style.color = "";
        }, 200);
    }

    async function forceUpdateCounter() {
        const selectors = [
            'strong[data-a-target="animated-channel-viewers-count"]',
            'div[data-a-target="channel-viewers-count"]',
            'p[data-test-selector="stream-info-card-component__description"]'
        ];

        for (const selector of selectors) {
            const originalCounter = document.querySelector(selector);
            if (originalCounter) {
                await updateCounter(originalCounter);
            }
        }
    }

    function stopScript() {
        if (observer) {
            observer.disconnect();
        }
        clearInterval(intervalId);
    }

    async function main() {
        observer = new MutationObserver(async () => {
            const selectors = [
                'strong[data-a-target="animated-channel-viewers-count"]',
                'div[data-a-target="channel-viewers-count"]',
                'p[data-test-selector="stream-info-card-component__description"]'
            ];

            for (const selector of selectors) {
                const originalCounter = document.querySelector(selector);
                if (originalCounter && !originalCounter.querySelector(".enhancer-chat-counter-wrapper")) {
                    await updateCounter(originalCounter);
                }
            }
        });

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

        // Автоматическое обновление каждые 30 секунд
        const intervalId = setInterval(async () => {
            const selectors = [
                'strong[data-a-target="animated-channel-viewers-count"]',
                'div[data-a-target="channel-viewers-count"]',
                'p[data-test-selector="stream-info-card-component__description"]'
            ];

            for (const selector of selectors) {
                const originalCounter = document.querySelector(selector);
                if (originalCounter) {
                    await updateCounter(originalCounter);
                }
            }
        }, UPDATE_INTERVAL);
    }

    main();
})();