Github repos stats

Load some stats for repo list, and display star counts for GitHub repositories. Please config github token first.

  1. // ==UserScript==
  2. // @name Github repos stats
  3. // @namespace Violentmonkey Scripts
  4. // @description Load some stats for repo list, and display star counts for GitHub repositories. Please config github token first.
  5. // @thank https://github.com/sir-kokabi/github-sorter
  6. // @match https://github.com/*
  7. // @version 1.2
  8.  
  9. // @grant GM_registerMenuCommand
  10. // @grant GM_xmlhttpRequest
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @grant GM_deleteValue
  14. // @grant GM_listValues
  15. //
  16. // @require https://cdn.jsdelivr.net/npm/@violentmonkey/shortcut@1
  17. // @license MIT
  18. // ==/UserScript==
  19.  
  20. // 设置缓存过期时间为 10天 : 1 小时(3600000 毫秒)*24*10
  21. const CACHE_EXPIRATION = 3600000 * 24 * 10;
  22.  
  23.  
  24.  
  25.  
  26. function myFetch(url, options = {})
  27. {
  28. return new Promise((resolve, reject) =>
  29. {
  30. GM_xmlhttpRequest(
  31. {
  32. method: options.method || 'GET',
  33. url: url,
  34. headers: options.headers,
  35. onload: function (response)
  36. {
  37. resolve(
  38. {
  39. status: response.status,
  40. json: () => JSON.parse(response.responseText)
  41. });
  42. },
  43. onerror: reject
  44. });
  45. });
  46. }
  47.  
  48.  
  49.  
  50. (async function main()
  51. {
  52. 'use strict';
  53.  
  54.  
  55. // 注册菜单命令
  56. GM_registerMenuCommand("Do Work: Query and Show Stats", showStats);
  57.  
  58. const currentPageRepo = (function extractRepoFromGitHubUrl(url) {
  59. // 1. 检查 URL 是否是 GitHub 链接
  60. if (!url.includes('github.com')) {
  61. return null;
  62. }
  63.  
  64. // 2. 使用正则表达式提取 repo 名
  65. const match = url.match(/github\.com\/([^/]+)\/([^/#?]+)/);
  66. if (match) {
  67. return match[1] +'/'+ match[2]
  68. }
  69.  
  70. return null;
  71. })(location.href);
  72.  
  73.  
  74.  
  75. function showStats()
  76. {
  77. const githubToken = GM_getValue("githubToken", "");
  78. if (!githubToken)
  79. {
  80. console.warn("GitHub token not set. Please set it in the script settings.");
  81. return;
  82. }
  83.  
  84.  
  85.  
  86. const url = window.location.href;
  87. const selector = getSelector(url);
  88.  
  89. if (!selector) return;
  90.  
  91.  
  92. inject(selector, githubToken);
  93.  
  94. } // end showStats
  95.  
  96.  
  97. const Tools = {
  98.  
  99. // Promise.all 运行太多的 promises? 分批运行,第一批都成功,才运行下一批
  100. // https://gist.github.com/scil/15d63220521808ba7839f423e4d8a784
  101. runPromisesInBatches:async function(promises, batchSize = 50,startIndex = 0) {
  102.  
  103. let results = [];
  104. let errorIndex=null;
  105.  
  106. while (startIndex < promises.length) {
  107. const batch = promises.slice(startIndex, startIndex + batchSize);
  108. try {
  109. const batchResults = await Promise.all(batch);
  110. results.push(...batchResults);
  111. // console.log(batchResults)
  112. startIndex += batchSize;
  113. } catch (error) {
  114. errorIndex = startIndex
  115. console.error(`Error processing batch starting at index ${startIndex}:`, error);
  116. // 处理错误,例如记录错误信息或停止执行
  117. break; // 停止执行,避免后续批次继续执行
  118. }
  119. }
  120.  
  121. return [results, errorIndex];
  122. }
  123. ,
  124. isGitHubRepo:function(url)
  125. {
  126. const githubRegex = /^https:\/\/github\.com\/[^/]+\/[^/#]+$/;
  127. return githubRegex.test(url);
  128. },
  129.  
  130. roundNumber:function(number)
  131. {
  132. if (number < 1000) return number;
  133.  
  134. const suffixes = ['', 'k', 'M', 'B', 'T'];
  135.  
  136. const suffixIndex = Math.floor(Math.log10(number) / 3);
  137. const scaledNumber = number / Math.pow(10, suffixIndex * 3);
  138.  
  139. const formattedNumber = scaledNumber % 1 === 0 ? scaledNumber.toFixed(0) : scaledNumber.toFixed(1);
  140.  
  141. return `${formattedNumber}${suffixes[suffixIndex]}`;
  142. },
  143. // 缓存包装函数
  144. getCachedValue:function(key, defaultValue, expirationTime)
  145. {
  146. const cachedData = GM_getValue(key);
  147. if (cachedData)
  148. {
  149. const
  150. {
  151. value,
  152. timestamp
  153. } = JSON.parse(cachedData);
  154. if (Date.now() - timestamp < expirationTime)
  155. {
  156. return value;
  157. }
  158. }
  159. return defaultValue;
  160. },
  161.  
  162. setCachedValue:function(key, value)
  163. {
  164. const data = JSON.stringify(
  165. {
  166. value: value,
  167. timestamp: Date.now()
  168. });
  169. GM_setValue(key, data);
  170. },
  171. }
  172.  
  173.  
  174. function getSelector(url)
  175. {
  176. const selectors = [
  177. {
  178. pattern: /https?:\/\/github.com\/[^\/]+\/[^\/]+\/*$/,
  179. // selector: "#readme",
  180. selector: '.markdown-body',
  181. },
  182. {
  183. pattern: /https?:\/\/github.com\/.*\/[Rr][Ee][Aa][Dd][Mm][Ee]\.md$/i,
  184. selector: "article",
  185. },
  186. {
  187. pattern: /https?:\/\/github.com\/[^\/]+\/[^\/]+\/(issues|pull)\/\d+\/*$/,
  188. selector: ".comment-body",
  189. },
  190. {
  191. pattern: /https?:\/\/github.com\/[^\/]+\/[^\/]+\/wiki\/*$/,
  192. selector: "#wiki-body",
  193. }, ];
  194.  
  195. const selector = selectors.find((
  196. {
  197. pattern
  198. }) => pattern.test(url))?.selector;
  199. return selector;
  200. }
  201.  
  202. async function inject(selector, githubToken)
  203. {
  204. const allLinks = document.querySelectorAll(`${selector} a`);
  205. const injectPromises = [];
  206.  
  207. allLinks.forEach((link) =>
  208. {
  209. if (Tools.isGitHubRepo(link.href) && !link.querySelector('strong#github-stars-14151312'))
  210. {
  211. injectPromises.push(injectStars(link, githubToken));
  212. }
  213. });
  214.  
  215. // await Promise.all(injectPromises);
  216. const results = await Tools.runPromisesInBatches(injectPromises,10,0);
  217. if(results[1]) {
  218. console.warn('停止在了 ', results[1])
  219. }
  220.  
  221.  
  222. const uls = Array.from(document.querySelectorAll(`${selector} ul`)).filter(ul => ul.querySelectorAll(':scope > li').length >= 2);
  223.  
  224. if (!uls) return;
  225.  
  226. for (const ul of uls)
  227. {
  228. sortLis(ul);
  229. }
  230.  
  231. function sortLis(ul)
  232. {
  233. const lis = Array.from(ul.querySelectorAll(":scope > li"));
  234.  
  235. lis.sort((a, b) =>
  236. {
  237. const aStars = getHighestStars(a);
  238. const bStars = getHighestStars(b);
  239.  
  240. return bStars - aStars;
  241. });
  242.  
  243. for (const li of lis)
  244. {
  245. ul.appendChild(li);
  246. }
  247. }
  248.  
  249. function getHighestStars(liElement)
  250. {
  251. const clonedLiElement = liElement.cloneNode(true);
  252.  
  253. const ulElements = clonedLiElement.querySelectorAll("ul");
  254. for (const ulElement of ulElements)
  255. {
  256. ulElement.remove();
  257. }
  258.  
  259. const starsElements = clonedLiElement.querySelectorAll("strong#github-stars-14151312");
  260. let highestStars = 0;
  261.  
  262. for (const starsElement of starsElements)
  263. {
  264. const stars = parseInt(starsElement.getAttribute("stars"));
  265. if (stars > highestStars)
  266. {
  267. highestStars = stars;
  268. }
  269. }
  270.  
  271. return highestStars;
  272. }
  273.  
  274. async function injectStars(link, githubToken)
  275. {
  276. const stats = await getStars(link.href, githubToken)
  277.  
  278. if (!stats) return;
  279.  
  280. const strong = document.createElement("strong");
  281. strong.id = "github-stars-14151312";
  282. strong.setAttribute("stars", stats.stars);
  283. strong.style.color = "#fff";
  284. strong.style.fontSize = "12px";
  285. strong.innerText = `★ ${Tools.roundNumber(stats.stars)}`;
  286. strong.style.backgroundColor = "#093812";
  287. strong.style.paddingRight = "5px";
  288. strong.style.paddingLeft = "5px";
  289. strong.style.textAlign = "center";
  290. strong.style.paddingBottom = "1px";
  291. strong.style.borderRadius = "5px";
  292. strong.style.marginLeft = "5px";
  293. link.appendChild(strong);
  294.  
  295.  
  296. }
  297. }
  298.  
  299.  
  300. function getStars(githubRepoURL, githubToken)
  301. {
  302. const repoName = githubRepoURL.match(/github\.com\/([^/]+\/[^/]+)/)[1];
  303.  
  304.  
  305. const cacheKey = `github_stats_${currentPageRepo}_${repoName}`;
  306.  
  307. // 尝试从缓存获取星标数
  308. const statsC = Tools.getCachedValue(cacheKey, null, CACHE_EXPIRATION);
  309. if (statsC !== null)
  310. {
  311. return statsC;
  312. }
  313.  
  314. return myFetch(`https://api.github.com/repos/${repoName}`,
  315. {
  316. headers:
  317. {
  318. Authorization: `Token ${githubToken}`
  319. },
  320. }).then((response) =>
  321. {
  322. const data = response.json();
  323. const stats = {stars: data.stargazers_count, forks_count: data.forks_count,
  324. open_issues_count: data.open_issues_count,
  325. created_at: data.created_at,
  326. pushed_at: data.pushed_at,
  327. archived: data.archived ,
  328. disabled: data.disabled ,
  329. };
  330.  
  331. // 缓存星标数
  332. Tools.setCachedValue(cacheKey, stats);
  333.  
  334. return stats;
  335.  
  336. }).catch((error) =>
  337. {
  338. console.error(`query stats for ${repoName} `,error)
  339. });;
  340.  
  341. }
  342.  
  343.  
  344. })();
  345.  
  346.  
  347.  
  348. // setGitHubToken
  349.  
  350. (async function setGitHubToken()
  351. {
  352. 'use strict';
  353.  
  354. async function setGitHubToken()
  355. {
  356. const githubToken = GM_getValue("githubToken", "");
  357.  
  358. const token = prompt(githubToken || "Please enter your GitHub Personal Access Token:");
  359. if (token)
  360. {
  361.  
  362. // 验证 token
  363. myFetch(`https://api.github.com/user`,
  364. {
  365. headers:
  366. {
  367. Authorization: `Bearer ${token}`
  368. },
  369. }).then((response) =>
  370. {
  371.  
  372. if (response.status !== 200)
  373. {
  374. console.warn("Invalid GitHub token. Please update it in the script settings.");
  375. return;
  376. }
  377. console.log('valid github token')
  378.  
  379. GM_setValue("githubToken", token);
  380.  
  381. alert("GitHub token has been set. Refresh the page to see the changes.");
  382.  
  383. }).catch((error) =>
  384. {
  385. alert(error)
  386. });
  387.  
  388. }
  389.  
  390.  
  391. }
  392. GM_registerMenuCommand("Config: Set GitHub Token and test it", setGitHubToken);
  393.  
  394.  
  395. })();
  396.  
  397.  
  398.  
  399.  
  400.  
  401. // printGithubStatsCache
  402. // clearGithubStatsCache
  403.  
  404. (function githubStatsCache()
  405. {
  406. 'use strict';
  407.  
  408. GM_registerMenuCommand("Tool: Print Stats Cache", printGithubStatsCache);
  409. GM_registerMenuCommand("Tool: Delete Stats Cache", clearGithubStatsCache);
  410.  
  411. function printGithubStatsCache(){
  412. const keys = GM_listValues();
  413. console.groupCollapsed('printGithubStatsCache')
  414. console.log('current cache number is ', keys.length)
  415. keys.forEach(key => {
  416. console.log(key,':',GM_getValue(key))
  417. });
  418. console.groupEnd('printGithubStatsCache')
  419. }
  420.  
  421. function clearGithubStatsCache(){
  422.  
  423.  
  424.  
  425. let pre = prompt("输入缓存前缀,默认是 github_stats_ 包含本脚本创建的所有统计缓存。特定repo页面上生成的统计缓存,前缀格式是 github_stats_<owner>/<name> ");
  426. if(!pre) pre='github_stats_'
  427.  
  428.  
  429.  
  430. const keys = GM_listValues(); let n = 0;
  431.  
  432. console.groupCollapsed('printGithubStatsCache for '+pre)
  433. keys.forEach(key => {
  434. if(key.startsWith(pre)){
  435. console.log(key,':',GM_getValue(key)); n++;
  436. }
  437. });
  438. console.log('cache number is ', n)
  439. console.groupEnd('printGithubStatsCache for '+pre)
  440.  
  441.  
  442.  
  443. const sure = prompt("相关缓存已打印。确认要删除吗?请输入1");
  444. if(sure!=='1') return
  445.  
  446. keys.forEach(key => {
  447. if(key.startsWith(pre))
  448. GM_deleteValue(key);
  449. });
  450. }
  451.  
  452. })();
  453.  
  454.  
  455.  
  456.  
  457.  
  458.  
  459.  
  460. // testGithubApi 没用 token
  461. (function testGithubApi()
  462. {
  463. 'use strict';
  464.  
  465.  
  466. const shortcut = 'c-g c-g';
  467. // Register shortcut
  468. VM.shortcut.register(shortcut, testGithubApi);
  469.  
  470. const name = '_Test: GithubApi without token';
  471.  
  472. // Register menu command
  473. const menuName = `${name} (${VM.shortcut.reprShortcut(shortcut)})`;
  474. GM_registerMenuCommand(menuName, testGithubApi);
  475.  
  476.  
  477. function testGithubApi(){
  478.  
  479. const repo = 'Zaid-Ajaj/Awesome' ; // 'vuejs/awesome-vue'
  480.  
  481. const ur = ["https://api.github.com"];
  482. ur.push("repos", repo)
  483.  
  484. const url = ur.join("/")
  485. console.debug('testGithubApi ' + url)
  486.  
  487.  
  488.  
  489. GM_xmlhttpRequest(
  490. {
  491. url,
  492. headers:
  493. {
  494.  
  495. "Accept": "application/vnd.github.v3+json",
  496. },
  497. onload: function (xhr)
  498. {
  499. console.debug(xhr.responseText);
  500. }
  501. });
  502. } // end testGithubApi
  503.  
  504.  
  505. })();
  506.  
  507.