YouTube Enhancer (Real-Time Subscriber Count)

Display Real-Time Subscriber Count.

当前为 2025-03-11 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         YouTube Enhancer (Real-Time Subscriber Count)
// @description  Display Real-Time Subscriber Count.
// @icon         https://raw.githubusercontent.com/exyezed/youtube-enhancer/refs/heads/main/extras/youtube-enhancer.png
// @version      1.4
// @author       exyezed
// @namespace    https://github.com/exyezed/youtube-enhancer/
// @supportURL   https://github.com/exyezed/youtube-enhancer/issues
// @license      MIT
// @match        https://www.youtube.com/*
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(function() {
    'use strict';

    const OPTIONS = ['subscribers', 'views', 'videos'];
    const FONT_LINK = "https://fonts.googleapis.com/css2?family=Rubik:wght@400;700&display=swap";
    const STATS_API_URL = 'https://api.livecounts.io/youtube-live-subscriber-counter/stats/';
    const DEFAULT_UPDATE_INTERVAL = 2000;
    const DEFAULT_OVERLAY_OPACITY = 0.75;

    let overlay = null;
    let isUpdating = false;
    let intervalId = null;
    let currentChannelName = null;
    let updateInterval = parseInt(localStorage.getItem('youtubeEnhancerInterval')) || DEFAULT_UPDATE_INTERVAL;
    let overlayOpacity = parseFloat(localStorage.getItem('youtubeEnhancerOpacity')) || DEFAULT_OVERLAY_OPACITY;

    const lastSuccessfulStats = new Map();
    const previousStats = new Map();
    
    let previousUrl = location.href;
    let isChecking = false;

    async function fetchChannel(url) {
      if (isChecking) return null;
      isChecking = true;
      
      try {
        const response = await fetch(url, {
          credentials: 'same-origin'
        });
        
        if (!response.ok) return null;
        
        const html = await response.text();
        const match = html.match(/var ytInitialData = (.+?);<\/script>/);
        return match && match[1] ? JSON.parse(match[1]) : null;
      } catch (error) {
        return null;
      } finally {
        isChecking = false;
      }
    }

    async function getChannelInfo(url) {
      const data = await fetchChannel(url);
      if (!data) return null;
      
      try {
        const channelName = data?.metadata?.channelMetadataRenderer?.title || "Unknown";
        const channelId = data?.metadata?.channelMetadataRenderer?.externalId || null;
        
        return {channelName, channelId};
      } catch (e) {
        return null;
      }
    }

    function isChannelPageUrl(url) {
      return url.includes('youtube.com/') && 
             (url.includes('/channel/') || url.includes('/@')) && 
             !url.includes('/video/') && 
             !url.includes('/watch');
    }

    function checkUrlChange() {
      const currentUrl = location.href;
      if (currentUrl !== previousUrl) {
        previousUrl = currentUrl;
        if (isChannelPageUrl(currentUrl)) {
          setTimeout(() => getChannelInfo(currentUrl), 500);
        }
      }
    }

    history.pushState = (function(f) {
      return function() {
        f.apply(this, arguments);
        checkUrlChange();
      };
    })(history.pushState);

    history.replaceState = (function(f) {
      return function() {
        f.apply(this, arguments);
        checkUrlChange();
      };
    })(history.replaceState);

    window.addEventListener('popstate', checkUrlChange);
    setInterval(checkUrlChange, 1000);

    function init() {
        loadFonts();
        initializeLocalStorage();
        addStyles();
        observePageChanges();
        addNavigationListener();
        
        if (isChannelPageUrl(location.href)) {
          getChannelInfo(location.href);
        }
    }

    function loadFonts() {
        const fontLink = document.createElement("link");
        fontLink.rel = "stylesheet";
        fontLink.href = FONT_LINK;
        document.head.appendChild(fontLink);
    }

    function initializeLocalStorage() {
        OPTIONS.forEach(option => {
            if (localStorage.getItem(`show-${option}`) === null) {
                localStorage.setItem(`show-${option}`, 'true');
            }
        });
    }

    function addStyles() {
        const style = document.createElement('style');
        style.textContent = `
            .settings-button {
                position: absolute;
                top: 12px;
                right: 12px;
                width: 16px;
                height: 16px;
                cursor: pointer;
                z-index: 2;
                transition: transform 0.3s ease;
            }
            .settings-button:hover {
                transform: rotate(45deg);
            }
            .settings-menu {
                position: absolute;
                top: 35px;
                right: 12px;
                background: rgba(0, 0, 0, 0.95);
                padding: 10px;
                border-radius: 6px;
                z-index: 2;
                display: none;
            }
            .settings-menu.show {
                display: flex;
            }
            .interval-slider, .opacity-slider {
                width: 160px;
                margin: 5px 0;
                height: 4px;
            }
            .interval-value, .opacity-value {
                color: white;
                font-size: 12px;
                margin-top: 3px;
                margin-bottom: 8px;
            }
            .setting-group {
                margin-bottom: 10px;
            }
            .setting-group:last-child {
                margin-bottom: 0;
            }
            @keyframes spin {
                from { transform: rotate(0deg); }
                to { transform: rotate(360deg); }
            }
        `;
        document.head.appendChild(style);
    }

    function createSettingsButton() {
        const button = document.createElement('div');
        button.className = 'settings-button';
        
        const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
        svg.setAttribute("viewBox", "0 0 512 512");
        
        const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
        path.setAttribute("fill", "white");
        path.setAttribute("d", "M495.9 166.6c3.2 8.7 .5 18.4-6.4 24.6l-43.3 39.4c1.1 8.3 1.7 16.8 1.7 25.4s-.6 17.1-1.7 25.4l43.3 39.4c6.9 6.2 9.6 15.9 6.4 24.6c-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2c-5.9 7.2-15.7 9.6-24.5 6.8l-55.7-17.7c-13.4 10.3-28.2 18.9-44 25.4l-12.5 57.1c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-12.5-57.1c-15.8-6.5-30.6-15.1-44-25.4L83.1 425.9c-8.8 2.8-18.6 .3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l43.3-39.4C64.6 273.1 64 264.6 64 256s.6-17.1 1.7-25.4L22.4 191.2c-6.9-6.2-9.6-15.9-6.4-24.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l55.7 17.7c13.4-10.3 28.2-18.9 44-25.4l12.5-57.1c2-9.1 9-16.3 18.2-17.8C227.3 1.2 241.5 0 256 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l12.5 57.1c15.8 6.5 30.6 15.1 44 25.4l55.7-17.7c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM256 336a80 80 0 1 0 0-160 80 80 0 1 0 0 160z");
        
        svg.appendChild(path);
        button.appendChild(svg);
        
        return button;
    }

    function createSettingsMenu() {
        const menu = document.createElement('div');
        menu.className = 'settings-menu';
        menu.style.gap = '15px'; 
        menu.style.width = '360px';
    
        const displaySection = createDisplaySection();
        const controlsSection = createControlsSection();
    
        menu.appendChild(displaySection);
        menu.appendChild(controlsSection);
    
        return menu;
    }

    function createDisplaySection() {
        const displaySection = document.createElement('div');
        displaySection.style.flex = '1';
        
        const displayLabel = document.createElement('label');
        displayLabel.textContent = 'Display Options';
        displayLabel.style.marginBottom = '10px';
        displayLabel.style.display = 'block';
        displayLabel.style.fontSize = '16px';
        displayLabel.style.fontWeight = 'bold';
        displaySection.appendChild(displayLabel);
    
        OPTIONS.forEach(option => {
            const checkboxContainer = document.createElement('div');
            checkboxContainer.style.display = 'flex';
            checkboxContainer.style.alignItems = 'center';
            checkboxContainer.style.marginTop = '5px';
    
            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.id = `show-${option}`;
            checkbox.checked = localStorage.getItem(`show-${option}`) !== 'false';
            checkbox.style.marginRight = '8px';
            checkbox.style.cursor = 'pointer';
    
            const checkboxLabel = document.createElement('label');
            checkboxLabel.htmlFor = `show-${option}`;
            checkboxLabel.textContent = option.charAt(0).toUpperCase() + option.slice(1);
            checkboxLabel.style.cursor = 'pointer';
            checkboxLabel.style.color = 'white';
            checkboxLabel.style.fontSize = '14px';
    
            checkbox.addEventListener('change', () => {
                localStorage.setItem(`show-${option}`, checkbox.checked);
                updateDisplayState();
            });
    
            checkboxContainer.appendChild(checkbox);
            checkboxContainer.appendChild(checkboxLabel);
            displaySection.appendChild(checkboxContainer);
        });

        return displaySection;
    }

    function createControlsSection() {
        const controlsSection = document.createElement('div');
        controlsSection.style.flex = '1';
        
        const intervalLabel = document.createElement('label');
        intervalLabel.textContent = 'Update Interval';
        intervalLabel.style.display = 'block';
        intervalLabel.style.marginBottom = '5px';
        intervalLabel.style.fontSize = '16px';
        intervalLabel.style.fontWeight = 'bold';
    
        const intervalSlider = document.createElement('input');
        intervalSlider.type = 'range';
        intervalSlider.min = '2';
        intervalSlider.max = '10';
        intervalSlider.value = updateInterval / 1000;
        intervalSlider.step = '1';
        intervalSlider.className = 'interval-slider';
    
        const intervalValue = document.createElement('div');
        intervalValue.className = 'interval-value';
        intervalValue.textContent = `${intervalSlider.value}s`;
        intervalValue.style.marginBottom = '15px';
        intervalValue.style.fontSize = '14px';
    
        intervalSlider.addEventListener('input', (e) => {
            const newInterval = parseInt(e.target.value) * 1000;
            intervalValue.textContent = `${e.target.value}s`;
            updateInterval = newInterval;
            localStorage.setItem('youtubeEnhancerInterval', newInterval);
    
            if (intervalId) {
                clearInterval(intervalId);
                intervalId = setInterval(() => {
                    updateOverlayContent(overlay, currentChannelName);
                }, newInterval);
            }
        });
    
        const opacityLabel = document.createElement('label');
        opacityLabel.textContent = 'Background Opacity';
        opacityLabel.style.display = 'block';
        opacityLabel.style.marginBottom = '5px';
        opacityLabel.style.fontSize = '16px';
        opacityLabel.style.fontWeight = 'bold';
    
        const opacitySlider = document.createElement('input');
        opacitySlider.type = 'range';
        opacitySlider.min = '50';
        opacitySlider.max = '90';
        opacitySlider.value = overlayOpacity * 100;
        opacitySlider.step = '5';
        opacitySlider.className = 'opacity-slider';
    
        const opacityValue = document.createElement('div');
        opacityValue.className = 'opacity-value';
        opacityValue.textContent = `${opacitySlider.value}%`;
        opacityValue.style.fontSize = '14px';
    
        opacitySlider.addEventListener('input', (e) => {
            const newOpacity = parseInt(e.target.value) / 100;
            opacityValue.textContent = `${e.target.value}%`;
            overlayOpacity = newOpacity;
            localStorage.setItem('youtubeEnhancerOpacity', newOpacity);
    
            if (overlay) {
                overlay.style.backgroundColor = `rgba(0, 0, 0, ${newOpacity})`;
            }
        });
    
        controlsSection.appendChild(intervalLabel);
        controlsSection.appendChild(intervalSlider);
        controlsSection.appendChild(intervalValue);
        controlsSection.appendChild(opacityLabel);
        controlsSection.appendChild(opacitySlider);
        controlsSection.appendChild(opacityValue);

        return controlsSection;
    }

    function createSpinner() {
        const spinnerContainer = document.createElement('div');
        spinnerContainer.style.position = 'absolute';
        spinnerContainer.style.top = '0';
        spinnerContainer.style.left = '0';
        spinnerContainer.style.width = '100%';
        spinnerContainer.style.height = '100%';
        spinnerContainer.style.display = 'flex';
        spinnerContainer.style.justifyContent = 'center';
        spinnerContainer.style.alignItems = 'center';
        spinnerContainer.classList.add('spinner-container');

        const spinner = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        spinner.setAttribute("viewBox", "0 0 512 512");
        spinner.setAttribute("width", "64");
        spinner.setAttribute("height", "64");
        spinner.classList.add('loading-spinner');
        spinner.style.animation = "spin 1s linear infinite";

        const secondaryPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
        secondaryPath.setAttribute("d", "M0 256C0 114.9 114.1 .5 255.1 0C237.9 .5 224 14.6 224 32c0 17.7 14.3 32 32 32C150 64 64 150 64 256s86 192 192 192c69.7 0 130.7-37.1 164.5-92.6c-3 6.6-3.3 14.8-1 22.2c1.2 3.7 3 7.2 5.4 10.3c1.2 1.5 2.6 3 4.1 4.3c.8 .7 1.6 1.3 2.4 1.9c.4 .3 .8 .6 1.3 .9s.9 .6 1.3 .8c5 2.9 10.6 4.3 16 4.3c11 0 21.8-5.7 27.7-16c-44.3 76.5-127 128-221.7 128C114.6 512 0 397.4 0 256z");
        secondaryPath.style.opacity = "0.4";
        secondaryPath.style.fill = "white";

        const primaryPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
        primaryPath.setAttribute("d",
            "M224 32c0-17.7 14.3-32 32-32C397.4 0 512 114.6 512 256c0 46.6-12.5 90.4-34.3 128c-8.8 15.3-28.4 20.5-43.7 11.7s-20.5-28.4-11.7-43.7c16.3-28.2 25.7-61 25.7-96c0-106-86-192-192-192c-17.7 0-32-14.3-32-32z");
        primaryPath.style.fill = "white";

        spinner.appendChild(secondaryPath);
        spinner.appendChild(primaryPath);
        spinnerContainer.appendChild(spinner);
        return spinnerContainer;
    }

    function createSVGIcon(path) {
        const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        svg.setAttribute("viewBox", "0 0 640 512");
        svg.setAttribute("width", "2rem");
        svg.setAttribute("height", "2rem");
        svg.style.marginRight = "0.5rem";
        svg.style.display = "none";

        const svgPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
        svgPath.setAttribute("d", path);
        svgPath.setAttribute("fill", "white");

        svg.appendChild(svgPath);
        return svg;
    }

    function createStatContainer(className, iconPath) {
        const container = document.createElement('div');
        Object.assign(container.style, {
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
            justifyContent: 'center',
            visibility: 'hidden',
            width: '33%',
            height: '100%',
            padding: '0 1rem'
        });

        const numberContainer = document.createElement('div');
        Object.assign(numberContainer.style, {
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
            justifyContent: 'center'
        });

        const differenceElement = document.createElement('div');
        differenceElement.classList.add(`${className}-difference`);
        Object.assign(differenceElement.style, {
            fontSize: '2.5rem',
            height: '2.5rem',
            marginBottom: '1rem'
        });

        const digitContainer = createNumberContainer();
        digitContainer.classList.add(`${className}-number`);
        Object.assign(digitContainer.style, {
            fontSize: '4rem',
            fontWeight: 'bold',
            lineHeight: '1',
            height: '4rem',
            fontFamily: 'inherit',
            letterSpacing: '0.025em'
        });

        numberContainer.appendChild(differenceElement);
        numberContainer.appendChild(digitContainer);

        const labelContainer = document.createElement('div');
        Object.assign(labelContainer.style, {
            display: 'flex',
            alignItems: 'center',
            marginTop: '0.5rem'
        });

        const icon = createSVGIcon(iconPath);
        Object.assign(icon.style, {
            width: '2rem',
            height: '2rem',
            marginRight: '0.75rem'
        });

        const labelElement = document.createElement('div');
        labelElement.classList.add(`${className}-label`);
        labelElement.style.fontSize = '2rem';

        labelContainer.appendChild(icon);
        labelContainer.appendChild(labelElement);

        container.appendChild(numberContainer);
        container.appendChild(labelContainer);

        return container;
    }

    function createOverlay(bannerElement) {
        clearExistingOverlay();

        if (!bannerElement) return null;

        const overlay = document.createElement('div');
        overlay.classList.add('channel-banner-overlay');
        Object.assign(overlay.style, {
            position: 'absolute',
            top: '0',
            left: '0',
            width: '100%',
            height: '100%',
            backgroundColor: `rgba(0, 0, 0, ${overlayOpacity})`,
            borderRadius: '15px',
            zIndex: '1',
            display: 'flex',
            justifyContent: 'space-around',
            alignItems: 'center',
            color: 'white',
            fontFamily: 'Rubik, sans-serif'
        });

        const settingsButton = createSettingsButton();
        const settingsMenu = createSettingsMenu();
        overlay.appendChild(settingsButton);
        overlay.appendChild(settingsMenu);

        settingsButton.addEventListener('click', (e) => {
            e.stopPropagation();
            settingsMenu.classList.toggle('show');
        });

        document.addEventListener('click', (e) => {
            if (!settingsMenu.contains(e.target) && !settingsButton.contains(e.target)) {
                settingsMenu.classList.remove('show');
            }
        });

        const spinner = createSpinner();
        overlay.appendChild(spinner);

        const subscribersElement = createStatContainer('subscribers', "M144 160c-44.2 0-80-35.8-80-80S99.8 0 144 0s80 35.8 80 80s-35.8 80-80 80zm368 0c-44.2 0-80-35.8-80-80s35.8-80 80-80s80 35.8 80 80s-35.8 80-80 80zM0 298.7C0 239.8 47.8 192 106.7 192h42.7c15.9 0 31 3.5 44.6 9.7c-1.3 7.2-1.9 14.7-1.9 22.3c0 38.2 16.8 72.5 43.3 96c-.2 0-.4 0-.7 0H21.3C9.6 320 0 310.4 0 298.7zM405.3 320c-.2 0-.4 0-.7 0c26.6-23.5 43.3-57.8 43.3-96c0-7.6-.7-15-1.9-22.3c13.6-6.3 28.7-9.7 44.6-9.7h42.7C592.2 192 640 239.8 640 298.7c0 11.8-9.6 21.3-21.3 21.3H405.3zM416 224c0 53-43 96-96 96s-96-43-96-96s43-96 96-96s96 43 96 96zM128 485.3C128 411.7 187.7 352 261.3 352H378.7C452.3 352 512 411.7 512 485.3c0 14.7-11.9 26.7-26.7 26.7H154.7c-14.7 0-26.7-11.9-26.7-26.7z");
        const viewsElement = createStatContainer('views', "M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM144 256a144 144 0 1 1 288 0 144 144 0 1 1 -288 0zm144-64c0 35.3-28.7 64-64 64c-7.1 0-13.9-1.2-20.3-3.3c-5.5-1.8-11.9 1.6-11.7 7.4c.3 6.9 1.3 13.8 3.2 20.7c13.7 51.2 66.4 81.6 117.6 67.9s81.6-66.4 67.9-117.6c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3z");
        const videosElement = createStatContainer('videos', "M0 128C0 92.7 28.7 64 64 64H320c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V128zM559.1 99.8c10.4 5.6 16.9 16.4 16.9 28.2V384c0 11.8-6.5 22.6-16.9 28.2s-23 5-32.9-1.6l-96-64L416 337.1V320 192 174.9l14.2-9.5 96-64c9.8-6.5 22.4-7.2 32.9-1.6z");

        overlay.appendChild(subscribersElement);
        overlay.appendChild(viewsElement);
        overlay.appendChild(videosElement);
        
        bannerElement.appendChild(overlay);
        updateDisplayState();
        return overlay;
    }

    function fetchWithGM(url, headers = {}) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: url,
                headers: headers,
                onload: function(response) {
                    if (response.status === 200) {
                        resolve(JSON.parse(response.responseText));
                    } else {
                        reject(new Error(`Failed to fetch: ${response.status}`));
                    }
                },
                onerror: function(error) {
                    reject(error);
                },
            });
        });
    }

    async function fetchChannelId(channelName) {
        try {
            const channelInfo = await getChannelInfo(window.location.href);
            if (channelInfo && channelInfo.channelId) {
                return channelInfo.channelId;
            }
            
            const metaTag = document.querySelector('meta[itemprop="channelId"]');
            if (metaTag && metaTag.content) {
                return metaTag.content;
            }
            
            const urlMatch = window.location.href.match(/channel\/(UC[\w-]+)/);
            if (urlMatch && urlMatch[1]) {
                return urlMatch[1];
            }
            
            throw new Error('Could not determine channel ID');
        } catch (error) {
            const metaTag = document.querySelector('meta[itemprop="channelId"]');
            if (metaTag && metaTag.content) {
                return metaTag.content;
            }
            
            const urlMatch = window.location.href.match(/channel\/(UC[\w-]+)/);
            if (urlMatch && urlMatch[1]) {
                return urlMatch[1];
            }
            
            throw new Error('Could not determine channel ID');
        }
    }
    
    async function fetchChannelStats(channelId) {
        try {
            let retries = 3;
            let lastError;
            
            while (retries > 0) {
                try {
                    const stats = await fetchWithGM(
                        `${STATS_API_URL}${channelId}`,
                        {
                            origin: "https://livecounts.io",
                            referer: "https://livecounts.io/",
                        }
                    );
                    
                    if (!stats || typeof stats.followerCount === 'undefined') {
                        throw new Error('Invalid stats response');
                    }
                    
                    lastSuccessfulStats.set(channelId, stats);
                    return stats;
                } catch (e) {
                    lastError = e;
                    retries--;
                    if (retries > 0) {
                        await new Promise(resolve => setTimeout(resolve, 1000));
                    }
                }
            }
            
            if (lastSuccessfulStats.has(channelId)) {
                return lastSuccessfulStats.get(channelId);
            }
            
            const fallbackStats = {
                followerCount: 0,
                bottomOdos: [0, 0],
                error: true
            };
            
            const subCountElem = document.querySelector('#subscriber-count');
            if (subCountElem) {
                const subText = subCountElem.textContent;
                const subMatch = subText.match(/[\d,]+/);
                if (subMatch) {
                    fallbackStats.followerCount = parseInt(subMatch[0].replace(/,/g, ''));
                }
            }
            
            return fallbackStats;
            
        } catch (error) {
            throw error;
        }
    }

    function clearExistingOverlay() {
        const existingOverlay = document.querySelector('.channel-banner-overlay');
        if (existingOverlay) {
            existingOverlay.remove();
        }
        if (intervalId) {
            clearInterval(intervalId);
            intervalId = null;
        }
        lastSuccessfulStats.clear();
        previousStats.clear();
        isUpdating = false;
        overlay = null;
    }

    function createDigitElement() {
        const digit = document.createElement('span');
        Object.assign(digit.style, {
            display: 'inline-block',
            width: '0.6em',
            textAlign: 'center',
            marginRight: '0.025em',
            marginLeft: '0.025em'
        });
        return digit;
    }

    function createCommaElement() {
        const comma = document.createElement('span');
        comma.textContent = ',';
        Object.assign(comma.style, {
            display: 'inline-block',
            width: '0.3em',
            textAlign: 'center'
        });
        return comma;
    }

    function createNumberContainer() {
        const container = document.createElement('div');
        Object.assign(container.style, {
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'center',
            letterSpacing: '0.025em'
        });
        return container;
    }

    function updateDigits(container, newValue) {
        const newValueStr = newValue.toString();
        const digits = [];
    
        for (let i = newValueStr.length - 1; i >= 0; i -= 3) {
            const start = Math.max(0, i - 2);
            digits.unshift(newValueStr.slice(start, i + 1));
        }
    
        while (container.firstChild) {
            container.removeChild(container.firstChild);
        }
        
        let digitIndex = 0;
    
        for (let i = 0; i < digits.length; i++) {
            const group = digits[i];
            for (let j = 0; j < group.length; j++) {
                const digitElement = createDigitElement();
                digitElement.textContent = group[j];
                container.appendChild(digitElement);
                digitIndex++;
            }
            if (i < digits.length - 1) {
                container.appendChild(createCommaElement());
            }
        }
    
        let elementIndex = 0;
        for (let i = 0; i < digits.length; i++) {
            const group = digits[i];
            for (let j = 0; j < group.length; j++) {
                const digitElement = container.children[elementIndex];
                const newDigit = parseInt(group[j]);
                const currentDigit = parseInt(digitElement.textContent || '0');
    
                if (currentDigit !== newDigit) {
                    animateDigit(digitElement, currentDigit, newDigit);
                }
                elementIndex++;
            }
            if (i < digits.length - 1) {
                elementIndex++;
            }
        }
    }

    function animateDigit(element, start, end) {
        const duration = 1000;
        const startTime = performance.now();

        function update(currentTime) {
            const elapsed = currentTime - startTime;
            const progress = Math.min(elapsed / duration, 1);
            const easeOutQuart = 1 - Math.pow(1 - progress, 4);
            const current = Math.round(start + (end - start) * easeOutQuart);
            element.textContent = current;

            if (progress < 1) {
                requestAnimationFrame(update);
            }
        }

        requestAnimationFrame(update);
    }

    function showContent(overlay) {
        const spinnerContainer = overlay.querySelector('.spinner-container');
        if (spinnerContainer) {
            spinnerContainer.remove();
        }

        const containers = overlay.querySelectorAll('div[style*="visibility: hidden"]');
        containers.forEach(container => {
            container.style.visibility = 'visible';
        });

        const icons = overlay.querySelectorAll('svg[style*="display: none"]');
        icons.forEach(icon => {
            icon.style.display = 'block';
        });
    }

    function updateDifferenceElement(element, currentValue, previousValue) {
        if (!previousValue) return;
    
        const difference = currentValue - previousValue;
        if (difference === 0) {
            element.textContent = '';
            return;
        }
    
        const sign = difference > 0 ? '+' : '';
        element.textContent = `${sign}${difference.toLocaleString()}`;
        element.style.color = difference > 0 ? '#1ed760' : '#f3727f';
    
        setTimeout(() => {
            element.textContent = '';
        }, 1000);
    }

    function updateDisplayState() {
        const overlay = document.querySelector('.channel-banner-overlay');
        if (!overlay) return;
    
        const statContainers = overlay.querySelectorAll('div[style*="width"]');
        if (!statContainers.length) return;
    
        let visibleCount = 0;
        const visibleContainers = [];
    
        statContainers.forEach(container => {
            const numberContainer = container.querySelector('[class$="-number"]');
            if (!numberContainer) return;
            
            const type = numberContainer.className.replace('-number', '');
            
            const isVisible = localStorage.getItem(`show-${type}`) !== 'false';
            
            if (isVisible) {
                container.style.display = 'flex';
                visibleCount++;
                visibleContainers.push(container);
            } else {
                container.style.display = 'none';
            }
        });
    
        visibleContainers.forEach(container => {
            container.style.width = '';
            container.style.margin = '';
            
            switch (visibleCount) {
                case 1:
                    container.style.width = '100%';
                    break;
                case 2:
                    container.style.width = '50%';
                    break;
                case 3:
                    container.style.width = '33.33%';
                    break;
                default:
                    container.style.display = 'none';
            }
        });
    
        overlay.style.display = 'flex';
    }

    async function updateOverlayContent(overlay, channelName) {
        if (isUpdating || channelName !== currentChannelName) return;
        isUpdating = true;
        
        try {
            const channelId = await fetchChannelId(channelName);
            const stats = await fetchChannelStats(channelId);
            
            if (channelName !== currentChannelName) {
                isUpdating = false;
                return;
            }
            
            if (stats.error) {
                const containers = overlay.querySelectorAll('[class$="-number"]');
                containers.forEach(container => {
                    if (container.classList.contains('subscribers-number') && stats.followerCount > 0) {
                        updateDigits(container, stats.followerCount);
                    } else {
                        container.textContent = '---';
                    }
                });
                return;
            }

            const updateElement = (className, value, label) => {
                const numberContainer = overlay.querySelector(`.${className}-number`);
                const differenceElement = overlay.querySelector(`.${className}-difference`);
                const labelElement = overlay.querySelector(`.${className}-label`);
                
                if (numberContainer) {
                    updateDigits(numberContainer, value);
                }
                
                if (differenceElement && previousStats.has(channelId)) {
                    const previousValue = className === 'subscribers' ?
                        previousStats.get(channelId).followerCount :
                        previousStats.get(channelId).bottomOdos[className === 'views' ? 0 : 1];
                    updateDifferenceElement(differenceElement, value, previousValue);
                }
                
                if (labelElement) {
                    labelElement.textContent = label;
                }
            };
            
            updateElement('subscribers', stats.followerCount, 'Subscribers');
            updateElement('views', stats.bottomOdos[0], 'Views');
            updateElement('videos', stats.bottomOdos[1], 'Videos');
            
            if (!previousStats.has(channelId)) {
                showContent(overlay);
            }
            
            previousStats.set(channelId, stats);
            
        } catch (error) {
            const containers = overlay.querySelectorAll('[class$="-number"]');
            containers.forEach(container => {
                container.textContent = '---';
            });
        } finally {
            isUpdating = false;
        }
    }

    function addOverlay(bannerElement) {
        const channelName = window.location.pathname.split("/")[1].replace("@", "");

        if (channelName === currentChannelName && overlay) {
            return;
        }

        currentChannelName = channelName;
        overlay = createOverlay(bannerElement);

        if (overlay) {
            if (intervalId) {
                clearInterval(intervalId);
            }

            intervalId = setInterval(() => {
                updateOverlayContent(overlay, channelName);
            }, updateInterval);

            updateOverlayContent(overlay, channelName);
        }
    }

    function isChannelPage() {
        return window.location.pathname.startsWith("/@") ||
               window.location.pathname.startsWith("/channel/") ||
               window.location.pathname.startsWith("/c/");
    }

    function observePageChanges() {
        const observer = new MutationObserver((mutations) => {
            for (const mutation of mutations) {
                if (mutation.type === 'childList') {
                    const bannerElement = document.getElementById('page-header-banner-sizer');
                    if (bannerElement && isChannelPage()) {
                        addOverlay(bannerElement);
                        break;
                    }
                }
            }
        });

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

    function addNavigationListener() {
        window.addEventListener("yt-navigate-finish", () => {
            if (!isChannelPage()) {
                clearExistingOverlay();
                currentChannelName = null;
            } else {
                const bannerElement = document.getElementById('page-header-banner-sizer');
                if (bannerElement) {
                    addOverlay(bannerElement);
                }
            }
        });
    }

    init();
})();