GitHub Total Last 100 Releases Download Counter

Displays the total number of downloads for the last 100 releases on the main page of a GitHub repository.

// ==UserScript==
// @name         GitHub Total Last 100 Releases Download Counter
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description   Displays the total number of downloads for the last 100 releases on the main page of a GitHub repository.
// @author       OpenAI
// @match        https://github.com/*
// @grant        GM_xmlhttpRequest
// @connect      api.github.com
// @license MIT
// ==/UserScript==

(function () {
    'use strict';


    function isRepoRootPage() {
        return /^\/[^\/]+\/[^\/]+\/?$/.test(location.pathname);
    }

    function getOwnerRepo() {
        const match = location.pathname.match(/^\/([^\/]+)\/([^\/]+)\/?$/);
        return match ? { owner: match[1], repo: match[2] } : null;
    }

    function waitForElement(selector, timeout = 10000) {
        return new Promise((resolve, reject) => {
            const interval = 200;
            let elapsed = 0;
            const check = () => {
                const el = document.querySelector(selector);
                if (el) return resolve(el);
                elapsed += interval;
                if (elapsed >= timeout) return reject('Timeout');
                setTimeout(check, interval);
            };
            check();
        });
    }

    function fetchDownloadTotal(owner, repo) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: `https://api.github.com/repos/${owner}/${repo}/releases?per_page=100`,
                headers: {
                    'Accept': 'application/vnd.github+json',
                },
                onload: function (response) {
                    if (response.status !== 200) return reject('GitHub API error');
                    try {
                        const releases = JSON.parse(response.responseText);
                        const total = releases.reduce((sum, r) => {
                            return sum + r.assets.reduce((aSum, asset) => aSum + asset.download_count, 0);
                        }, 0);
                        resolve(total);
                    } catch (e) {
                        reject(e);
                    }
                }
            });
        });
    }

    function isDarkTheme() {
        const body = document.body;
        const hasDark = body.classList.contains('color-mode-dark') || body.classList.contains('theme-dark');
        const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
        return hasDark || prefersDark;
    }

    function injectCounter(total) {
        if (document.getElementById('release-download-counter')) return;

        const isDark = isDarkTheme();

        const counter = document.createElement('div');
        counter.id = 'release-download-counter';
        counter.style.margin = '8px 0';
        counter.style.padding = '6px 12px';
        counter.style.border = '1px solid';
        counter.style.borderRadius = '6px';
        counter.style.fontSize = '14px';
        counter.style.fontWeight = '500';
        counter.style.color = isDark ? '#c9d1d9' : '#24292f';
        counter.style.background = isDark ? '#161b22' : '#f6f8fa';
        counter.style.borderColor = isDark ? '#30363d' : '#d0d7de';
        counter.textContent = `📦 Total downloads (last 100 versions): ${total.toLocaleString()}`;

        waitForElement('.Layout-main').then(main => {
            main.prepend(counter);
        }).catch(() => {
            console.warn('Unable to inject the counter (Layout-main not found)');
        });
    }

    function run() {
        if (!isRepoRootPage()) return;
        const repoInfo = getOwnerRepo();
        if (!repoInfo) return;

        fetchDownloadTotal(repoInfo.owner, repoInfo.repo)
            .then(total => injectCounter(total))
            .catch(err => console.warn('Error Github API:', err));
    }

    // GitHub support for single-page application (SPA) navigation
    let lastUrl = location.href;
    new MutationObserver(() => {
        if (location.href !== lastUrl) {
            lastUrl = location.href;
            if (isRepoRootPage()) {
                setTimeout(run, 1000);
            }
        }
    }).observe(document.body, { childList: true, subtree: true });

    // First boot
    setTimeout(run, 1000);
})();