start backend work

This commit is contained in:
Lee 2024-10-08 15:32:02 +01:00
parent 04ce91b459
commit aa0a0c4c16
445 changed files with 367 additions and 11413 deletions

@ -10,7 +10,7 @@ spec:
entryPoints: entryPoints:
- websecure - websecure
routes: routes:
- match: Host(`ssr.fascinated.cc`) && PathPrefix(`/api`) - match: Host(`ssr.fascinated.cc`) && PathPrefix(`/api-test`)
kind: Rule kind: Rule
middlewares: middlewares:
- name: default-headers - name: default-headers

@ -7,4 +7,4 @@ metadata:
spec: spec:
stripPrefix: stripPrefix:
prefixes: prefixes:
- "/api" - "/api-test"

@ -1,15 +1,15 @@
name: "Deploy Website" name: "Deploy Website"
on: #on:
workflow_dispatch: # workflow_dispatch:
push: # push:
branches: # branches:
- master # - master
paths: # paths:
- website/** # - website/**
- common/** # - common/**
- .gitea/kubernetes/website/** # - .gitea/kubernetes/website/**
- .gitea/workflows/deploy-website.yml # - .gitea/workflows/deploy-website.yml
jobs: jobs:
deploy: deploy:

@ -1,25 +0,0 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

@ -1,31 +0,0 @@
FROM node:20-alpine3.17
# Install pnpm globally
RUN npm install -g pnpm
ENV PNPM_HOME=/usr/local/bin
WORKDIR /app
ARG GIT_REV
ENV GIT_REV=${GIT_REV}
# Copy necessary files for installation
COPY package.json* pnpm-lock.yaml* pnpm-workspace.yaml* ./
COPY common ./common
COPY backend ./backend
# Install all dependencies (for common and backend)
RUN pnpm install
# Run in production mode
ENV NODE_ENV=production
# Build the common workspace first, then the backend
RUN pnpm --filter ...common build
RUN pnpm --filter ...backend build
# Expose the port your application runs on
EXPOSE 8080
# Command to run your app
CMD ["node", "backend/dist/main.js"]

@ -1,8 +0,0 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

@ -1,44 +0,0 @@
{
"name": "backend",
"version": "1.0.0",
"author": "fascinated7",
"license": "MIT",
"private": true,
"scripts": {
"dev": "nest start --watch --webpack webpack-hmr.config.js",
"build": "nest build",
"start": "nest start"
},
"dependencies": {
"@fastify/one-line-logger": "^2.0.0",
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/platform-fastify": "^10.4.4",
"@ssr/common": "workspace:*",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/express": "^4.17.17",
"@types/node": "^20.3.1",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"concurrently": "^9.0.1",
"eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"nodemon": "^2.0.20",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsup": "^8.3.0",
"typescript": "^5"
}
}

@ -1,12 +0,0 @@
import { Module } from "@nestjs/common";
import { AppController } from "./controller/app.controller";
import { PlayerService } from "./service/player.service";
import { PlayerController } from "./controller/player.controller";
import { AppService } from "./service/app.service";
@Module({
imports: [],
controllers: [AppController, PlayerController],
providers: [AppService, PlayerService],
})
export class AppModule {}

@ -1,15 +0,0 @@
import { Controller, Get } from "@nestjs/common";
import { AppService } from "../service/app.service";
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get("/")
getHome() {
return {
message: "ScoreSaber Reloaded API",
version: this.appService.getVersion(),
};
}
}

@ -1,12 +0,0 @@
import { Controller, Get, Param } from "@nestjs/common";
import { PlayerService } from "../service/player.service";
@Controller("/player")
export class PlayerController {
constructor(private readonly playerService: PlayerService) {}
@Get("/history/:id")
getHistory(@Param("id") id: string) {
return this.playerService.getHistory(id);
}
}

@ -1,21 +0,0 @@
import { NestFactory } from "@nestjs/core";
import { FastifyAdapter, NestFastifyApplication } from "@nestjs/platform-fastify";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter({
logger: {
transport: {
target: "@fastify/one-line-logger",
},
},
}),
{
logger: ["error", "warn", "log"],
}
);
await app.listen(8080, "0.0.0.0");
}
bootstrap();

@ -1,14 +0,0 @@
import { Injectable } from "@nestjs/common";
import { isProduction } from "@ssr/common/dist";
@Injectable()
export class AppService {
/**
* Gets the app version.
*
* @returns the app version
*/
getVersion(): string {
return `1.0.0-${isProduction() ? process.env.GIT_REV.substring(0, 7) : "dev"}`;
}
}

@ -1,16 +0,0 @@
import { Injectable } from "@nestjs/common";
@Injectable()
export class PlayerService {
/**
* Gets the statistic history for the given player
*
* @param id the id of the player
* @returns the players statistic history
*/
getHistory(id: string) {
return {
id: id,
};
}
}

@ -1,4 +0,0 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

@ -1,6 +0,0 @@
module.exports = function (options) {
return {
...options,
stats: "minimal", // This disables the full-screen mode and simplifies the output
};
};

BIN
bun.lockb Normal file

Binary file not shown.

@ -1 +0,0 @@
export * from "src/utils";

@ -1,21 +0,0 @@
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
}
}

@ -1,14 +1,13 @@
{ {
"name": "scoresaber-reloadedv3", "name": "scoresaber-reloaded",
"version": "1.0.0", "version": "1.0.0",
"workspaces": [
"projects/backend",
"projects/website",
"projects/common"
],
"scripts": { "scripts": {
"dev": "pnpm --parallel --workspace-concurrency=4 run -r dev", "dev": "bun run --filter '*' dev"
"build:website": "pnpm --filter website build",
"build:backend": "pnpm --filter backend build",
"start:website": "pnpm --filter website start",
"start:backend": "pnpm --filter backend start"
}, },
"author": "fascinated7", "author": "fascinated7",
"license": "MIT" "license": "MIT"

10721
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

@ -1,4 +0,0 @@
packages:
- "common"
- "website"
- "backend"

42
projects/backend/.gitignore vendored Normal file

@ -0,0 +1,42 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
**/*.trace
**/*.zip
**/*.tar.gz
**/*.tgz
**/*.log
package-lock.json
**/*.bun

@ -0,0 +1,19 @@
FROM imbios/bun-node AS base
# Install dependencies
FROM base AS depends
WORKDIR /app
COPY . .
RUN bun install --frozen-lockfile
# Run the app
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
COPY --from=depends /app/node_modules ./node_modules
COPY --from=depends /app/package.json* /app/bun.lockb* ./
COPY --from=depends /app/projects/backend ./projects/backend
CMD ["bun", "run", "--filter", "backend", "start"]

@ -0,0 +1,9 @@
# Backend
## Development
To start the development server run:
```bash
bun run dev
```
Open http://localhost:3000/ with your browser to see the result.

@ -0,0 +1,21 @@
{
"name": "backend",
"version": "1.0.0",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "bun run --watch src/index.ts",
"start": "bun run src/index.ts"
},
"dependencies": {
"@elysiajs/cors": "^1.1.1",
"@ssr/common": "workspace:common",
"@tqman/nice-logger": "^1.0.1",
"elysia": "latest",
"elysia-autoroutes": "^0.5.0",
"elysia-decorators": "^1.0.2"
},
"devDependencies": {
"bun-types": "latest"
},
"module": "src/index.js"
}

@ -0,0 +1,10 @@
/**
* Gets the app version.
*/
export function getAppVersion() {
if (!process.env.APP_VERSION) {
const packageJson = require("../../package.json");
process.env.APP_VERSION = packageJson.version;
}
return process.env.APP_VERSION + "-" + (process.env.GIT_REV?.substring(0, 7) ?? "dev");
}

@ -0,0 +1,13 @@
import { Controller, Get } from "elysia-decorators";
import { getAppVersion } from "../common/app-utils";
@Controller("/")
export default class AppController {
@Get()
public index() {
return {
app: "backend",
version: getAppVersion(),
};
}
}

@ -0,0 +1,53 @@
import { Elysia } from "elysia";
import cors from "@elysiajs/cors";
import { decorators } from "elysia-decorators";
import { logger } from "@tqman/nice-logger";
import AppController from "./controller/app";
const app = new Elysia();
/**
* Custom error handler
*/
app.onError({ as: "global" }, ({ code, error }) => {
// Return default error for type validation
if (code === "VALIDATION") {
return error.all;
}
let status = "status" in error ? error.status : undefined;
return {
...((status && { statusCode: status }) || { status: code }),
...(error.message != code && { message: error.message }),
timestamp: new Date().toISOString(),
};
});
/**
* Enable CORS
*/
app.use(cors());
/**
* Request logger
*/
app.use(
logger({
mode: "combined",
})
);
/**
* Controllers
*/
app.use(
decorators({
controllers: [AppController],
})
);
app.onStart(() => {
console.log("Listening on port http://localhost:8080");
});
app.listen(8080);

@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2021",
"module": "ES2022",
"moduleResolution": "node",
"types": ["bun-types"],
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}

@ -1,13 +1,17 @@
{ {
"name": "@ssr/common", "name": "@ssr/common",
"version": "1.0.0", "version": "1.0.0",
"type": "module",
"scripts": { "scripts": {
"dev": "tsup src/index.ts --watch", "dev": "tsup src/index.ts --watch",
"build": "tsup src/index.ts" "build": "tsup src/index.ts"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.7.4", "@types/node": "^22.7.4",
"tsup": "^6.5.0", "tsup": "^8",
"typescript": "^5" "typescript": "^5"
},
"dependencies": {
"ky": "^1.7.2"
} }
} }

@ -0,0 +1,49 @@
export * from "src/utils/utils";
export * from "src/utils/time-utils";
/**
* Player stuff
*/
export * from "src/types/player/player-history";
export * from "src/types/player/player-tracked-since";
export * from "src/types/player/player";
export * from "src/types/player/impl/scoresaber-player";
export * from "src/utils/player-utils";
/**
* Score stuff
*/
export * from "src/types/score/score";
export * from "src/types/score/score-sort";
export * from "src/types/score/modifier";
export * from "src/types/score/impl/scoresaber-score";
/**
* Service stuff
*/
export * from "src/service/impl/beatsaver";
export * from "src/service/impl/scoresaber";
/**
* Scoresaber Tokens
*/
export * from "src/types/token/scoresaber/score-saber-badge-token";
export * from "src/types/token/scoresaber/score-saber-difficulty-token";
export * from "src/types/token/scoresaber/score-saber-leaderboard-player-info-token";
export * from "src/types/token/scoresaber/score-saber-leaderboard-scores-page-token";
export * from "src/types/token/scoresaber/score-saber-leaderboard-token";
export * from "src/types/token/scoresaber/score-saber-metadata-token";
export * from "src/types/token/scoresaber/score-saber-player-score-token";
export * from "src/types/token/scoresaber/score-saber-player-scores-page-token";
export * from "src/types/token/scoresaber/score-saber-player-search-token";
export * from "src/types/token/scoresaber/score-saber-player-token";
export * from "src/types/token/scoresaber/score-saber-players-page-token";
export * from "src/types/token/scoresaber/score-saber-score-token";
/**
* Beatsaver Tokens
*/
export * from "src/types/token/beatsaver/beat-saver-account-token";
export * from "src/types/token/beatsaver/beat-saver-map-metadata-token";
export * from "src/types/token/beatsaver/beat-saver-map-stats-token";
export * from "src/types/token/beatsaver/beat-saver-map-token";

@ -0,0 +1,34 @@
import Service from "../service";
import { BeatSaverMapToken } from "../../types/token/beatsaver/beat-saver-map-token";
const API_BASE = "https://api.beatsaver.com";
const LOOKUP_MAP_BY_HASH_ENDPOINT = `${API_BASE}/maps/hash/:query`;
class BeatSaverService extends Service {
constructor() {
super("BeatSaver");
}
/**
* Gets the map that match the query.
*
* @param query the query to search for
* @param useProxy whether to use the proxy or not
* @returns the map that match the query, or undefined if no map were found
*/
async lookupMap(query: string): Promise<BeatSaverMapToken | undefined> {
const before = performance.now();
this.log(`Looking up map "${query}"...`);
const response = await this.fetch<BeatSaverMapToken>(LOOKUP_MAP_BY_HASH_ENDPOINT.replace(":query", query));
// Map not found
if (response == undefined) {
return undefined;
}
this.log(`Found map "${response.id}" in ${(performance.now() - before).toFixed(0)}ms`);
return response;
}
}
export const beatsaverService = new BeatSaverService();

@ -1,12 +1,12 @@
import ScoreSaberLeaderboardScoresPageToken from "@/common/model/token/scoresaber/score-saber-leaderboard-scores-page-token";
import ScoreSaberPlayerScoresPageToken from "@/common/model/token/scoresaber/score-saber-player-scores-page-token";
import { ScoreSaberPlayerSearchToken } from "@/common/model/token/scoresaber/score-saber-player-search-token";
import ScoreSaberPlayerToken from "@/common/model/token/scoresaber/score-saber-player-token";
import { ScoreSaberPlayersPageToken } from "@/common/model/token/scoresaber/score-saber-players-page-token";
import { ScoreSort } from "../../model/score/score-sort";
import Service from "../service"; import Service from "../service";
import ScoreSaberPlayer, { getScoreSaberPlayerFromToken } from "@/common/model/player/impl/scoresaber-player"; import { ScoreSaberPlayerSearchToken } from "../../types/token/scoresaber/score-saber-player-search-token";
import ScoreSaberLeaderboardToken from "@/common/model/token/scoresaber/score-saber-leaderboard-token"; import ScoreSaberPlayerToken from "../../types/token/scoresaber/score-saber-player-token";
import ScoreSaberPlayer, { getScoreSaberPlayerFromToken } from "../../types/player/impl/scoresaber-player";
import { ScoreSaberPlayersPageToken } from "../../types/token/scoresaber/score-saber-players-page-token";
import { ScoreSort } from "../../types/score/score-sort";
import ScoreSaberPlayerScoresPageToken from "../../types/token/scoresaber/score-saber-player-scores-page-token";
import ScoreSaberLeaderboardToken from "../../types/token/scoresaber/score-saber-leaderboard-token";
import ScoreSaberLeaderboardScoresPageToken from "../../types/token/scoresaber/score-saber-leaderboard-scores-page-token";
const API_BASE = "https://scoresaber.com/api"; const API_BASE = "https://scoresaber.com/api";
@ -34,16 +34,12 @@ class ScoreSaberService extends Service {
* Gets the players that match the query. * Gets the players that match the query.
* *
* @param query the query to search for * @param query the query to search for
* @param useProxy whether to use the proxy or not
* @returns the players that match the query, or undefined if no players were found * @returns the players that match the query, or undefined if no players were found
*/ */
async searchPlayers(query: string, useProxy = true): Promise<ScoreSaberPlayerSearchToken | undefined> { async searchPlayers(query: string): Promise<ScoreSaberPlayerSearchToken | undefined> {
const before = performance.now(); const before = performance.now();
this.log(`Searching for players matching "${query}"...`); this.log(`Searching for players matching "${query}"...`);
const results = await this.fetch<ScoreSaberPlayerSearchToken>( const results = await this.fetch<ScoreSaberPlayerSearchToken>(SEARCH_PLAYERS_ENDPOINT.replace(":query", query));
useProxy,
SEARCH_PLAYERS_ENDPOINT.replace(":query", query)
);
if (results === undefined) { if (results === undefined) {
return undefined; return undefined;
} }
@ -59,12 +55,12 @@ class ScoreSaberService extends Service {
* Looks up a player by their ID. * Looks up a player by their ID.
* *
* @param playerId the ID of the player to look up * @param playerId the ID of the player to look up
* @param useProxy whether to use the proxy or not * @param apiUrl the url to the API for SSR
* @returns the player that matches the ID, or undefined * @returns the player that matches the ID, or undefined
*/ */
async lookupPlayer( async lookupPlayer(
playerId: string, playerId: string,
useProxy = true apiUrl: string
): Promise< ): Promise<
| { | {
player: ScoreSaberPlayer; player: ScoreSaberPlayer;
@ -74,13 +70,13 @@ class ScoreSaberService extends Service {
> { > {
const before = performance.now(); const before = performance.now();
this.log(`Looking up player "${playerId}"...`); this.log(`Looking up player "${playerId}"...`);
const token = await this.fetch<ScoreSaberPlayerToken>(useProxy, LOOKUP_PLAYER_ENDPOINT.replace(":id", playerId)); const token = await this.fetch<ScoreSaberPlayerToken>(LOOKUP_PLAYER_ENDPOINT.replace(":id", playerId));
if (token === undefined) { if (token === undefined) {
return undefined; return undefined;
} }
this.log(`Found player "${playerId}" in ${(performance.now() - before).toFixed(0)}ms`); this.log(`Found player "${playerId}" in ${(performance.now() - before).toFixed(0)}ms`);
return { return {
player: await getScoreSaberPlayerFromToken(token), player: await getScoreSaberPlayerFromToken(apiUrl, token),
rawPlayer: token, rawPlayer: token,
}; };
} }
@ -89,14 +85,12 @@ class ScoreSaberService extends Service {
* Lookup players on a specific page * Lookup players on a specific page
* *
* @param page the page to get players for * @param page the page to get players for
* @param useProxy whether to use the proxy or not
* @returns the players on the page, or undefined * @returns the players on the page, or undefined
*/ */
async lookupPlayers(page: number, useProxy = true): Promise<ScoreSaberPlayersPageToken | undefined> { async lookupPlayers(page: number): Promise<ScoreSaberPlayersPageToken | undefined> {
const before = performance.now(); const before = performance.now();
this.log(`Looking up players on page "${page}"...`); this.log(`Looking up players on page "${page}"...`);
const response = await this.fetch<ScoreSaberPlayersPageToken>( const response = await this.fetch<ScoreSaberPlayersPageToken>(
useProxy,
LOOKUP_PLAYERS_ENDPOINT.replace(":page", page.toString()) LOOKUP_PLAYERS_ENDPOINT.replace(":page", page.toString())
); );
if (response === undefined) { if (response === undefined) {
@ -111,18 +105,12 @@ class ScoreSaberService extends Service {
* *
* @param page the page to get players for * @param page the page to get players for
* @param country the country to get players for * @param country the country to get players for
* @param useProxy whether to use the proxy or not
* @returns the players on the page, or undefined * @returns the players on the page, or undefined
*/ */
async lookupPlayersByCountry( async lookupPlayersByCountry(page: number, country: string): Promise<ScoreSaberPlayersPageToken | undefined> {
page: number,
country: string,
useProxy = true
): Promise<ScoreSaberPlayersPageToken | undefined> {
const before = performance.now(); const before = performance.now();
this.log(`Looking up players on page "${page}" for country "${country}"...`); this.log(`Looking up players on page "${page}" for country "${country}"...`);
const response = await this.fetch<ScoreSaberPlayersPageToken>( const response = await this.fetch<ScoreSaberPlayersPageToken>(
useProxy,
LOOKUP_PLAYERS_BY_COUNTRY_ENDPOINT.replace(":page", page.toString()).replace(":country", country) LOOKUP_PLAYERS_BY_COUNTRY_ENDPOINT.replace(":page", page.toString()).replace(":country", country)
); );
if (response === undefined) { if (response === undefined) {
@ -139,7 +127,6 @@ class ScoreSaberService extends Service {
* @param sort the sort to use * @param sort the sort to use
* @param page the page to get scores for * @param page the page to get scores for
* @param search * @param search
* @param useProxy whether to use the proxy or not
* @returns the scores of the player, or undefined * @returns the scores of the player, or undefined
*/ */
async lookupPlayerScores({ async lookupPlayerScores({
@ -147,7 +134,6 @@ class ScoreSaberService extends Service {
sort, sort,
page, page,
search, search,
useProxy = true,
}: { }: {
playerId: string; playerId: string;
sort: ScoreSort; sort: ScoreSort;
@ -160,7 +146,6 @@ class ScoreSaberService extends Service {
`Looking up scores for player "${playerId}", sort "${sort}", page "${page}"${search ? `, search "${search}"` : ""}...` `Looking up scores for player "${playerId}", sort "${sort}", page "${page}"${search ? `, search "${search}"` : ""}...`
); );
const response = await this.fetch<ScoreSaberPlayerScoresPageToken>( const response = await this.fetch<ScoreSaberPlayerScoresPageToken>(
useProxy,
LOOKUP_PLAYER_SCORES_ENDPOINT.replace(":id", playerId) LOOKUP_PLAYER_SCORES_ENDPOINT.replace(":id", playerId)
.replace(":limit", 8 + "") .replace(":limit", 8 + "")
.replace(":sort", sort) .replace(":sort", sort)
@ -179,13 +164,11 @@ class ScoreSaberService extends Service {
* Looks up a leaderboard * Looks up a leaderboard
* *
* @param leaderboardId the ID of the leaderboard to look up * @param leaderboardId the ID of the leaderboard to look up
* @param useProxy whether to use the proxy or not
*/ */
async lookupLeaderboard(leaderboardId: string, useProxy = true): Promise<ScoreSaberLeaderboardToken | undefined> { async lookupLeaderboard(leaderboardId: string): Promise<ScoreSaberLeaderboardToken | undefined> {
const before = performance.now(); const before = performance.now();
this.log(`Looking up leaderboard "${leaderboardId}"...`); this.log(`Looking up leaderboard "${leaderboardId}"...`);
const response = await this.fetch<ScoreSaberLeaderboardToken>( const response = await this.fetch<ScoreSaberLeaderboardToken>(
useProxy,
LOOKUP_LEADERBOARD_ENDPOINT.replace(":id", leaderboardId) LOOKUP_LEADERBOARD_ENDPOINT.replace(":id", leaderboardId)
); );
if (response === undefined) { if (response === undefined) {
@ -200,18 +183,15 @@ class ScoreSaberService extends Service {
* *
* @param leaderboardId the ID of the leaderboard to look up * @param leaderboardId the ID of the leaderboard to look up
* @param page the page to get scores for * @param page the page to get scores for
* @param useProxy whether to use the proxy or not
* @returns the scores of the leaderboard, or undefined * @returns the scores of the leaderboard, or undefined
*/ */
async lookupLeaderboardScores( async lookupLeaderboardScores(
leaderboardId: string, leaderboardId: string,
page: number, page: number
useProxy = true
): Promise<ScoreSaberLeaderboardScoresPageToken | undefined> { ): Promise<ScoreSaberLeaderboardScoresPageToken | undefined> {
const before = performance.now(); const before = performance.now();
this.log(`Looking up scores for leaderboard "${leaderboardId}", page "${page}"...`); this.log(`Looking up scores for leaderboard "${leaderboardId}", page "${page}"...`);
const response = await this.fetch<ScoreSaberLeaderboardScoresPageToken>( const response = await this.fetch<ScoreSaberLeaderboardScoresPageToken>(
useProxy,
LOOKUP_LEADERBOARD_SCORES_ENDPOINT.replace(":id", leaderboardId).replace(":page", page.toString()) LOOKUP_LEADERBOARD_SCORES_ENDPOINT.replace(":id", leaderboardId).replace(":page", page.toString())
); );
if (response === undefined) { if (response === undefined) {

@ -1,11 +1,10 @@
import ky from "ky"; import ky from "ky";
import { isRunningAsWorker } from "@/common/browser-utils";
export default class Service { export default class Service {
/** /**
* The name of the service. * The name of the service.
*/ */
private name: string; private readonly name: string;
constructor(name: string) { constructor(name: string) {
this.name = name; this.name = name;
@ -17,7 +16,7 @@ export default class Service {
* @param data the data to log * @param data the data to log
*/ */
public log(data: unknown) { public log(data: unknown) {
console.log(`[${isRunningAsWorker() ? "Worker - " : ""}${this.name}]: ${data}`); console.log(`[${this.name}]: ${data}`);
} }
/** /**
@ -29,25 +28,17 @@ export default class Service {
*/ */
private buildRequestUrl(useProxy: boolean, url: string): string { private buildRequestUrl(useProxy: boolean, url: string): string {
return (useProxy ? "https://proxy.fascinated.cc/" : "") + url; return (useProxy ? "https://proxy.fascinated.cc/" : "") + url;
// return (useProxy ? config.siteUrl + "/api/proxy?url=" : "") + url;
} }
/** /**
* Fetches data from the given url. * Fetches data from the given url.
* *
* @param useProxy whether to use proxy or not
* @param url the url to fetch * @param url the url to fetch
* @returns the fetched data * @returns the fetched data
*/ */
public async fetch<T>(useProxy: boolean, url: string): Promise<T | undefined> { public async fetch<T>(url: string): Promise<T | undefined> {
try { try {
return await ky return await ky.get<T>(this.buildRequestUrl(true, url)).json();
.get<T>(this.buildRequestUrl(useProxy, url), {
next: {
revalidate: 60, // 1 minute
},
})
.json();
} catch (error) { } catch (error) {
console.error(`Error fetching data from ${url}:`, error); console.error(`Error fetching data from ${url}:`, error);
return undefined; return undefined;

@ -1,9 +1,8 @@
import Player, { StatisticChange } from "../player"; import Player, { StatisticChange } from "../player";
import ScoreSaberPlayerToken from "@/common/model/token/scoresaber/score-saber-player-token";
import { PlayerHistory } from "@/common/player/player-history";
import { config } from "../../../../../config";
import ky from "ky"; import ky from "ky";
import { formatDateMinimal, getDaysAgoDate, getMidnightAlignedDate } from "@/common/time-utils"; import { PlayerHistory } from "../player-history";
import ScoreSaberPlayerToken from "../../token/scoresaber/score-saber-player-token";
import { formatDateMinimal, getDaysAgoDate, getMidnightAlignedDate } from "../../../utils/time-utils";
/** /**
* A ScoreSaber player. * A ScoreSaber player.
@ -66,7 +65,10 @@ export default interface ScoreSaberPlayer extends Player {
isBeingTracked?: boolean; isBeingTracked?: boolean;
} }
export async function getScoreSaberPlayerFromToken(token: ScoreSaberPlayerToken): Promise<ScoreSaberPlayer> { export async function getScoreSaberPlayerFromToken(
apiUrl: string,
token: ScoreSaberPlayerToken
): Promise<ScoreSaberPlayer> {
const bio: ScoreSaberBio = { const bio: ScoreSaberBio = {
lines: token.bio?.split("\n") || [], lines: token.bio?.split("\n") || [],
linesStripped: token.bio?.replace(/<[^>]+>/g, "")?.split("\n") || [], linesStripped: token.bio?.replace(/<[^>]+>/g, "")?.split("\n") || [],
@ -87,7 +89,7 @@ export async function getScoreSaberPlayerFromToken(token: ScoreSaberPlayerToken)
const history = await ky const history = await ky
.get<{ .get<{
[key: string]: PlayerHistory; [key: string]: PlayerHistory;
}>(`${config.siteUrl}/api/player/history?id=${token.id}`) }>(`${apiUrl}/api/player/history?id=${token.id}`)
.json(); .json();
if (history === undefined || Object.entries(history).length === 0) { if (history === undefined || Object.entries(history).length === 0) {
console.log("Player has no history, using fallback"); console.log("Player has no history, using fallback");

@ -1,4 +1,4 @@
import { PlayerHistory } from "@/common/player/player-history"; import { PlayerHistory } from "./player-history";
export default class Player { export default class Player {
/** /**

@ -1,6 +1,6 @@
import Score from "@/common/model/score/score"; import Score from "../score";
import { Modifier } from "@/common/model/score/modifier"; import { Modifier } from "../modifier";
import ScoreSaberScoreToken from "@/common/model/token/scoresaber/score-saber-score-token"; import ScoreSaberScoreToken from "../../token/scoresaber/score-saber-score-token";
export default class ScoreSaberScore extends Score { export default class ScoreSaberScore extends Score {
constructor( constructor(

@ -1,4 +1,4 @@
import { Modifier } from "@/common/model/score/modifier"; import { Modifier } from "./modifier";
export default class Score { export default class Score {
/** /**

@ -0,0 +1,13 @@
import { PlayerHistory } from "../types/player/player-history";
/**
* Sorts the player history based on date,
* so the most recent date is first
*
* @param history the player history
*/
export function sortPlayerHistory(history: Map<string, PlayerHistory>) {
return Array.from(history.entries()).sort(
(a, b) => Date.parse(b[0]) - Date.parse(a[0]) // Sort in descending order
);
}

@ -1,16 +1,16 @@
{ {
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "ES2022",
"moduleResolution": "Bundler",
"target": "ES2022",
"declaration": true, "declaration": true,
"removeComments": true, "removeComments": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true, "sourceMap": true,
"outDir": "./dist", "outDir": "./dist",
"baseUrl": "./", "baseUrl": "./",
"incremental": true,
"skipLibCheck": true, "skipLibCheck": true,
"strictNullChecks": false, "strictNullChecks": false,
"noImplicitAny": false, "noImplicitAny": false,

@ -5,5 +5,6 @@ export default defineConfig({
splitting: false, splitting: false,
sourcemap: true, sourcemap: true,
clean: true, clean: true,
dts: true, // This line enables type declaration file generation dts: true, // Generates type declarations
format: ["esm"], // Ensures output is in ESM format
}); });

@ -9,6 +9,7 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@ssr/common": "workspace:*",
"@formkit/tempo": "^0.1.2", "@formkit/tempo": "^0.1.2",
"@heroicons/react": "^2.1.5", "@heroicons/react": "^2.1.5",
"@hookform/resolvers": "^3.9.0", "@hookform/resolvers": "^3.9.0",

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Before

Width:  |  Height:  |  Size: 841 B

After

Width:  |  Height:  |  Size: 841 B

Before

Width:  |  Height:  |  Size: 132 B

After

Width:  |  Height:  |  Size: 132 B

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before

Width:  |  Height:  |  Size: 766 B

After

Width:  |  Height:  |  Size: 766 B

Before

Width:  |  Height:  |  Size: 659 B

After

Width:  |  Height:  |  Size: 659 B

Before

Width:  |  Height:  |  Size: 604 B

After

Width:  |  Height:  |  Size: 604 B

Before

Width:  |  Height:  |  Size: 121 B

After

Width:  |  Height:  |  Size: 121 B

Before

Width:  |  Height:  |  Size: 522 B

After

Width:  |  Height:  |  Size: 522 B

Before

Width:  |  Height:  |  Size: 445 B

After

Width:  |  Height:  |  Size: 445 B

Before

Width:  |  Height:  |  Size: 320 B

After

Width:  |  Height:  |  Size: 320 B

Before

Width:  |  Height:  |  Size: 909 B

After

Width:  |  Height:  |  Size: 909 B

Before

Width:  |  Height:  |  Size: 109 B

After

Width:  |  Height:  |  Size: 109 B

Before

Width:  |  Height:  |  Size: 554 B

After

Width:  |  Height:  |  Size: 554 B

Before

Width:  |  Height:  |  Size: 311 B

After

Width:  |  Height:  |  Size: 311 B

Before

Width:  |  Height:  |  Size: 179 B

After

Width:  |  Height:  |  Size: 179 B

Before

Width:  |  Height:  |  Size: 214 B

After

Width:  |  Height:  |  Size: 214 B

Before

Width:  |  Height:  |  Size: 339 B

After

Width:  |  Height:  |  Size: 339 B

Before

Width:  |  Height:  |  Size: 324 B

After

Width:  |  Height:  |  Size: 324 B

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 282 B

Before

Width:  |  Height:  |  Size: 127 B

After

Width:  |  Height:  |  Size: 127 B

Before

Width:  |  Height:  |  Size: 254 B

After

Width:  |  Height:  |  Size: 254 B

Before

Width:  |  Height:  |  Size: 105 B

After

Width:  |  Height:  |  Size: 105 B

Before

Width:  |  Height:  |  Size: 326 B

After

Width:  |  Height:  |  Size: 326 B

Some files were not shown because too many files have changed in this diff Show More