// ==UserScript==
// @name Github repos stats
// @namespace Violentmonkey Scripts
// @description Load some stats for repo list, and display star counts for GitHub repositories. Please config github token first.
// @thank https://github.com/sir-kokabi/github-sorter
// @match https://github.com/*
// @version 1.2
// @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;
function myFetch(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
});
});
}
(async function main()
{
'use strict';
// 注册菜单命令
GM_registerMenuCommand("Do Work: 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 myFetch(`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)
});;
}
})();
// setGitHubToken
(async function setGitHubToken()
{
'use strict';
async function setGitHubToken()
{
const githubToken = GM_getValue("githubToken", "");
const token = prompt(githubToken || "Please enter your GitHub Personal Access Token:");
if (token)
{
// 验证 token
myFetch(`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("Config: Set GitHub Token and test it", setGitHubToken);
})();
// printGithubStatsCache
// clearGithubStatsCache
(function githubStatsCache()
{
'use strict';
GM_registerMenuCommand("Tool: Print Stats Cache", printGithubStatsCache);
GM_registerMenuCommand("Tool: 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);
});
}
})();
// testGithubApi 没用 token
(function testGithubApi()
{
'use strict';
const shortcut = 'c-g c-g';
// Register shortcut
VM.shortcut.register(shortcut, testGithubApi);
const name = '_Test: GithubApi 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
})();