mirror of
https://github.com/Aviortheking/codestats-skyline.git
synced 2025-04-22 10:12:08 +00:00
feat: Add support to specify year
+ Did some cleanup for the whole code Signed-off-by: Avior <f.bouillon@aptatio.com>
This commit is contained in:
parent
abfd6d8445
commit
9a8cb6dff4
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
dist/
|
||||||
|
127
3d.scad
127
3d.scad
@ -1,5 +1,6 @@
|
|||||||
// -- TO CHANGE START -- //
|
/*- TO CHANGE START */
|
||||||
// do not chang it yourself
|
|
||||||
|
// Number of XPs in a day for each months
|
||||||
jan = [6433, 12574, 12013, 14896, 6242, 12306, 11732, 22842, 25780, 7375, 1030, 2676, 8857, 7948, 11429, 21389, 13025, 5335, 8942, 4457, 14171, 9951, 10766, 7780, 29865, 24567, 7702, 24382, 17697, 12100, 16698];
|
jan = [6433, 12574, 12013, 14896, 6242, 12306, 11732, 22842, 25780, 7375, 1030, 2676, 8857, 7948, 11429, 21389, 13025, 5335, 8942, 4457, 14171, 9951, 10766, 7780, 29865, 24567, 7702, 24382, 17697, 12100, 16698];
|
||||||
feb = [21093, 2887, 10773, 10345, 5897, 6255, 20849, 17071, 17168, 8504, 27323, 33772, 16624, 16915, 14535, 10766, 24339, 14177, 24986, 13760, 47418, 59739, 19918, 12892, 20671, 13143, 9618, 9389, 0, 0];
|
feb = [21093, 2887, 10773, 10345, 5897, 6255, 20849, 17071, 17168, 8504, 27323, 33772, 16624, 16915, 14535, 10766, 24339, 14177, 24986, 13760, 47418, 59739, 19918, 12892, 20671, 13143, 9618, 9389, 0, 0];
|
||||||
mar = [23643, 29897, 23874, 26867, 20092, 8363, 6073, 10425, 26663, 111087, 30647, 19681, 19344, 15507, 17636, 12541, 7369, 17568, 17077, 15472, 9506, 6338, 8851, 21544, 24243, 7023, 5401, 7157, 20584, 23584, 16505];
|
mar = [23643, 29897, 23874, 26867, 20092, 8363, 6073, 10425, 26663, 111087, 30647, 19681, 19344, 15507, 17636, 12541, 7369, 17568, 17077, 15472, 9506, 6338, 8851, 21544, 24243, 7023, 5401, 7157, 20584, 23584, 16505];
|
||||||
@ -12,76 +13,108 @@ sep = [8156, 14355, 7410, 7958, 5638, 1974, 7015, 3924, 559, 8996, 19943, 28816,
|
|||||||
oct = [7109, 11067, 13580, 24341, 10774, 11742, 19973, 23626, 30664, 9417, 16783, 9260, 14108, 9489, 4830, 28002, 21885, 16954, 15868, 16902, 19634, 22134, 24960, 11625, 10090, 13084, 20795, 8160, 11223, 5578, 1741];
|
oct = [7109, 11067, 13580, 24341, 10774, 11742, 19973, 23626, 30664, 9417, 16783, 9260, 14108, 9489, 4830, 28002, 21885, 16954, 15868, 16902, 19634, 22134, 24960, 11625, 10090, 13084, 20795, 8160, 11223, 5578, 1741];
|
||||||
nov = [0, 13537, 30169, 11276, 6862, 13438, 10021, 14033, 11938, 10941, 3634, 23317, 33661, 22821, 30040, 13944, 13980, 10466, 8872, 9020, 10166, 37281, 31896, 15089, 15184, 14277, 8070, 13321, 11396, 9223];
|
nov = [0, 13537, 30169, 11276, 6862, 13438, 10021, 14033, 11938, 10941, 3634, 23317, 33661, 22821, 30040, 13944, 13980, 10466, 8872, 9020, 10166, 37281, 31896, 15089, 15184, 14277, 8070, 13321, 11396, 9223];
|
||||||
dec = [8826, 11600, 5676, 8647, 8214, 26350, 12609, 9070, 21900, 17233, 11803, 17161, 12652, 2534, 5066, 26487, 27623, 21798, 17840, 11337, 3662, 9396, 16589, 242, 0, 0, 2674, 1892, 98, 10846, 9384];
|
dec = [8826, 11600, 5676, 8647, 8214, 26350, 12609, 9070, 21900, 17233, 11803, 17161, 12652, 2534, 5066, 26487, 27623, 21798, 17840, 11337, 3662, 9396, 16589, 242, 0, 0, 2674, 1892, 98, 10846, 9384];
|
||||||
|
|
||||||
|
// the maximum number of XPs in a day
|
||||||
max = 111087;
|
max = 111087;
|
||||||
|
|
||||||
|
// the minimum number of XPs in a day
|
||||||
min = 0;
|
min = 0;
|
||||||
|
|
||||||
|
// Left aligned text for the username
|
||||||
text = "Aviortheking";
|
text = "Aviortheking";
|
||||||
// -- TO CHANGE END -- //
|
|
||||||
|
|
||||||
|
// Right aligned text for the year
|
||||||
|
year = "2021";
|
||||||
|
/* TO CHANGE END -*/
|
||||||
|
|
||||||
|
// Define the maximum height
|
||||||
maxHeight = 100;
|
maxHeight = 100;
|
||||||
|
|
||||||
|
// Define the spacing between values
|
||||||
spacing = 0;
|
spacing = 0;
|
||||||
|
|
||||||
socleHeight = 20;
|
// Define the base height
|
||||||
textHeight = 10;
|
baseHeight = 20;
|
||||||
|
|
||||||
|
// Define the text height
|
||||||
|
textHeight = baseHeight / 2;
|
||||||
|
|
||||||
|
// define the minimum value to be before displaying
|
||||||
minValue = 0;
|
minValue = 0;
|
||||||
|
|
||||||
|
barSize = 10;
|
||||||
|
|
||||||
|
// 33 = 31 days + 2 border)
|
||||||
|
baseWidth = barSize * 33;
|
||||||
|
|
||||||
|
// 14 = 12 months + 2 border
|
||||||
|
baseLength = barSize * 14;
|
||||||
|
|
||||||
|
|
||||||
|
// Function that generate a whole month
|
||||||
module generateMonth(month, offset = 0) {
|
module generateMonth(month, offset = 0) {
|
||||||
|
|
||||||
|
// loop through each days for the month
|
||||||
for (index = [0 : len(month) -1 ]) {
|
for (index = [0 : len(month) -1 ]) {
|
||||||
|
|
||||||
|
// get the XP
|
||||||
it = month[index];
|
it = month[index];
|
||||||
translate([10 + (10 + spacing) * index,10 + (10 + spacing) * offset, socleHeight - 1]) {
|
|
||||||
cube([10,10,it < minValue ? 0 : it * maxHeight / max + 1]);
|
// Render the bar
|
||||||
|
translate([
|
||||||
|
barSize + (barSize + spacing) * index,
|
||||||
|
barSize + (barSize + spacing) * offset,
|
||||||
|
baseHeight - 1 // put it in the bottom part to make sure they are one
|
||||||
|
]) {
|
||||||
|
cube([
|
||||||
|
barSize,
|
||||||
|
barSize,
|
||||||
|
it < minValue ? 0 : it * maxHeight / max + 1 // make it higher for the reason of the comment above
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// merge everyting
|
||||||
union() {
|
union() {
|
||||||
|
// remove part of bottom with text
|
||||||
difference() {
|
difference() {
|
||||||
cube([330, 140, socleHeight]);
|
|
||||||
translate([5,1.9,(socleHeight - textHeight) / 2]) {
|
cube([baseWidth, baseLength, baseHeight]);
|
||||||
rotate([90,0,0]) {
|
|
||||||
|
// if year is specified
|
||||||
|
if (len(year) > 0) {
|
||||||
|
|
||||||
|
// move the year to the right
|
||||||
|
translate([baseWidth - 5, 1.9, (baseHeight - textHeight) / 2]) {
|
||||||
|
|
||||||
|
// rotate it
|
||||||
|
rotate([90, 0, 0]) {
|
||||||
|
|
||||||
|
// extrude the base
|
||||||
|
linear_extrude(2)
|
||||||
|
text(year, size=textHeight, halign="right");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// move the username to the left
|
||||||
|
translate([5, 1.9, (baseHeight - textHeight) / 2]) {
|
||||||
|
|
||||||
|
// rotate it
|
||||||
|
rotate([90, 0, 0]) {
|
||||||
|
|
||||||
|
// extrude the base
|
||||||
linear_extrude(2)
|
linear_extrude(2)
|
||||||
text(text, size=textHeight);
|
text(text, size=textHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (monthIndex = [ 0 : 11 ]) {
|
// Generate each months :D
|
||||||
if (monthIndex == 0) {
|
months = [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec];
|
||||||
generateMonth(jan, monthIndex);
|
|
||||||
}
|
|
||||||
if (monthIndex == 1) {
|
|
||||||
generateMonth(feb, monthIndex);
|
|
||||||
}
|
|
||||||
if (monthIndex == 2) {
|
|
||||||
generateMonth(mar, monthIndex);
|
|
||||||
}
|
|
||||||
if (monthIndex == 3) {
|
|
||||||
generateMonth(apr, monthIndex);
|
|
||||||
}
|
|
||||||
if (monthIndex == 4) {
|
|
||||||
generateMonth(may, monthIndex);
|
|
||||||
}
|
|
||||||
if (monthIndex == 5) {
|
|
||||||
generateMonth(jun, monthIndex);
|
|
||||||
}
|
|
||||||
if (monthIndex == 6) {
|
|
||||||
generateMonth(jul, monthIndex);
|
|
||||||
}
|
|
||||||
if (monthIndex == 7) {
|
|
||||||
generateMonth(aug, monthIndex);
|
|
||||||
}
|
|
||||||
if (monthIndex == 8) {
|
|
||||||
generateMonth(sep, monthIndex);
|
|
||||||
}
|
|
||||||
if (monthIndex == 9) {
|
|
||||||
generateMonth(oct, monthIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (monthIndex == 10) {
|
for (monthIndex = [ 0 : 11 ]) {
|
||||||
generateMonth(nov, monthIndex);
|
generateMonth(months[monthIndex], monthIndex);
|
||||||
}
|
|
||||||
if (monthIndex == 11) {
|
|
||||||
generateMonth(dec, monthIndex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,12 @@
|
|||||||
#########
|
#########
|
||||||
# openscad doesn't seems to work on Alpine Linux :(
|
# openscad doesn't seems to work on Alpine Linux :(
|
||||||
# FROM docker.io/node:alpine as IMAGE
|
# FROM docker.io/node:alpine as IMAGE
|
||||||
FROM docker.io/node:latest as IMAGE
|
FROM docker.io/node:slim as IMAGE
|
||||||
|
|
||||||
# External deps (for node-gyp add: "python3 make g++")
|
# External deps (for node-gyp add: "python3 make g++")
|
||||||
# git is used to install the npm packages with git deps
|
# git is used to install the npm packages with git deps
|
||||||
RUN apt-get update &&\
|
RUN apt-get update &&\
|
||||||
apt-get install git openscad -y
|
apt-get install openscad -y
|
||||||
|
|
||||||
# run as non root user
|
# run as non root user
|
||||||
USER node
|
USER node
|
||||||
|
12
README.md
12
README.md
@ -20,6 +20,18 @@ https://codestats-skyline.avior.me/?username=Aviortheking
|
|||||||
|
|
||||||
It will generate and allows you to download the generated STL!
|
It will generate and allows you to download the generated STL!
|
||||||
|
|
||||||
|
### render a specific year
|
||||||
|
|
||||||
|
You can also render a specific year!
|
||||||
|
|
||||||
|
simply add the year to the URL
|
||||||
|
|
||||||
|
```
|
||||||
|
https://codestats-skyline.avior.me/?username=Aviortheking&year=2021
|
||||||
|
```
|
||||||
|
|
||||||
|
and voilà!
|
||||||
|
|
||||||
## Deploy the tool yourself!
|
## Deploy the tool yourself!
|
||||||
|
|
||||||
You can use the Dockerfile to deploy the tool yourself!
|
You can use the Dockerfile to deploy the tool yourself!
|
||||||
|
21
index.ts
21
index.ts
@ -5,22 +5,35 @@ import { generateThingy } from './main'
|
|||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
app.get('/', async (req, res) => {
|
app.get('/', async (req, res) => {
|
||||||
const { username } = req.query
|
|
||||||
|
|
||||||
|
// Fetch the query
|
||||||
|
const { username, year } = req.query as Record<string, string>
|
||||||
|
|
||||||
|
// if no username is sent in the request return this simple HTML lul
|
||||||
if (!username) {
|
if (!username) {
|
||||||
res.status(400).send('Error: missing username parameter in query')
|
res.status(400).send('Error: missing username parameter in query')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const bfr = await generateThingy(username as string)
|
// transform the year intoo a number
|
||||||
|
const yearN = year ? isNaN(parseInt(year)) ? undefined : parseInt(year) : undefined
|
||||||
|
|
||||||
res.setHeader('Content-Disposition', `attachment; filename=${username}.stl`)
|
// Generate the STL
|
||||||
res.status(200).send(bfr)
|
const bfr = await generateThingy(username, yearN)
|
||||||
|
|
||||||
|
// force download the STL with a good name
|
||||||
|
res
|
||||||
|
.setHeader('Content-Disposition', `attachment; filename=${username}-${year ? year : 'full'}.stl`)
|
||||||
|
.status(200)
|
||||||
|
.send(bfr)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Create the server
|
||||||
const server = http
|
const server = http
|
||||||
.createServer(app)
|
.createServer(app)
|
||||||
|
|
||||||
|
// start the server on the specified port or the 3000
|
||||||
const port = process.env.PORT ? parseInt(process.env.PORT) : 3000
|
const port = process.env.PORT ? parseInt(process.env.PORT) : 3000
|
||||||
server.listen(port, '0.0.0.0')
|
server.listen(port, '0.0.0.0')
|
||||||
|
|
||||||
console.log(`Server started on port ${port}!`)
|
console.log(`Server started on port ${port}!`)
|
69
main.ts
69
main.ts
@ -2,7 +2,7 @@ import { objectLoop, objectValues } from '@dzeio/object-util'
|
|||||||
import fetch from 'node-fetch'
|
import fetch from 'node-fetch'
|
||||||
import { promises as fs } from 'fs'
|
import { promises as fs } from 'fs'
|
||||||
import { CodeStatsJSON } from './interfaces'
|
import { CodeStatsJSON } from './interfaces'
|
||||||
// @ts-expect-error no typing for this lib
|
// @ts-expect-error no typing available for this lib
|
||||||
import nodescad from 'nodescad'
|
import nodescad from 'nodescad'
|
||||||
|
|
||||||
const months = [
|
const months = [
|
||||||
@ -30,14 +30,32 @@ async function exists(path: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const generateThingy = async (username: string) => {
|
export const generateThingy = async (username: string, filterYear?: number): Promise<Buffer | null> => {
|
||||||
|
|
||||||
const res = await fetch(`https://codestats.net/api/users/${username}`)
|
// Start generation
|
||||||
|
const now = new Date()
|
||||||
|
console.log(`Generating for ${username} (year = ${filterYear})`)
|
||||||
|
|
||||||
|
// Fetch from code::stats the informations (currently no cache is used, but is too much requests are sent one will be put)
|
||||||
|
const res = await fetch(`https://codestats.net/api/users/${username}`, {
|
||||||
|
headers: {
|
||||||
|
// Identify the software
|
||||||
|
'User-Agent': '@aviortheking/codestats-skyline'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
console.error(`Could not generate STL, Code::Stats is being mean to me (statusCode: ${res.status})`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response from Code::Stats
|
||||||
const data = await res.json() as CodeStatsJSON
|
const data = await res.json() as CodeStatsJSON
|
||||||
|
|
||||||
|
// object that will contain aggregated values
|
||||||
const correctedData: Record<string, Array<number>> = {}
|
const correctedData: Record<string, Array<number>> = {}
|
||||||
|
|
||||||
|
// Fill the arrays
|
||||||
for (const month of months) {
|
for (const month of months) {
|
||||||
if (month === '') {
|
if (month === '') {
|
||||||
continue
|
continue
|
||||||
@ -45,50 +63,58 @@ export const generateThingy = async (username: string) => {
|
|||||||
correctedData[month] = Array(31).fill(0)
|
correctedData[month] = Array(31).fill(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loop through each dates
|
||||||
objectLoop(data.dates, (value, key) => {
|
objectLoop(data.dates, (value, key) => {
|
||||||
|
// get the year-month-day from the key
|
||||||
const [yearTxt, monthTxt, dayTxt] = key.split('-')
|
const [yearTxt, monthTxt, dayTxt] = key.split('-')
|
||||||
// if we want to add a way to filter by year we can by using the `_` var
|
const [year, month, day] = [parseInt(yearTxt), parseInt(monthTxt), parseInt(dayTxt)]
|
||||||
const [_, month, day] = [parseInt(yearTxt), parseInt(monthTxt), parseInt(dayTxt)]
|
|
||||||
if (!(months[month] in correctedData)) {
|
// if there is year filtering and it is not the correct year, filter out
|
||||||
correctedData[months[month]] = []
|
if (filterYear && filterYear !== year) {
|
||||||
}
|
return
|
||||||
if (!(day in correctedData[months[month]])) {
|
|
||||||
correctedData[months[month]][day] = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add XP points to the new object
|
||||||
correctedData[months[month]][day] += value
|
correctedData[months[month]][day] += value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// get the maximum value from the list
|
||||||
const max = objectValues(correctedData).reduce((p, c) => {
|
const max = objectValues(correctedData).reduce((p, c) => {
|
||||||
const monthMax = Math.max(...c)
|
const monthMax = Math.max(...c.slice(1))
|
||||||
return monthMax > p ? monthMax : p
|
return monthMax > p ? monthMax : p
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
|
// get the minimum value from the list
|
||||||
const min = objectValues(correctedData).reduce((p, c) => {
|
const min = objectValues(correctedData).reduce((p, c) => {
|
||||||
const monthMax = Math.min(...c)
|
const monthMax = Math.min(...c.slice(1))
|
||||||
return monthMax < p ? monthMax : p
|
return monthMax < p ? monthMax : p
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
|
// prepare the file replacement
|
||||||
let final = ''
|
let final = ''
|
||||||
|
|
||||||
|
// Add each months
|
||||||
objectLoop(correctedData, (value, key) => {
|
objectLoop(correctedData, (value, key) => {
|
||||||
final += `${key} = [${value.slice(1).join(', ')}];\n`
|
final += `${key} = [${value.slice(1).join(', ')}];\n`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Add additionnal informations for generation
|
||||||
final += `max = ${max};\n`
|
final += `max = ${max};\n`
|
||||||
final += `min = ${min};\n`
|
final += `min = ${min};\n`
|
||||||
final += `text = "${username}";`
|
final += `text = "${username}";\n`
|
||||||
|
final += `year = ${filterYear};\n`
|
||||||
|
|
||||||
console.log('Output:')
|
// Fetch the CAD file
|
||||||
console.log(final)
|
|
||||||
|
|
||||||
// handle prod file
|
|
||||||
let cadPath = './3d.scad'
|
let cadPath = './3d.scad'
|
||||||
if (!await exists(cadPath)) {
|
if (!await exists(cadPath)) {
|
||||||
cadPath = '../3d.scad'
|
cadPath = '../3d.scad'
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = await fs.readFile(cadPath, 'utf-8')
|
const file = await fs.readFile(cadPath, 'utf-8')
|
||||||
const modified = file.replace(/\/\/.+\/\//gs, final)
|
|
||||||
// await fs.writeFile('output.scad', file.replace(/\/\/.+\/\//gs, final))
|
|
||||||
|
|
||||||
|
// Modify the file content with our previous values
|
||||||
|
const modified = file.replace(/\/\*-.+-\*\//gs, final)
|
||||||
|
|
||||||
|
// Render the file to an STL with OpenSCAD
|
||||||
const stl = await new Promise<any>((res, rej) => {
|
const stl = await new Promise<any>((res, rej) => {
|
||||||
nodescad.render({
|
nodescad.render({
|
||||||
input: modified
|
input: modified
|
||||||
@ -102,5 +128,8 @@ export const generateThingy = async (username: string) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log(`Finished generating for ${username} (year = ${filterYear}, timeToGenerate = ${new Date().getTime() - now.getTime()}ms)`)
|
||||||
|
|
||||||
|
// return the buffer
|
||||||
return stl.buffer as Buffer
|
return stl.buffer as Buffer
|
||||||
}
|
}
|
||||||
|
5302
package-lock.json
generated
5302
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -7,17 +7,17 @@
|
|||||||
"start": "node dist/index.js"
|
"start": "node dist/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dzeio/config": "^1.1.8",
|
|
||||||
"@dzeio/object-util": "^1.4.2",
|
"@dzeio/object-util": "^1.4.2",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"node-fetch": "^2",
|
"node-fetch": "^2",
|
||||||
"nodescad": "^1.1.0",
|
"nodescad": "^1.1.0"
|
||||||
"typescript": "^4.9.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@dzeio/config": "^1.1.8",
|
||||||
"@types/express": "^4.17.14",
|
"@types/express": "^4.17.14",
|
||||||
"@types/node-fetch": "^2",
|
"@types/node-fetch": "^2",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"ts-node-dev": "^2.0.0"
|
"ts-node-dev": "^2.0.0",
|
||||||
|
"typescript": "^4.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user