start backend work
@ -10,7 +10,7 @@ spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host(`ssr.fascinated.cc`) && PathPrefix(`/api`)
|
||||
- match: Host(`ssr.fascinated.cc`) && PathPrefix(`/api-test`)
|
||||
kind: Rule
|
||||
middlewares:
|
||||
- name: default-headers
|
||||
|
@ -7,4 +7,4 @@ metadata:
|
||||
spec:
|
||||
stripPrefix:
|
||||
prefixes:
|
||||
- "/api"
|
||||
- "/api-test"
|
||||
|
@ -1,15 +1,15 @@
|
||||
name: "Deploy Website"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- website/**
|
||||
- common/**
|
||||
- .gitea/kubernetes/website/**
|
||||
- .gitea/workflows/deploy-website.yml
|
||||
#on:
|
||||
# workflow_dispatch:
|
||||
# push:
|
||||
# branches:
|
||||
# - master
|
||||
# paths:
|
||||
# - website/**
|
||||
# - common/**
|
||||
# - .gitea/kubernetes/website/**
|
||||
# - .gitea/workflows/deploy-website.yml
|
||||
|
||||
jobs:
|
||||
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
|
||||
};
|
||||
};
|
@ -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
|
||||
}
|
||||
}
|
15
package.json
@ -1,14 +1,13 @@
|
||||
{
|
||||
"name": "scoresaber-reloadedv3",
|
||||
"name": "scoresaber-reloaded",
|
||||
"version": "1.0.0",
|
||||
"workspaces": [
|
||||
"projects/backend",
|
||||
"projects/website",
|
||||
"projects/common"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "pnpm --parallel --workspace-concurrency=4 run -r 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"
|
||||
"dev": "bun run --filter '*' dev"
|
||||
},
|
||||
"author": "fascinated7",
|
||||
"license": "MIT"
|
||||
|
10721
pnpm-lock.yaml
generated
@ -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
|
19
projects/backend/Dockerfile
Normal file
@ -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"]
|
9
projects/backend/README.md
Normal file
@ -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.
|
21
projects/backend/package.json
Normal file
@ -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"
|
||||
}
|
10
projects/backend/src/common/app-utils.ts
Normal file
@ -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");
|
||||
}
|
13
projects/backend/src/controller/app.ts
Normal file
@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
53
projects/backend/src/index.ts
Normal file
@ -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);
|
12
projects/backend/tsconfig.json
Normal file
@ -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",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsup src/index.ts --watch",
|
||||
"build": "tsup src/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.7.4",
|
||||
"tsup": "^6.5.0",
|
||||
"tsup": "^8",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"ky": "^1.7.2"
|
||||
}
|
||||
}
|
49
projects/common/src/index.ts
Normal file
@ -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";
|
34
projects/common/src/service/impl/beatsaver.ts
Normal file
@ -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 ScoreSaberPlayer, { getScoreSaberPlayerFromToken } from "@/common/model/player/impl/scoresaber-player";
|
||||
import ScoreSaberLeaderboardToken from "@/common/model/token/scoresaber/score-saber-leaderboard-token";
|
||||
import { ScoreSaberPlayerSearchToken } from "../../types/token/scoresaber/score-saber-player-search-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";
|
||||
|
||||
@ -34,16 +34,12 @@ class ScoreSaberService extends Service {
|
||||
* Gets the players that match the query.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
async searchPlayers(query: string, useProxy = true): Promise<ScoreSaberPlayerSearchToken | undefined> {
|
||||
async searchPlayers(query: string): Promise<ScoreSaberPlayerSearchToken | undefined> {
|
||||
const before = performance.now();
|
||||
this.log(`Searching for players matching "${query}"...`);
|
||||
const results = await this.fetch<ScoreSaberPlayerSearchToken>(
|
||||
useProxy,
|
||||
SEARCH_PLAYERS_ENDPOINT.replace(":query", query)
|
||||
);
|
||||
const results = await this.fetch<ScoreSaberPlayerSearchToken>(SEARCH_PLAYERS_ENDPOINT.replace(":query", query));
|
||||
if (results === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
@ -59,12 +55,12 @@ class ScoreSaberService extends Service {
|
||||
* Looks up a player by their ID.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
async lookupPlayer(
|
||||
playerId: string,
|
||||
useProxy = true
|
||||
apiUrl: string
|
||||
): Promise<
|
||||
| {
|
||||
player: ScoreSaberPlayer;
|
||||
@ -74,13 +70,13 @@ class ScoreSaberService extends Service {
|
||||
> {
|
||||
const before = performance.now();
|
||||
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) {
|
||||
return undefined;
|
||||
}
|
||||
this.log(`Found player "${playerId}" in ${(performance.now() - before).toFixed(0)}ms`);
|
||||
return {
|
||||
player: await getScoreSaberPlayerFromToken(token),
|
||||
player: await getScoreSaberPlayerFromToken(apiUrl, token),
|
||||
rawPlayer: token,
|
||||
};
|
||||
}
|
||||
@ -89,14 +85,12 @@ class ScoreSaberService extends Service {
|
||||
* Lookup players on a specific page
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
async lookupPlayers(page: number, useProxy = true): Promise<ScoreSaberPlayersPageToken | undefined> {
|
||||
async lookupPlayers(page: number): Promise<ScoreSaberPlayersPageToken | undefined> {
|
||||
const before = performance.now();
|
||||
this.log(`Looking up players on page "${page}"...`);
|
||||
const response = await this.fetch<ScoreSaberPlayersPageToken>(
|
||||
useProxy,
|
||||
LOOKUP_PLAYERS_ENDPOINT.replace(":page", page.toString())
|
||||
);
|
||||
if (response === undefined) {
|
||||
@ -111,18 +105,12 @@ class ScoreSaberService extends Service {
|
||||
*
|
||||
* @param page the page 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
|
||||
*/
|
||||
async lookupPlayersByCountry(
|
||||
page: number,
|
||||
country: string,
|
||||
useProxy = true
|
||||
): Promise<ScoreSaberPlayersPageToken | undefined> {
|
||||
async lookupPlayersByCountry(page: number, country: string): Promise<ScoreSaberPlayersPageToken | undefined> {
|
||||
const before = performance.now();
|
||||
this.log(`Looking up players on page "${page}" for country "${country}"...`);
|
||||
const response = await this.fetch<ScoreSaberPlayersPageToken>(
|
||||
useProxy,
|
||||
LOOKUP_PLAYERS_BY_COUNTRY_ENDPOINT.replace(":page", page.toString()).replace(":country", country)
|
||||
);
|
||||
if (response === undefined) {
|
||||
@ -139,7 +127,6 @@ class ScoreSaberService extends Service {
|
||||
* @param sort the sort to use
|
||||
* @param page the page to get scores for
|
||||
* @param search
|
||||
* @param useProxy whether to use the proxy or not
|
||||
* @returns the scores of the player, or undefined
|
||||
*/
|
||||
async lookupPlayerScores({
|
||||
@ -147,7 +134,6 @@ class ScoreSaberService extends Service {
|
||||
sort,
|
||||
page,
|
||||
search,
|
||||
useProxy = true,
|
||||
}: {
|
||||
playerId: string;
|
||||
sort: ScoreSort;
|
||||
@ -160,7 +146,6 @@ class ScoreSaberService extends Service {
|
||||
`Looking up scores for player "${playerId}", sort "${sort}", page "${page}"${search ? `, search "${search}"` : ""}...`
|
||||
);
|
||||
const response = await this.fetch<ScoreSaberPlayerScoresPageToken>(
|
||||
useProxy,
|
||||
LOOKUP_PLAYER_SCORES_ENDPOINT.replace(":id", playerId)
|
||||
.replace(":limit", 8 + "")
|
||||
.replace(":sort", sort)
|
||||
@ -179,13 +164,11 @@ class ScoreSaberService extends Service {
|
||||
* Looks up a leaderboard
|
||||
*
|
||||
* @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();
|
||||
this.log(`Looking up leaderboard "${leaderboardId}"...`);
|
||||
const response = await this.fetch<ScoreSaberLeaderboardToken>(
|
||||
useProxy,
|
||||
LOOKUP_LEADERBOARD_ENDPOINT.replace(":id", leaderboardId)
|
||||
);
|
||||
if (response === undefined) {
|
||||
@ -200,18 +183,15 @@ class ScoreSaberService extends Service {
|
||||
*
|
||||
* @param leaderboardId the ID of the leaderboard to look up
|
||||
* @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
|
||||
*/
|
||||
async lookupLeaderboardScores(
|
||||
leaderboardId: string,
|
||||
page: number,
|
||||
useProxy = true
|
||||
page: number
|
||||
): Promise<ScoreSaberLeaderboardScoresPageToken | undefined> {
|
||||
const before = performance.now();
|
||||
this.log(`Looking up scores for leaderboard "${leaderboardId}", page "${page}"...`);
|
||||
const response = await this.fetch<ScoreSaberLeaderboardScoresPageToken>(
|
||||
useProxy,
|
||||
LOOKUP_LEADERBOARD_SCORES_ENDPOINT.replace(":id", leaderboardId).replace(":page", page.toString())
|
||||
);
|
||||
if (response === undefined) {
|
@ -1,11 +1,10 @@
|
||||
import ky from "ky";
|
||||
import { isRunningAsWorker } from "@/common/browser-utils";
|
||||
|
||||
export default class Service {
|
||||
/**
|
||||
* The name of the service.
|
||||
*/
|
||||
private name: string;
|
||||
private readonly name: string;
|
||||
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
@ -17,7 +16,7 @@ export default class Service {
|
||||
* @param data the data to log
|
||||
*/
|
||||
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 {
|
||||
return (useProxy ? "https://proxy.fascinated.cc/" : "") + url;
|
||||
// return (useProxy ? config.siteUrl + "/api/proxy?url=" : "") + url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches data from the given url.
|
||||
*
|
||||
* @param useProxy whether to use proxy or not
|
||||
* @param url the url to fetch
|
||||
* @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 {
|
||||
return await ky
|
||||
.get<T>(this.buildRequestUrl(useProxy, url), {
|
||||
next: {
|
||||
revalidate: 60, // 1 minute
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return await ky.get<T>(this.buildRequestUrl(true, url)).json();
|
||||
} catch (error) {
|
||||
console.error(`Error fetching data from ${url}:`, error);
|
||||
return undefined;
|
@ -1,9 +1,8 @@
|
||||
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 { 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.
|
||||
@ -66,7 +65,10 @@ export default interface ScoreSaberPlayer extends Player {
|
||||
isBeingTracked?: boolean;
|
||||
}
|
||||
|
||||
export async function getScoreSaberPlayerFromToken(token: ScoreSaberPlayerToken): Promise<ScoreSaberPlayer> {
|
||||
export async function getScoreSaberPlayerFromToken(
|
||||
apiUrl: string,
|
||||
token: ScoreSaberPlayerToken
|
||||
): Promise<ScoreSaberPlayer> {
|
||||
const bio: ScoreSaberBio = {
|
||||
lines: token.bio?.split("\n") || [],
|
||||
linesStripped: token.bio?.replace(/<[^>]+>/g, "")?.split("\n") || [],
|
||||
@ -87,7 +89,7 @@ export async function getScoreSaberPlayerFromToken(token: ScoreSaberPlayerToken)
|
||||
const history = await ky
|
||||
.get<{
|
||||
[key: string]: PlayerHistory;
|
||||
}>(`${config.siteUrl}/api/player/history?id=${token.id}`)
|
||||
}>(`${apiUrl}/api/player/history?id=${token.id}`)
|
||||
.json();
|
||||
if (history === undefined || Object.entries(history).length === 0) {
|
||||
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 {
|
||||
/**
|
@ -1,6 +1,6 @@
|
||||
import Score from "@/common/model/score/score";
|
||||
import { Modifier } from "@/common/model/score/modifier";
|
||||
import ScoreSaberScoreToken from "@/common/model/token/scoresaber/score-saber-score-token";
|
||||
import Score from "../score";
|
||||
import { Modifier } from "../modifier";
|
||||
import ScoreSaberScoreToken from "../../token/scoresaber/score-saber-score-token";
|
||||
|
||||
export default class ScoreSaberScore extends Score {
|
||||
constructor(
|
@ -1,4 +1,4 @@
|
||||
import { Modifier } from "@/common/model/score/modifier";
|
||||
import { Modifier } from "./modifier";
|
||||
|
||||
export default class Score {
|
||||
/**
|
13
projects/common/src/utils/player-utils.ts
Normal file
@ -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": {
|
||||
"module": "commonjs",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "Bundler",
|
||||
"target": "ES2022",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2021",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": false,
|
||||
"noImplicitAny": false,
|
@ -5,5 +5,6 @@ export default defineConfig({
|
||||
splitting: false,
|
||||
sourcemap: 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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ssr/common": "workspace:*",
|
||||
"@formkit/tempo": "^0.1.2",
|
||||
"@heroicons/react": "^2.1.5",
|
||||
"@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 |