Github repos stats

Load some stats for repo list, and display star counts for GitHub repositories.

Від 17.09.2024. Дивіться остання версія.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        Github repos stats
// @namespace   Violentmonkey Scripts
// @description Load some stats for repo list, and display star counts for GitHub repositories. 

// @match https://github.com/*
// @version     1.0

// @grant       GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant      GM_listValues
//
// @require https://cdn.jsdelivr.net/npm/@violentmonkey/shortcut@1
// @license MIT
// ==/UserScript==

	// 设置缓存过期时间为 10天 :  1 小时(3600000 毫秒)*24*10
const CACHE_EXPIRATION = 3600000 * 24 * 10;





(async function ()
{
	'use strict';


	// 注册菜单命令
	GM_registerMenuCommand("Query and Show Stats", showStats);

  const currentPageRepo = (function extractRepoFromGitHubUrl(url) {
    // 1. 检查 URL 是否是 GitHub 链接
    if (!url.includes('github.com')) {
      return null;
    }

    // 2. 使用正则表达式提取 repo 名
    const match = url.match(/github\.com\/([^/]+)\/([^/#?]+)/);
    if (match) {
      return  match[1] +'/'+   match[2]
    }

    return null;
  })(location.href);



	function showStats()
	{
		const githubToken = GM_getValue("githubToken", "");
		if (!githubToken)
		{
			console.warn("GitHub token not set. Please set it in the script settings.");
			return;
		}



		const url = window.location.href;
		const selector = getSelector(url);

		if (!selector) return;


		inject(selector, githubToken);

	} // end showStats


const Tools = {

  // Promise.all 运行太多的 promises? 分批运行,第一批都成功,才运行下一批
  // https://gist.github.com/scil/15d63220521808ba7839f423e4d8a784
    runPromisesInBatches:async function(promises, batchSize = 50,startIndex = 0) {

    let results = [];
    let errorIndex=null;

    while (startIndex < promises.length) {
      const batch = promises.slice(startIndex, startIndex + batchSize);
      try {
        const batchResults = await Promise.all(batch);
        results.push(...batchResults);
        // console.log(batchResults)
        startIndex += batchSize;
      } catch (error) {
        errorIndex = startIndex
        console.error(`Error processing batch starting at index ${startIndex}:`, error);
        // 处理错误,例如记录错误信息或停止执行
        break; // 停止执行,避免后续批次继续执行
      }
    }

    return [results, errorIndex];
  }
  ,
  	isGitHubRepo:function(url)
	{
		const githubRegex = /^https:\/\/github\.com\/[^/]+\/[^/#]+$/;
		return githubRegex.test(url);
	},

	roundNumber:function(number)
	{
		if (number < 1000) return number;

		const suffixes = ['', 'k', 'M', 'B', 'T'];

		const suffixIndex = Math.floor(Math.log10(number) / 3);
		const scaledNumber = number / Math.pow(10, suffixIndex * 3);

		const formattedNumber = scaledNumber % 1 === 0 ? scaledNumber.toFixed(0) : scaledNumber.toFixed(1);

		return `${formattedNumber}${suffixes[suffixIndex]}`;
	},
	// 缓存包装函数
	getCachedValue:function(key, defaultValue, expirationTime)
	{
		const cachedData =  GM_getValue(key);
		if (cachedData)
		{
			const
			{
				value,
				timestamp
			} = JSON.parse(cachedData);
			if (Date.now() - timestamp < expirationTime)
			{
				return value;
			}
		}
		return defaultValue;
	},

	setCachedValue:function(key, value)
	{
		const data = JSON.stringify(
		{
			value: value,
			timestamp: Date.now()
		});
		 GM_setValue(key, data);
	},
}


	function getSelector(url)
	{
		const selectors = [
		{
			pattern: /https?:\/\/github.com\/[^\/]+\/[^\/]+\/*$/,
			// selector: "#readme",
			selector: '.markdown-body',
		},
		{
			pattern: /https?:\/\/github.com\/.*\/[Rr][Ee][Aa][Dd][Mm][Ee]\.md$/i,
			selector: "article",
		},
		{
			pattern: /https?:\/\/github.com\/[^\/]+\/[^\/]+\/(issues|pull)\/\d+\/*$/,
			selector: ".comment-body",
		},
		{
			pattern: /https?:\/\/github.com\/[^\/]+\/[^\/]+\/wiki\/*$/,
			selector: "#wiki-body",
		}, ];

		const selector = selectors.find((
		{
			pattern
		}) => pattern.test(url))?.selector;
		return selector;
	}

	async function inject(selector, githubToken)
	{
		const allLinks = document.querySelectorAll(`${selector} a`);
		const injectPromises = [];

		allLinks.forEach((link) =>
		{
			if (Tools.isGitHubRepo(link.href) && !link.querySelector('strong#github-stars-14151312'))
			{
				injectPromises.push(injectStars(link, githubToken));
			}
		});

		// await Promise.all(injectPromises);
    const results = await Tools.runPromisesInBatches(injectPromises,10,0);
    if(results[1]) {
      console.warn('停止在了 ', results[1])
    }


		const uls = Array.from(document.querySelectorAll(`${selector} ul`)).filter(ul => ul.querySelectorAll(':scope > li').length >= 2);

		if (!uls) return;

		for (const ul of uls)
		{
			sortLis(ul);
		}

		function sortLis(ul)
		{
			const lis = Array.from(ul.querySelectorAll(":scope > li"));

			lis.sort((a, b) =>
			{
				const aStars = getHighestStars(a);
				const bStars = getHighestStars(b);

				return bStars - aStars;
			});

			for (const li of lis)
			{
				ul.appendChild(li);
			}
		}

		function getHighestStars(liElement)
		{
			const clonedLiElement = liElement.cloneNode(true);

			const ulElements = clonedLiElement.querySelectorAll("ul");
			for (const ulElement of ulElements)
			{
				ulElement.remove();
			}

			const starsElements = clonedLiElement.querySelectorAll("strong#github-stars-14151312");
			let highestStars = 0;

			for (const starsElement of starsElements)
			{
				const stars = parseInt(starsElement.getAttribute("stars"));
				if (stars > highestStars)
				{
					highestStars = stars;
				}
			}

			return highestStars;
		}

		async function injectStars(link, githubToken)
		{
        			const stats  = await getStars(link.href, githubToken)

				if (!stats) return;

				const strong = document.createElement("strong");
				strong.id = "github-stars-14151312";
				strong.setAttribute("stars", stats.stars);
				strong.style.color = "#fff";
				strong.style.fontSize = "12px";
				strong.innerText = `★ ${Tools.roundNumber(stats.stars)}`;
				strong.style.backgroundColor = "#093812";
				strong.style.paddingRight = "5px";
				strong.style.paddingLeft = "5px";
				strong.style.textAlign = "center";
				strong.style.paddingBottom = "1px";
				strong.style.borderRadius = "5px";
				strong.style.marginLeft = "5px";
				link.appendChild(strong);


		}
	}


	function getStars(githubRepoURL, githubToken)
	{
		const repoName = githubRepoURL.match(/github\.com\/([^/]+\/[^/]+)/)[1];


		const cacheKey = `github_stats_${currentPageRepo}_${repoName}`;

		// 尝试从缓存获取星标数
		const statsC =  Tools.getCachedValue(cacheKey, null, CACHE_EXPIRATION);
		if (statsC !== null)
		{
			return statsC;
		}

		return gmFetch(`https://api.github.com/repos/${repoName}`,
		{
			headers:
			{
				Authorization: `Token ${githubToken}`
			},
		}).then((response) =>
		{
			const data =  response.json();
			const stats = {stars: data.stargazers_count,   forks_count: data.forks_count,
  open_issues_count: data.open_issues_count,
  created_at: data.created_at,
  pushed_at: data.pushed_at,
  archived: data.archived ,
  disabled: data.disabled ,
  };

			// 缓存星标数
			 Tools.setCachedValue(cacheKey, stats);

			return stats;

		}).catch((error) =>
		{
			console.error(`query stats for ${repoName} `,error)
		});;

	}



	function gmFetch(url, options = {})
	{
		return new Promise((resolve, reject) =>
		{
			GM_xmlhttpRequest(
			{
				method: options.method || 'GET',
				url: url,
				headers: options.headers,
				onload: function (response)
				{
					resolve(
					{
						status: response.status,
						json: () => JSON.parse(response.responseText)
					});
				},
				onerror: reject
			});
		});
	}
})();





// testGithubApi 没用 token
(function ()
{
	'use strict';


	const shortcut = 'c-g c-g';
	// Register shortcut
	VM.shortcut.register(shortcut, testGithubApi);

	const name = 'testGithubApi without token';

	// Register menu command
	const menuName = `${name} (${VM.shortcut.reprShortcut(shortcut)})`;
	GM_registerMenuCommand(menuName, testGithubApi);


  function testGithubApi(){

    const repo = 'Zaid-Ajaj/Awesome' ; // 'vuejs/awesome-vue'

    const ur = ["https://api.github.com"];
    ur.push("repos", repo)

    const url = ur.join("/")
    console.debug('testGithubApi ' + url)



    GM_xmlhttpRequest(
    {
      url,
      headers:
      {

        "Accept": "application/vnd.github.v3+json",
      },
      onload: function (xhr)
      {
        console.debug(xhr.responseText);
      }
    });
  } // end testGithubApi


})();



// printGithubStatsCache
// clearGithubStatsCache

(function ()
{
	'use strict';

   GM_registerMenuCommand("Print Stats Cache", printGithubStatsCache);
  GM_registerMenuCommand("Delete Stats Cache", clearGithubStatsCache);

  function printGithubStatsCache(){
        const keys = GM_listValues();
    console.groupCollapsed('printGithubStatsCache')
    console.log('current cache number is ', keys.length)
        keys.forEach(key => {
          console.log(key,':',GM_getValue(key))
        });
    console.groupEnd('printGithubStatsCache')
  }

  function clearGithubStatsCache(){



    let pre = prompt("输入缓存前缀,默认是 github_stats_ 包含本脚本创建的所有统计缓存。特定repo页面上生成的统计缓存,前缀格式是 github_stats_<owner>/<name> ");
    if(!pre) pre='github_stats_'



    const keys = GM_listValues(); let n = 0;

    console.groupCollapsed('printGithubStatsCache for '+pre)
        keys.forEach(key => {
          if(key.startsWith(pre)){
            console.log(key,':',GM_getValue(key)); n++;
          }
        });
    console.log('cache number is ', n)
    console.groupEnd('printGithubStatsCache for '+pre)



    const sure = prompt("相关缓存已打印。确认要删除吗?请输入1");
    if(sure!=='1') return

        keys.forEach(key => {
          if(key.startsWith(pre))
            GM_deleteValue(key);
        });
  }

})();




// setGitHubToken

(async function ()
{
	'use strict';

	async function setGitHubToken()
	{
		const githubToken = GM_getValue("githubToken", "");

		const token = prompt(githubToken || "Please enter your GitHub Personal Access Token:");
		if (token)
		{

			// 验证 token
			gmFetch(`https://api.github.com/user`,
			{
				headers:
				{
					Authorization: `Bearer ${token}`
				},
			}).then((response) =>
			{

				if (response.status !== 200)
				{
					console.warn("Invalid GitHub token. Please update it in the script settings.");
					return;
				}
				console.log('valid github token')

				GM_setValue("githubToken", token);

				alert("GitHub token has been set. Refresh the page to see the changes.");

			}).catch((error) =>
			{
				alert(error)
			});

		}


	}
	GM_registerMenuCommand("Set GitHub Token and test it", setGitHubToken);


})();