diff --git a/src/actions/actions-app.ts b/src/actions/actions-app.ts new file mode 100644 index 0000000..8b6564e --- /dev/null +++ b/src/actions/actions-app.ts @@ -0,0 +1,89 @@ +// 1. get the repo's contributors list +// 2. generate svg +// 3. save to local +// 4. 根据仓库地址,会保存头像在本地,如果通过actions 触发后,通过对比,本地存在头像则不会再更新 +// 4.1 第4点中,bug:如果用户更新,则还是旧头像。出现用户头像,除非用头像生成 md5 存储作为指纹 +// import { http } from 'node:http' + +import {writeFile} from 'node:fs/promises'; +import {chunk} from 'lodash'; +import {svgStart, svgEnd, asyncHandleUsersSVG, asyncHandlerUserDefsSVG} from './svg'; +import {UserConfig, UserItem} from '../types'; +import {getOwnerRepo} from '../utils'; + +import config from '../../config' +import console from 'node:console'; + +// global constants +const SVG_WIDTH = 800; +const SVG_HEIGHT = 370; +const FONT_SIZE = 0 +const BASE_SIZE = 100 + +export const generateUserListSVG = async (userList: UserItem[], config: UserConfig) => { + + // split => two-dimensional array + let splitList: UserItem[] | UserItem[][] = userList + + const svgWidth = config.width || SVG_WIDTH + const svgHeight = config.height || SVG_HEIGHT + const baseSize = config.size || BASE_SIZE + const fontSize = config.fontSize || FONT_SIZE + + const isRadius = config.isRadius !== false // default need radius => 50%; + const outSize = fontSize + baseSize + const oneRowMax = Math.floor(svgWidth / outSize) + + // split new array + if (userList.length > oneRowMax) { + splitList = chunk(userList, oneRowMax) + } + + const svgConfig = { + baseSize, + fontSize, + oneRowMax, + outSize, + svgWidth, + svgHeight, + isRadius, + } + + // radius + if (config.isRadius === undefined || config.isRadius === true) { + return `${svgStart(svgWidth, svgHeight)} + + ${await asyncHandlerUserDefsSVG(splitList, svgConfig)} + + +${await asyncHandleUsersSVG(splitList, svgConfig)} +${svgEnd()} + ` + } else if (!config.isRadius) { + const userBlockData = await asyncHandleUsersSVG(splitList, svgConfig) + return `${svgStart(svgWidth, svgHeight)} + ${userBlockData} +${svgEnd()} + ` + } + return '' +} + +export const saveSVG = async (ownerRepo: string, userConfig: UserConfig, repoUserList: UserItem[]) => { + console.log('saveSVG=>', ownerRepo, repoUserList.length) + const {owner, repo} = getOwnerRepo && getOwnerRepo(ownerRepo) || {} + if (!owner || !repo) { + console.error('Invalid repo address:', ownerRepo) + return + } + try { + const svgStr = await generateUserListSVG(repoUserList, userConfig) + const filename = `./repos/${owner}/${repo}.svg` + await writeFile(filename, svgStr, {encoding: 'utf-8'}) + } catch (error) { + console.error('err=>', error) + } +} + +// saveSVG('veaba/contributors',listTen) +// saveSVG('vuejs-translations/docs-zh-cn',listTen) diff --git a/src/actions/actions-github.ts b/src/actions/actions-github.ts new file mode 100644 index 0000000..a182a0d --- /dev/null +++ b/src/actions/actions-github.ts @@ -0,0 +1,63 @@ +import axios from "axios" + +import { createWriteStream } from 'node:fs' +import { resolve } from 'path' +import { UserConfig, UserItem } from "../types" +import { sortBy } from 'lodash' +import { data } from '../../tests/mock' +import { getTotalList, getOwnerRepo } from "../utils" + + + + +/** + * get contributors avatar to public/avatars + * + * 1、TODO check local has been save ? + * + * 2、TODO 通过存在,则通过 md5 判断,一致则略过 + * + * 3、TODO 不一致则拉取 + * + * 4、TODO if not, get remote data and save to public/avatars + * + * 5、TODO 存储一份本地 avatars 映射的 md5 list + * +*/ +export const getRepoData = async (repoKey: string, repoConfig: UserConfig): Promise => { + const { owner, repo } = getOwnerRepo(repoKey) + + let repData = [] + try { + const resp = await axios.get(`http://api.github.com/repos/${repoKey}/stats/contributors`) + if (resp?.data) { + repData = resp.data || [] + } + } catch (err) { + console.error('get repo stats contributors err=>') + } + repData = getTotalList(data, repoConfig) + + const sortList = sortBy(repData, (item) => -item.total) + return sortList +} + + +export const downloadAvatar = async (userItem: UserItem) => { + try { + const resp = await axios.get(`https://avatars.githubusercontent.com/u/${userItem.id}?v=4`, { + responseType: "stream" + }) + + if (resp?.status === 200) { + const writer = createWriteStream(resolve(__dirname, `../public/avatars/${userItem.id}.jpg`),) + resp.data.pipe(writer) + return new Promise((resolve, reject) => { + writer.on('finish', resolve) + writer.on('error', reject) + }) + } + } catch (err) { + console.error('err=>', err) + } +} diff --git a/src/actions/actions-image.ts b/src/actions/actions-image.ts new file mode 100644 index 0000000..df3f180 --- /dev/null +++ b/src/actions/actions-image.ts @@ -0,0 +1,5 @@ +import { readFile } from 'node:fs/promises' +export const imagePathToBase64 = async (imagePath: string) => { + const imageBuffer = await readFile(imagePath) + return imageBuffer.toString('base64') +} diff --git a/src/actions/actions-index.ts b/src/actions/actions-index.ts new file mode 100644 index 0000000..4b2bc6c --- /dev/null +++ b/src/actions/actions-index.ts @@ -0,0 +1,47 @@ +// import config from '../../config' +// import { resolve } from 'path' +// import { saveSVG } from './app'; +// import { writeFile, access } from "node:fs/promises" // access +// import { downloadAvatar, getRepoData } from './github'; +// import { MD5Item, UserItem } from '../types'; +// // import { isHasFile, readMD5 } from "../utils" +// +// import md5JSON from '../../public/avatars/avatarsMD5.json' +// +// const md5s: any = md5JSON +// +// interface TypesContributors { +// repo: string; +// } +// +// const updateAvatars = async (sortList: UserItem[]) => { +// await Promise.all(sortList.map(async userItem => { +// +// const isHas = await isHasFile(resolve(__dirname, `../public/avatars/${userItem.id}.jpg`)) +// if (!isHas) { +// return await downloadAvatar(userItem) +// } +// })) +// } +// +// // export const serverStart = async () => { +// // const now = Date.now() +// // console.log('开始=>', new Date()) +// // +// // console.time('task=>') +// // const repos = Object.keys(config) +// // // const reposConfigs = Object.values(config) +// // +// // await Promise.all(repos.map(async repo => { +// // const repoConfig = config[repo] +// // const repoList = await getRepoData(repo, repoConfig) +// // await updateAvatars(repoList) +// // await saveSVG(repo, repoList) +// // })) +// // +// // console.timeEnd('task=>') +// // console.log('结束=>', new Date(), Date.now() - now) +// // +// // } +// +// // serverStart()