From 47015e222c3009ce8f790600fdf9d5833d49e643 Mon Sep 17 00:00:00 2001 From: richardscull <106016833+richardscull@users.noreply.github.com> Date: Thu, 25 Dec 2025 08:30:45 +0200 Subject: [PATCH 1/3] feat: Use @richardscull/eslint-config instead of prettier --- .prettierrc.json | 5 - bun.lock | 748 ++++++++++++++++++++++++++++++++++++++++++++++- eslint.config.ts | 3 + package.json | 40 ++- tsconfig.json | 190 ++++++------ 5 files changed, 865 insertions(+), 121 deletions(-) delete mode 100644 .prettierrc.json create mode 100644 eslint.config.ts diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index 68dcd27..0000000 --- a/.prettierrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "semi": true, - "tabWidth": 4, - "singleQuote": true -} diff --git a/bun.lock b/bun.lock index f09d151..475ed47 100644 --- a/bun.lock +++ b/bun.lock @@ -28,13 +28,19 @@ "rosu-pp-js": "^3.1.0", }, "devDependencies": { + "@richardscull/eslint-config": "^1.0.5", "@types/bun": "^1.3.4", "@types/pg": "^8.11.10", "@types/qs": "^6.9.18", "bun-types": "latest", "drizzle-kit": "^0.26.2", + "eslint": "^9.39.2", + "jiji": "^0.1.7", + "lint-staged": "^16.2.7", "prettier": "^3.3.3", + "simple-git-hooks": "^2.13.1", "tsx": "^4.19.2", + "typescript": "^5", }, "peerDependencies": { "typescript": "^5", @@ -44,10 +50,48 @@ "packages": { "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + "@altano/repository-tools": ["@altano/repository-tools@2.0.1", "", {}, "sha512-YE/52CkFtb+YtHPgbWPai7oo5N9AKnMuP5LM+i2AG7G1H2jdYBCO1iDnkDE3dZ3C1MIgckaF+d5PNRulgt0bdw=="], + + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="], + + "@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="], + + "@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], + + "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + + "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + "@bogeychan/elysia-logger": ["@bogeychan/elysia-logger@0.1.10", "", { "peerDependencies": { "elysia": ">= 1.2.10", "pino": ">= 9.6.0" } }, "sha512-wFp3KUCNIkCV4zcbo70gifHH99ch7e4LNrP5Xa5e2MRO2MDyPgM92SWQmrzkng6PsSZk13+UKJskGNQ+ZuDZkQ=="], "@borewit/text-codec": ["@borewit/text-codec@0.1.1", "", {}, "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA=="], + "@deno/shim-deno": ["@deno/shim-deno@0.18.2", "", { "dependencies": { "@deno/shim-deno-test": "^0.5.0", "which": "^4.0.0" } }, "sha512-oQ0CVmOio63wlhwQF75zA4ioolPvOwAoK0yuzcS5bDC1JUvH3y1GS8xPh8EOpcoDQRU4FTG8OQfxhpR+c6DrzA=="], + + "@deno/shim-deno-test": ["@deno/shim-deno-test@0.5.0", "", {}, "sha512-4nMhecpGlPi0cSzT67L+Tm+GOJqvuk8gqHBziqcUQOarnuIax1z96/gJHCSIz2Z0zhxE6Rzwb3IZXPtFh51j+w=="], + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], "@elysiajs/cors": ["@elysiajs/cors@1.4.0", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-pb0SCzBfFbFSYA/U40HHO7R+YrcXBJXOWgL20eSViK33ol1e20ru2/KUaZYo5IMUn63yaTJI/bQERuQ+77ND8g=="], @@ -56,6 +100,14 @@ "@elysiajs/swagger": ["@elysiajs/swagger@1.3.1", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-LcbLHa0zE6FJKWPWKsIC/f+62wbDv3aXydqcNPVPyqNcaUgwvCajIi+5kHEU6GO3oXUCpzKaMsb3gsjt8sLzFQ=="], + "@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + + "@es-joy/jsdoccomment": ["@es-joy/jsdoccomment@0.78.0", "", { "dependencies": { "@types/estree": "^1.0.8", "@typescript-eslint/types": "^8.46.4", "comment-parser": "1.4.1", "esquery": "^1.6.0", "jsdoc-type-pratt-parser": "~7.0.0" } }, "sha512-rQkU5u8hNAq2NVRzHnIUUvR6arbO0b6AOlvpTNS48CkiKSn/xtNfOzBK23JE4SiW89DgvU7GtxLVgV4Vn2HBAw=="], + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], @@ -112,16 +164,86 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="], + "@eslint-community/eslint-plugin-eslint-comments": ["@eslint-community/eslint-plugin-eslint-comments@4.5.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0", "ignore": "^5.2.4" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" } }, "sha512-MAhuTKlr4y/CE3WYX26raZjy+I/kS2PLKSzvfmDCGrBLTFHOYwqROZdr4XwPgXwX3K9rjzMr4pSmUWGnzsUyMg=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint-react/ast": ["@eslint-react/ast@2.4.0", "", { "dependencies": { "@eslint-react/eff": "2.4.0", "@typescript-eslint/types": "^8.50.1", "@typescript-eslint/typescript-estree": "^8.50.1", "@typescript-eslint/utils": "^8.50.1", "string-ts": "^2.3.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-xi/uVi4/jaqPDgF9tO4laLAAZLBrKXearHKIAJWmnY+ymu0LBjX8VaLuf6GuUq7ryek5NO2kOZDYNx4C3qV4iw=="], + + "@eslint-react/core": ["@eslint-react/core@2.4.0", "", { "dependencies": { "@eslint-react/ast": "2.4.0", "@eslint-react/eff": "2.4.0", "@eslint-react/shared": "2.4.0", "@eslint-react/var": "2.4.0", "@typescript-eslint/scope-manager": "^8.50.1", "@typescript-eslint/types": "^8.50.1", "@typescript-eslint/utils": "^8.50.1", "birecord": "^0.1.1", "ts-pattern": "^5.9.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Ibt8NlFhT+FffNKtZmb2xamHEFzwK0AzmmOUGVm/B49B2ShOOR3kQg7ZaVNUR3By0Q0hPlIYnefKH2KgaUJ7jA=="], + + "@eslint-react/eff": ["@eslint-react/eff@2.4.0", "", {}, "sha512-iWB2IaO+ygt8YPGXqUIg3KQmu3GgKecwbHrz0nasEO2BuhR7rAPaBcqnC3s8NvMUicJ/q03yWzfTgMuFST5+jg=="], + + "@eslint-react/eslint-plugin": ["@eslint-react/eslint-plugin@2.4.0", "", { "dependencies": { "@eslint-react/eff": "2.4.0", "@eslint-react/shared": "2.4.0", "@typescript-eslint/scope-manager": "^8.50.1", "@typescript-eslint/type-utils": "^8.50.1", "@typescript-eslint/types": "^8.50.1", "@typescript-eslint/utils": "^8.50.1", "eslint-plugin-react-dom": "2.4.0", "eslint-plugin-react-hooks-extra": "2.4.0", "eslint-plugin-react-naming-convention": "2.4.0", "eslint-plugin-react-web-api": "2.4.0", "eslint-plugin-react-x": "2.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-+d3JGOc+EM80LGO7Ynz8vRRex+xA+ilM0/BDvwHTlvfdNK6GeH8EV7RewAClvGijfkEMxCoehglVRGwjzmoKbw=="], + + "@eslint-react/shared": ["@eslint-react/shared@2.4.0", "", { "dependencies": { "@eslint-react/eff": "2.4.0", "@typescript-eslint/utils": "^8.50.1", "ts-pattern": "^5.9.0", "zod": "^4.2.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-1LYGz8AzAN9knt56h2onTL4beTLxys/KzV+PJwODydAqYKIlWAOtJJK1HLwDrXneiLP8G2mHrt2XwcmrXzzaRw=="], + + "@eslint-react/var": ["@eslint-react/var@2.4.0", "", { "dependencies": { "@eslint-react/ast": "2.4.0", "@eslint-react/eff": "2.4.0", "@typescript-eslint/scope-manager": "^8.50.1", "@typescript-eslint/types": "^8.50.1", "@typescript-eslint/utils": "^8.50.1", "ts-pattern": "^5.9.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vIVIQfBS8qfzu/AM4/fAek+Qab63MZwintr7gdSOjRy5z/7Kixjzg5Nj1AeW78jBtPrZSDYUpYb8ZXM0mN/Qag=="], + + "@eslint/compat": ["@eslint/compat@1.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0" }, "peerDependencies": { "eslint": "^8.40 || 9" }, "optionalPeers": ["eslint"] }, "sha512-cfO82V9zxxGBxcQDr1lfaYB7wykTa0b00mGa36FrJl7iTFd0Z2cHfEYuxcBRP/iNijCsWsEkA+jzT8hGYmv33w=="], + + "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + + "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="], + + "@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + "@faker-js/faker": ["@faker-js/faker@10.1.0", "", {}, "sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg=="], + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + "@ioredis/commands": ["@ioredis/commands@1.4.0", "", {}, "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ=="], + "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], + + "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@libsql/client-wasm": ["@libsql/client-wasm@0.15.15", "", { "dependencies": { "@libsql/core": "^0.15.14", "@libsql/libsql-wasm-experimental": "^0.0.2", "js-base64": "^3.7.5" } }, "sha512-ah71HHAsywWRSinJg7SKbJc+mTIgw7wHPYLE9DfW79wa+CSQXX1rEbrDNQx0HvHC1YFd2IpVpVASBFlqh54vVA=="], "@libsql/core": ["@libsql/core@0.15.15", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-C88Z6UKl+OyuKKPwz224riz02ih/zHYI3Ho/LAcVOgjsunIRZoBw7fjRfaH9oPMmSNeQfhGklSG2il1URoOIsA=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], + "@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="], + + "@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="], + + "@richardscull/eslint-config": ["@richardscull/eslint-config@1.0.5", "", { "dependencies": { "@eslint-community/eslint-plugin-eslint-comments": "^4.5.0", "@eslint-react/eslint-plugin": "^2.3.13", "@eslint/js": "^9.39.2", "@stylistic/eslint-plugin": "^5.6.1", "@typescript-eslint/types": "^8.50.0", "@unocss/eslint-config": "^66.5.10", "defu": "^6.1.4", "eslint-config-flat-gitignore": "^2.1.0", "eslint-plugin-antfu": "^3.1.1", "eslint-plugin-command": "^3.4.0", "eslint-plugin-hyoban": "^0.6.1", "eslint-plugin-import-x": "^4.16.1", "eslint-plugin-jsonc": "^2.21.0", "eslint-plugin-package-json": "^0.85.0", "eslint-plugin-react-google-translate": "^0.1.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.26", "eslint-plugin-regexp": "^2.10.0", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-tailwindcss": "4.0.0-beta.0", "eslint-plugin-unicorn": "^62.0.0", "eslint-plugin-unused-imports": "^4.3.0", "globals": "^16.5.0", "jsonc-eslint-parser": "^2.4.2", "local-pkg": "^1.1.2", "read-package-up": "^12.0.0", "typescript-eslint": "^8.50.0" }, "peerDependencies": { "eslint": "^9.0.0", "typescript": ">=4.8.4" }, "optionalPeers": ["typescript"] }, "sha512-v5BH+HGnPQszq8laxXYXHhyhlnHnBZnDBUiA9WOIIsDXFDJbpb+L0oV/WWdI/JDQMJBdQzDZMfv7EXpS364jTA=="], + "@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="], "@scalar/themes": ["@scalar/themes@0.9.86", "", { "dependencies": { "@scalar/types": "0.1.7" } }, "sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row=="], @@ -130,60 +252,214 @@ "@sinclair/typebox": ["@sinclair/typebox@0.34.41", "", {}, "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g=="], + "@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.6.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.0", "@typescript-eslint/types": "^8.47.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-JCs+MqoXfXrRPGbGmho/zGS/jMcn3ieKl/A8YImqib76C8kjgZwq5uUFzc30lJkMvcchuRn6/v8IApLxli3Jyw=="], + "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@types/adm-zip": ["@types/adm-zip@0.5.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw=="], "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], + "@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="], "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.50.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/type-utils": "8.50.1", "@typescript-eslint/utils": "8.50.1", "@typescript-eslint/visitor-keys": "8.50.1", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.50.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.50.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/types": "8.50.1", "@typescript-eslint/typescript-estree": "8.50.1", "@typescript-eslint/visitor-keys": "8.50.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.50.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.50.1", "@typescript-eslint/types": "^8.50.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.50.1", "", { "dependencies": { "@typescript-eslint/types": "8.50.1", "@typescript-eslint/visitor-keys": "8.50.1" } }, "sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.50.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.50.1", "", { "dependencies": { "@typescript-eslint/types": "8.50.1", "@typescript-eslint/typescript-estree": "8.50.1", "@typescript-eslint/utils": "8.50.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.50.1", "", {}, "sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.50.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.50.1", "@typescript-eslint/tsconfig-utils": "8.50.1", "@typescript-eslint/types": "8.50.1", "@typescript-eslint/visitor-keys": "8.50.1", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.50.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/types": "8.50.1", "@typescript-eslint/typescript-estree": "8.50.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.50.1", "", { "dependencies": { "@typescript-eslint/types": "8.50.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ=="], + "@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="], + "@unocss/config": ["@unocss/config@66.5.10", "", { "dependencies": { "@unocss/core": "66.5.10", "unconfig": "^7.4.1" } }, "sha512-udBhfMe+2MU70ZdjnRLnwLQ+0EHYJ4f5JjjvHsfmQ0If4KeYmSStWBuX+/LHNQidhl487JiwW1lBDQ8pKHmbiw=="], + + "@unocss/core": ["@unocss/core@66.5.10", "", {}, "sha512-SEmPE4pWNn9VcCvZqovPwFGuG/j69W3zh+x1Ky4z/I2pnyoB0Y0lBmq22KVu/dwExe+ZKKTQpxa0j5rbE27rDQ=="], + + "@unocss/eslint-config": ["@unocss/eslint-config@66.5.10", "", { "dependencies": { "@unocss/eslint-plugin": "66.5.10" } }, "sha512-kDoXTBZcI7RCdWPekKrjgiuRcNYfdwjEkG6HtS1++jM0LhK6QgaMfu4p+4j0gfAz86ZNotghM3u8aWO6Fu0nRA=="], + + "@unocss/eslint-plugin": ["@unocss/eslint-plugin@66.5.10", "", { "dependencies": { "@typescript-eslint/utils": "^8.46.4", "@unocss/config": "66.5.10", "@unocss/core": "66.5.10", "@unocss/rule-utils": "66.5.10", "magic-string": "^0.30.21", "synckit": "^0.11.11" } }, "sha512-Fzvl5ISMoGnALo9tqI15nNNWZza2ICqmzyujQCyzsxDZEVZzajNvt8wACVHoEz+dUZykjMPJqqdmX5ZijcPZ1w=="], + + "@unocss/rule-utils": ["@unocss/rule-utils@66.5.10", "", { "dependencies": { "@unocss/core": "^66.5.10", "magic-string": "^0.30.21" } }, "sha512-497GPWZpArNG25cto0Yq3/Yw+i0x7/N/ySq1HHeE3lB43sdmCv6+m6QEv14I/9/e5WJhQOmrY5LmHZYXC7xxMw=="], + + "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="], + + "@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.11.1", "", { "os": "android", "cpu": "arm64" }, "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g=="], + + "@unrs/resolver-binding-darwin-arm64": ["@unrs/resolver-binding-darwin-arm64@1.11.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g=="], + + "@unrs/resolver-binding-darwin-x64": ["@unrs/resolver-binding-darwin-x64@1.11.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ=="], + + "@unrs/resolver-binding-freebsd-x64": ["@unrs/resolver-binding-freebsd-x64@1.11.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw=="], + + "@unrs/resolver-binding-linux-arm-gnueabihf": ["@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw=="], + + "@unrs/resolver-binding-linux-arm-musleabihf": ["@unrs/resolver-binding-linux-arm-musleabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw=="], + + "@unrs/resolver-binding-linux-arm64-gnu": ["@unrs/resolver-binding-linux-arm64-gnu@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ=="], + + "@unrs/resolver-binding-linux-arm64-musl": ["@unrs/resolver-binding-linux-arm64-musl@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w=="], + + "@unrs/resolver-binding-linux-ppc64-gnu": ["@unrs/resolver-binding-linux-ppc64-gnu@1.11.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA=="], + + "@unrs/resolver-binding-linux-riscv64-gnu": ["@unrs/resolver-binding-linux-riscv64-gnu@1.11.1", "", { "os": "linux", "cpu": "none" }, "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ=="], + + "@unrs/resolver-binding-linux-riscv64-musl": ["@unrs/resolver-binding-linux-riscv64-musl@1.11.1", "", { "os": "linux", "cpu": "none" }, "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew=="], + + "@unrs/resolver-binding-linux-s390x-gnu": ["@unrs/resolver-binding-linux-s390x-gnu@1.11.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg=="], + + "@unrs/resolver-binding-linux-x64-gnu": ["@unrs/resolver-binding-linux-x64-gnu@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w=="], + + "@unrs/resolver-binding-linux-x64-musl": ["@unrs/resolver-binding-linux-x64-musl@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA=="], + + "@unrs/resolver-binding-wasm32-wasi": ["@unrs/resolver-binding-wasm32-wasi@1.11.1", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.11" }, "cpu": "none" }, "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ=="], + + "@unrs/resolver-binding-win32-arm64-msvc": ["@unrs/resolver-binding-win32-arm64-msvc@1.11.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw=="], + + "@unrs/resolver-binding-win32-ia32-msvc": ["@unrs/resolver-binding-win32-ia32-msvc@1.11.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ=="], + + "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + "adm-zip": ["adm-zip@0.5.16", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="], + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "ansi-escapes": ["ansi-escapes@7.2.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw=="], + + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], "axios": ["axios@1.13.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="], + + "birecord": ["birecord@0.1.1", "", {}, "sha512-VUpsf/qykW0heRlC8LooCq28Kxn3mAqKohhDG/49rrsQ1dT1CXyj/pgXS+5BSRzFTR/3DyIBOqQOrGyZOh71Aw=="], + + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "builtin-modules": ["builtin-modules@5.0.0", "", {}, "sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg=="], + "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001761", "", {}, "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "change-case": ["change-case@5.4.4", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="], + + "ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], + + "clean-regexp": ["clean-regexp@1.0.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw=="], + + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "cli-truncate": ["cli-truncate@5.1.1", "", { "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" } }, "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A=="], + + "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], + "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], + + "comment-parser": ["comment-parser@1.4.1", "", {}, "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg=="], + + "compare-versions": ["compare-versions@6.1.1", "", {}, "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + "core-js-compat": ["core-js-compat@3.47.0", "", { "dependencies": { "browserslist": "^4.28.0" } }, "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], - "debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], + "detect-indent": ["detect-indent@7.0.2", "", {}, "sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A=="], + + "detect-newline": ["detect-newline@4.0.1", "", {}, "sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog=="], + + "diff-sequences": ["diff-sequences@27.5.1", "", {}, "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ=="], + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], "drizzle-kit": ["drizzle-kit@0.26.2", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.1", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-cMq8omEKywjIy5KcqUo6LvEFxkl8/zYHsgYjFVXjmPWWtuW4blcz+YW9+oIhoaALgs2ebRjzXwsJgN9i6P49Dw=="], @@ -192,6 +468,8 @@ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="], + "elysia": ["elysia@1.3.21", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.1.6", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": "^0.34.33", "openapi-types": "^12.1.3" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-LLfDSoVA5fBoqKQfMJyzmHLkya8zMbEYwd7DS7v2iQB706mgzWg0gufXl58cFALErcvSayplrkDvjkmlYTkIZQ=="], "elysia-autoload": ["elysia-autoload@1.7.0", "", { "peerDependencies": { "elysia": "^1.3.1" } }, "sha512-aezzh/UBOTHkqSfBUj110DGdm4zUqYLX8HH5jDpov2FGC/WOOT8YGrlCqKVM6UiSk+bSoH+whHTIkgF5a1njGw=="], @@ -202,8 +480,14 @@ "elysia-requestid": ["elysia-requestid@1.0.9", "", { "peerDependencies": { "elysia": "^0.7.30", "typescript": "^5.2.2" } }, "sha512-8/7kt/y5Sa16e6XS0+UtPVN7gBCHLp7k7z7j3OsqjsoUv3ofUutwV+yrHNayBsrYULVEoBMKBfqw5X/fT9zx3A=="], + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + "enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], + + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], @@ -216,20 +500,116 @@ "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="], + + "eslint-compat-utils": ["eslint-compat-utils@0.6.5", "", { "dependencies": { "semver": "^7.5.4" }, "peerDependencies": { "eslint": ">=6.0.0" } }, "sha512-vAUHYzue4YAa2hNACjB8HvUQj5yehAZgiClyFVVom9cP8z5NSFq3PwB/TtJslN2zAMgRX6FCFCjYBbQh71g5RQ=="], + + "eslint-config-flat-gitignore": ["eslint-config-flat-gitignore@2.1.0", "", { "dependencies": { "@eslint/compat": "^1.2.5" }, "peerDependencies": { "eslint": "^9.5.0" } }, "sha512-cJzNJ7L+psWp5mXM7jBX+fjHtBvvh06RBlcweMhKD8jWqQw0G78hOW5tpVALGHGFPsBV+ot2H+pdDGJy6CV8pA=="], + + "eslint-fix-utils": ["eslint-fix-utils@0.4.0", "", { "peerDependencies": { "@types/estree": ">=1", "eslint": ">=8" }, "optionalPeers": ["@types/estree"] }, "sha512-nCEciwqByGxsKiWqZjqK7xfL+7dUX9Pi0UL3J0tOwfxVN9e6Y59UxEt1ZYsc3XH0ce6T1WQM/QU2DbKK/6IG7g=="], + + "eslint-import-context": ["eslint-import-context@0.1.9", "", { "dependencies": { "get-tsconfig": "^4.10.1", "stable-hash-x": "^0.2.0" }, "peerDependencies": { "unrs-resolver": "^1.0.0" }, "optionalPeers": ["unrs-resolver"] }, "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg=="], + + "eslint-json-compat-utils": ["eslint-json-compat-utils@0.2.1", "", { "dependencies": { "esquery": "^1.6.0" }, "peerDependencies": { "eslint": "*", "jsonc-eslint-parser": "^2.4.0" } }, "sha512-YzEodbDyW8DX8bImKhAcCeu/L31Dd/70Bidx2Qex9OFUtgzXLqtfWL4Hr5fM/aCCB8QUZLuJur0S9k6UfgFkfg=="], + + "eslint-plugin-antfu": ["eslint-plugin-antfu@3.1.1", "", { "peerDependencies": { "eslint": "*" } }, "sha512-7Q+NhwLfHJFvopI2HBZbSxWXngTwBLKxW1AGXLr2lEGxcEIK/AsDs8pn8fvIizl5aZjBbVbVK5ujmMpBe4Tvdg=="], + + "eslint-plugin-command": ["eslint-plugin-command@3.4.0", "", { "dependencies": { "@es-joy/jsdoccomment": "^0.78.0" }, "peerDependencies": { "eslint": "*" } }, "sha512-EW4eg/a7TKEhG0s5IEti72kh3YOTlnhfFNuctq5WnB1fst37/IHTd5OkD+vnlRf3opTvUcSRihAateP6bT5ZcA=="], + + "eslint-plugin-hyoban": ["eslint-plugin-hyoban@0.6.1", "", { "peerDependencies": { "eslint": "*" } }, "sha512-DJI5rCIATcK2e4f7TMt1+sdMSXEAytcn469dLV0hSn4lVvVXsT6uLH/Pogj/cm+m6I8AuUWoHGj/OId5M8tLCg=="], + + "eslint-plugin-import-x": ["eslint-plugin-import-x@4.16.1", "", { "dependencies": { "@typescript-eslint/types": "^8.35.0", "comment-parser": "^1.4.1", "debug": "^4.4.1", "eslint-import-context": "^0.1.9", "is-glob": "^4.0.3", "minimatch": "^9.0.3 || ^10.0.1", "semver": "^7.7.2", "stable-hash-x": "^0.2.0", "unrs-resolver": "^1.9.2" }, "peerDependencies": { "@typescript-eslint/utils": "^8.0.0", "eslint": "^8.57.0 || ^9.0.0", "eslint-import-resolver-node": "*" }, "optionalPeers": ["@typescript-eslint/utils", "eslint-import-resolver-node"] }, "sha512-vPZZsiOKaBAIATpFE2uMI4w5IRwdv/FpQ+qZZMR4E+PeOcM4OeoEbqxRMnywdxP19TyB/3h6QBB0EWon7letSQ=="], + + "eslint-plugin-jsonc": ["eslint-plugin-jsonc@2.21.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.5.1", "diff-sequences": "^27.5.1", "eslint-compat-utils": "^0.6.4", "eslint-json-compat-utils": "^0.2.1", "espree": "^9.6.1 || ^10.3.0", "graphemer": "^1.4.0", "jsonc-eslint-parser": "^2.4.0", "natural-compare": "^1.4.0", "synckit": "^0.6.2 || ^0.7.3 || ^0.11.5" }, "peerDependencies": { "eslint": ">=6.0.0" } }, "sha512-HttlxdNG5ly3YjP1cFMP62R4qKLxJURfBZo2gnMY+yQojZxkLyOpY1H1KRTKBmvQeSG9pIpSGEhDjE17vvYosg=="], + + "eslint-plugin-package-json": ["eslint-plugin-package-json@0.85.0", "", { "dependencies": { "@altano/repository-tools": "^2.0.1", "change-case": "^5.4.4", "detect-indent": "^7.0.2", "detect-newline": "^4.0.1", "eslint-fix-utils": "~0.4.0", "package-json-validator": "~0.59.0", "semver": "^7.7.3", "sort-object-keys": "^2.0.0", "sort-package-json": "^3.4.0", "validate-npm-package-name": "^7.0.0" }, "peerDependencies": { "eslint": ">=8.0.0", "jsonc-eslint-parser": "^2.0.0" } }, "sha512-MrOxFvhbqLuk4FIPG9v3u9Amn0n137J8LKILHvgfxK3rRyAHEVzuZM0CtpXFTx7cx4LzmAzONtlpjbM0UFNuTA=="], + + "eslint-plugin-react-dom": ["eslint-plugin-react-dom@2.4.0", "", { "dependencies": { "@eslint-react/ast": "2.4.0", "@eslint-react/core": "2.4.0", "@eslint-react/eff": "2.4.0", "@eslint-react/shared": "2.4.0", "@eslint-react/var": "2.4.0", "@typescript-eslint/scope-manager": "^8.50.1", "@typescript-eslint/types": "^8.50.1", "@typescript-eslint/utils": "^8.50.1", "compare-versions": "^6.1.1", "string-ts": "^2.3.1", "ts-pattern": "^5.9.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nzBLj2bD2JJuIJlonENAE9Dp8Sy9Gw7Y45Y4mwjJ8PLV6hABP6W/sgeF0NXpzBiyClXRnjoCPRwylY0XjUaR+w=="], + + "eslint-plugin-react-google-translate": ["eslint-plugin-react-google-translate@0.1.1", "", { "dependencies": { "requireindex": "^1.2.0" }, "peerDependencies": { "eslint": ">=7" } }, "sha512-70w3YW211yFSexWLshO+rSVtr6NDuA0qaU/ARVQ9pG0WFia5CyGFhKCed6LKKngdgfQTzjrcWlg7IPJehJwTsg=="], + + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="], + + "eslint-plugin-react-hooks-extra": ["eslint-plugin-react-hooks-extra@2.4.0", "", { "dependencies": { "@eslint-react/ast": "2.4.0", "@eslint-react/core": "2.4.0", "@eslint-react/eff": "2.4.0", "@eslint-react/shared": "2.4.0", "@eslint-react/var": "2.4.0", "@typescript-eslint/scope-manager": "^8.50.1", "@typescript-eslint/type-utils": "^8.50.1", "@typescript-eslint/types": "^8.50.1", "@typescript-eslint/utils": "^8.50.1", "string-ts": "^2.3.1", "ts-pattern": "^5.9.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-uLOSXhW1+RgXrkwErfnoiGSsAxdtCJyPG8yyswR+OL3bhaT3gwj5HcyEWpj+9GrzvDnb6oknfddpyAl2RmOOHw=="], + + "eslint-plugin-react-naming-convention": ["eslint-plugin-react-naming-convention@2.4.0", "", { "dependencies": { "@eslint-react/ast": "2.4.0", "@eslint-react/core": "2.4.0", "@eslint-react/eff": "2.4.0", "@eslint-react/shared": "2.4.0", "@eslint-react/var": "2.4.0", "@typescript-eslint/scope-manager": "^8.50.1", "@typescript-eslint/type-utils": "^8.50.1", "@typescript-eslint/types": "^8.50.1", "@typescript-eslint/utils": "^8.50.1", "string-ts": "^2.3.1", "ts-pattern": "^5.9.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-7kmdrdKVO+54AUtUYzrpGqs9+wRREOWrr1A1DoMItZ8KXPv6TRWlUxm2opFFe2QysV0tSVvb4TVlfWxKcG1eLw=="], + + "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.26", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ=="], + + "eslint-plugin-react-web-api": ["eslint-plugin-react-web-api@2.4.0", "", { "dependencies": { "@eslint-react/ast": "2.4.0", "@eslint-react/core": "2.4.0", "@eslint-react/eff": "2.4.0", "@eslint-react/shared": "2.4.0", "@eslint-react/var": "2.4.0", "@typescript-eslint/scope-manager": "^8.50.1", "@typescript-eslint/types": "^8.50.1", "@typescript-eslint/utils": "^8.50.1", "string-ts": "^2.3.1", "ts-pattern": "^5.9.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-NUGVZXgegv9l1zNNeX+n8EheGZtHcZGxBW6zmqUNr/762GikOgJHwaER8xDD073nKfEfFb+4JkBcpWRqAiTPnA=="], + + "eslint-plugin-react-x": ["eslint-plugin-react-x@2.4.0", "", { "dependencies": { "@eslint-react/ast": "2.4.0", "@eslint-react/core": "2.4.0", "@eslint-react/eff": "2.4.0", "@eslint-react/shared": "2.4.0", "@eslint-react/var": "2.4.0", "@typescript-eslint/scope-manager": "^8.50.1", "@typescript-eslint/type-utils": "^8.50.1", "@typescript-eslint/types": "^8.50.1", "@typescript-eslint/utils": "^8.50.1", "compare-versions": "^6.1.1", "is-immutable-type": "^5.0.1", "string-ts": "^2.3.1", "ts-api-utils": "^2.1.0", "ts-pattern": "^5.9.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ufKDXiDoMujcIT97Q6pCQs7j5q6Dtu/0AbXvrbNDLNXWVkCfZ7ayoRKMunvPU+WUHqnnzg9iv0o9QoaWwxG6rw=="], + + "eslint-plugin-regexp": ["eslint-plugin-regexp@2.10.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", "comment-parser": "^1.4.0", "jsdoc-type-pratt-parser": "^4.0.0", "refa": "^0.12.1", "regexp-ast-analysis": "^0.7.1", "scslre": "^0.3.0" }, "peerDependencies": { "eslint": ">=8.44.0" } }, "sha512-ovzQT8ESVn5oOe5a7gIDPD5v9bCSjIFJu57sVPDqgPRXicQzOnYfFN21WoQBQF18vrhT5o7UMKFwJQVVjyJ0ng=="], + + "eslint-plugin-simple-import-sort": ["eslint-plugin-simple-import-sort@12.1.1", "", { "peerDependencies": { "eslint": ">=5.0.0" } }, "sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA=="], + + "eslint-plugin-tailwindcss": ["eslint-plugin-tailwindcss@4.0.0-beta.0", "", { "dependencies": { "fast-glob": "^3.2.5", "postcss": "^8.4.4", "synckit": "^0.11.4", "tailwind-api-utils": "^1.0.3" }, "peerDependencies": { "tailwindcss": "^3.4.0 || ^4.0.0" } }, "sha512-WWCajZgQu38Sd67ZCl2W6i3MRzqB0d+H8s4qV9iB6lBJbsDOIpIlj6R1Fj2FXkoWErbo05pZnZYbCGIU9o/DsA=="], + + "eslint-plugin-unicorn": ["eslint-plugin-unicorn@62.0.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "@eslint-community/eslint-utils": "^4.9.0", "@eslint/plugin-kit": "^0.4.0", "change-case": "^5.4.4", "ci-info": "^4.3.1", "clean-regexp": "^1.0.0", "core-js-compat": "^3.46.0", "esquery": "^1.6.0", "find-up-simple": "^1.0.1", "globals": "^16.4.0", "indent-string": "^5.0.0", "is-builtin-module": "^5.0.0", "jsesc": "^3.1.0", "pluralize": "^8.0.0", "regexp-tree": "^0.1.27", "regjsparser": "^0.13.0", "semver": "^7.7.3", "strip-indent": "^4.1.1" }, "peerDependencies": { "eslint": ">=9.38.0" } }, "sha512-HIlIkGLkvf29YEiS/ImuDZQbP12gWyx5i3C6XrRxMvVdqMroCI9qoVYCoIl17ChN+U89pn9sVwLxhIWj5nEc7g=="], + + "eslint-plugin-unused-imports": ["eslint-plugin-unused-imports@4.3.0", "", { "peerDependencies": { "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", "eslint": "^9.0.0 || ^8.0.0" }, "optionalPeers": ["@typescript-eslint/eslint-plugin"] }, "sha512-ZFBmXMGBYfHttdRtOG9nFFpmUvMtbHSjsKrS20vdWdbfiVYsO3yA2SGYy9i9XmZJDfMGBflZGBCm70SEnFQtOA=="], + + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], "exact-mirror": ["exact-mirror@0.1.6", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-EXGDixoDotCGrXCce63zmGHDA+3Id6PPkIwshBHuB10dwVc4YV4gfaYLuysHOxyURmwyt4UL186ann0oYa2CFQ=="], + "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], + "fast-copy": ["fast-copy@3.0.2", "", {}, "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ=="], "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + "file-type": ["file-type@21.1.1", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-ifJXo8zUqbQ/bLbl9sFoqHNTNWbnPY1COImFfM6CCy7z+E+jC1eY9YfOKkx0fckIg+VljAy2/87T61fp0+eEkg=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "find-up-simple": ["find-up-simple@1.0.1", "", {}, "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], @@ -238,14 +618,32 @@ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], + "git-hooks-list": ["git-hooks-list@4.1.1", "", {}, "sha512-cmP497iLq54AZnv4YRAEMnEyQ1eIn4tGKbmswqwmFV4GBnAqE8NLtWxxdXa++AalfgL5EBH4IxTPyquEuGY/jA=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], @@ -254,31 +652,125 @@ "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], + "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], + + "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], + "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + "hosted-git-info": ["hosted-git-info@9.0.2", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], + + "index-to-position": ["index-to-position@1.2.0", "", {}, "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw=="], + "ioredis": ["ioredis@5.8.2", "", { "dependencies": { "@ioredis/commands": "1.4.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q=="], + "is-builtin-module": ["is-builtin-module@5.0.0", "", { "dependencies": { "builtin-modules": "^5.0.0" } }, "sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-immutable-type": ["is-immutable-type@5.0.1", "", { "dependencies": { "@typescript-eslint/type-utils": "^8.0.0", "ts-api-utils": "^2.0.0", "ts-declaration-location": "^1.0.4" }, "peerDependencies": { "eslint": "*", "typescript": ">=4.7.4" } }, "sha512-LkHEOGVZZXxGl8vDs+10k3DvP++SEoYEAJLRk6buTFi6kD7QekThV7xHS0j6gpnUCQ0zpud/gMDGiV4dQneLTg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jiji": ["jiji@0.1.7", "", { "dependencies": { "@deno/shim-deno": "~0.18.0" }, "bin": { "jiji": "esm/main.js" } }, "sha512-jXqvico7re/jOUJAA5Un+5axlW/qGXTLBreT7aQ32+SR3NImu7nGxols+AZUUqJMaSfvI+TBYymHm3FNRvxNHw=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "jsdoc-type-pratt-parser": ["jsdoc-type-pratt-parser@4.8.0", "", {}, "sha512-iZ8Bdb84lWRuGHamRXFyML07r21pcwBrLkHEuHgEY5UbCouBwv7ECknDRKzsQIXMiqpPymqtIf8TC/shYKB5rw=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "jsonc-eslint-parser": ["jsonc-eslint-parser@2.4.2", "", { "dependencies": { "acorn": "^8.5.0", "eslint-visitor-keys": "^3.0.0", "espree": "^9.0.0", "semver": "^7.3.5" } }, "sha512-1e4qoRgnn448pRuMvKGsFFymUCquZV0mpGgOyIKNgD3JVDTsVJyRBGH/Fm0tBb8WsWGgmB1mDe6/yJMQM37DUA=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lint-staged": ["lint-staged@16.2.7", "", { "dependencies": { "commander": "^14.0.2", "listr2": "^9.0.5", "micromatch": "^4.0.8", "nano-spawn": "^2.0.0", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.8.1" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow=="], + + "listr2": ["listr2@9.0.5", "", { "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g=="], + + "local-pkg": ["local-pkg@1.1.2", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], "lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="], + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], + "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nano-spawn": ["nano-spawn@2.0.0", "", {}, "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "napi-postinstall": ["napi-postinstall@0.3.4", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="], - "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + + "normalize-package-data": ["normalize-package-data@8.0.0", "", { "dependencies": { "hosted-git-info": "^9.0.0", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" } }, "sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], @@ -286,8 +778,26 @@ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "package-json-validator": ["package-json-validator@0.59.0", "", { "dependencies": { "semver": "^7.7.2", "validate-npm-package-license": "^3.0.4", "validate-npm-package-name": "^7.0.0", "yargs": "~18.0.0" }, "bin": { "pjv": "lib/bin/pjv.mjs" } }, "sha512-WBTDKtO9pBa9GmA1sPbQHqlWxRdnHNfLFIIA49PPgV7px/rG27gHX57DWy77qyu374fla4veaIHy+gA+qRRuug=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-json": ["parse-json@8.3.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "index-to-position": "^1.1.0", "type-fest": "^4.39.1" } }, "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], "pg": ["pg@8.16.3", "", { "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", "pg-protocol": "^1.10.3", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw=="], @@ -306,6 +816,12 @@ "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], + "pino": ["pino@10.1.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w=="], "pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="], @@ -316,6 +832,12 @@ "pino-std-serializers": ["pino-std-serializers@7.0.0", "", {}, "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="], + "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + + "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="], "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], @@ -326,6 +848,8 @@ "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], @@ -336,10 +860,20 @@ "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], + "read-package-up": ["read-package-up@12.0.0", "", { "dependencies": { "find-up-simple": "^1.0.1", "read-pkg": "^10.0.0", "type-fest": "^5.2.0" } }, "sha512-Q5hMVBYur/eQNWDdbF4/Wqqr9Bjvtrw2kjGxxBbKLbx8bVCL8gcArjTy8zDUuLGQicftpMuU0riQNcAsbtOVsw=="], + + "read-pkg": ["read-pkg@10.0.0", "", { "dependencies": { "@types/normalize-package-data": "^2.4.4", "normalize-package-data": "^8.0.0", "parse-json": "^8.3.0", "type-fest": "^5.2.0", "unicorn-magic": "^0.3.0" } }, "sha512-A70UlgfNdKI5NSvTTfHzLQj7NJRpJ4mT5tGafkllJ4wh71oYuGm/pzphHcmW4s35iox56KSK721AihodoXSc/A=="], + "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], @@ -348,16 +882,44 @@ "redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="], + "refa": ["refa@0.12.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.8.0" } }, "sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g=="], + + "regexp-ast-analysis": ["regexp-ast-analysis@0.7.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.8.0", "refa": "^0.12.1" } }, "sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A=="], + + "regexp-tree": ["regexp-tree@0.1.27", "", { "bin": { "regexp-tree": "bin/regexp-tree" } }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="], + + "regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], + + "requireindex": ["requireindex@1.2.0", "", {}, "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + "rosu-pp-js": ["rosu-pp-js@3.1.0", "", {}, "sha512-5YR/ar+XKLvAJAEvPhfEeBunDAY/qPFoey9b9ykvbNDR1K6FO77Mj7VblPpVs9O5TFRwecVSvOLFHMaY63l3YA=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + "scslre": ["scslre@0.3.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.8.0", "refa": "^0.12.0", "regexp-ast-analysis": "^0.7.0" } }, "sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ=="], + "secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="], + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], @@ -366,56 +928,216 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "simple-git-hooks": ["simple-git-hooks@2.13.1", "", { "bin": { "simple-git-hooks": "cli.js" } }, "sha512-WszCLXwT4h2k1ufIXAgsbiTOazqqevFCIncOuUBZJ91DdvWcC5+OFkluWRQPrcuSYd8fjq+o2y1QfWqYMoAToQ=="], + + "slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], + "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], + "sort-object-keys": ["sort-object-keys@2.0.1", "", {}, "sha512-R89fO+z3x7hiKPXX5P0qim+ge6Y60AjtlW+QQpRozrrNcR1lw9Pkpm5MLB56HoNvdcLHL4wbpq16OcvGpEDJIg=="], + + "sort-package-json": ["sort-package-json@3.6.0", "", { "dependencies": { "detect-indent": "^7.0.2", "detect-newline": "^4.0.1", "git-hooks-list": "^4.1.1", "is-plain-obj": "^4.1.0", "semver": "^7.7.3", "sort-object-keys": "^2.0.1", "tinyglobby": "^0.2.15" }, "bin": { "sort-package-json": "cli.js" } }, "sha512-fyJsPLhWvY7u2KsKPZn1PixbXp+1m7V8NWqU8CvgFRbMEX41Ffw1kD8n0CfJiGoaSfoAvbrqRRl/DcHO8omQOQ=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "spdx-correct": ["spdx-correct@3.2.0", "", { "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA=="], + + "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="], + + "spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="], + + "spdx-license-ids": ["spdx-license-ids@3.0.22", "", {}, "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ=="], + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + "stable-hash-x": ["stable-hash-x@0.2.0", "", {}, "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ=="], + "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + + "string-ts": ["string-ts@2.3.1", "", {}, "sha512-xSJq+BS52SaFFAVxuStmx6n5aYZU571uYUnUrPXkPFCfdHyZMMlbP2v2Wx5sNBnAVzq/2+0+mcBLBa3Xa5ubYw=="], + + "string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="], + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + + "strip-indent": ["strip-indent@4.1.1", "", {}, "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA=="], + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "synckit": ["synckit@0.11.11", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw=="], + + "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], + + "tailwind-api-utils": ["tailwind-api-utils@1.0.3", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "local-pkg": "^1.1.1" }, "peerDependencies": { "tailwindcss": "^3.3.0 || ^4.0.0 || ^4.0.0-beta" } }, "sha512-KpzUHkH1ug1sq4394SLJX38ZtpeTiqQ1RVyFTTSY2XuHsNSTWUkRo108KmyyrMWdDbQrLYkSHaNKj/a3bmA4sQ=="], + + "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], + + "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + "thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "token-types": ["token-types@6.1.1", "", { "dependencies": { "@borewit/text-codec": "^0.1.0", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ=="], + "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], + + "ts-declaration-location": ["ts-declaration-location@1.0.7", "", { "dependencies": { "picomatch": "^4.0.2" }, "peerDependencies": { "typescript": ">=4.0.0" } }, "sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA=="], + + "ts-pattern": ["ts-pattern@5.9.0", "", {}, "sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tsx": ["tsx@4.20.6", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg=="], - "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "type-fest": ["type-fest@5.3.1", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "typescript-eslint": ["typescript-eslint@8.50.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.50.1", "@typescript-eslint/parser": "8.50.1", "@typescript-eslint/typescript-estree": "8.50.1", "@typescript-eslint/utils": "8.50.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ=="], + + "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], + "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], + "unconfig": ["unconfig@7.4.2", "", { "dependencies": { "@quansync/fs": "^1.0.0", "defu": "^6.1.4", "jiti": "^2.6.1", "quansync": "^1.0.0", "unconfig-core": "7.4.2" } }, "sha512-nrMlWRQ1xdTjSnSUqvYqJzbTBFugoqHobQj58B2bc8qxHKBBHMNNsWQFP3Cd3/JZK907voM2geYPWqD4VK3MPQ=="], + + "unconfig-core": ["unconfig-core@7.4.2", "", { "dependencies": { "@quansync/fs": "^1.0.0", "quansync": "^1.0.0" } }, "sha512-VgPCvLWugINbXvMQDf8Jh0mlbvNjNC6eSUziHsBCMpxR05OPrNrvDnyatdMjRgcHaaNsCqz+wjNXxNw1kRLHUg=="], + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], + + "unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="], + + "validate-npm-package-name": ["validate-npm-package-name@7.0.1", "", {}, "sha512-BM0Upcemlce8/9+HE+/VpWqn3u3mYh6Om/FEC8yPMnEHwf710fW5Q6fhjT1SQyRlZD1G9CJbgfH+rWgAcIvjlQ=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], + + "yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], + + "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="], "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], + + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@deno/shim-deno/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], + + "@es-joy/jsdoccomment/jsdoc-type-pratt-parser": ["jsdoc-type-pratt-parser@7.0.0", "", {}, "sha512-c7YbokssPOSHmqTbSAmTtnVgAVa/7lumWNYqomgd5KOMyPrRve2anx6lonfOsXEQacqF9FKVUj7bLg4vRSvdYA=="], + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint-react/shared/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], + + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "@libsql/client-wasm/@libsql/libsql-wasm-experimental": ["@libsql/libsql-wasm-experimental@0.0.2", "", { "bundled": true, "bin": { "sqlite-wasm": "bin/index.js" } }, "sha512-xkiu88QwozGr3KEt9h0zeHLYUIWkeDchXmuOUW4/Wh/mRZkDlNtIIePAR0FiLl1j0o4OyTEOtPnvmaXQ5MNTKQ=="], - "@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="], + "@quansync/fs/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="], - "@tokenizer/inflate/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="], "@types/bun/bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "clean-regexp/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "elysia-rate-limit/debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], + + "esbuild-register/debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], + + "eslint-plugin-import-x/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "hosted-git-info/lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], + + "ioredis/debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], + + "jsonc-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "jsonc-eslint-parser/espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], + + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + + "parse-json/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + + "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "tsx/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "unconfig/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="], + + "unconfig-core/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="], + + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "yargs/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "@deno/shim-deno/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], @@ -462,7 +1184,19 @@ "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="], - "@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "@scalar/themes/@scalar/types/nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], + + "@scalar/themes/@scalar/types/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "elysia-rate-limit/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], + + "esbuild-register/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], + + "ioredis/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], + + "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], diff --git a/eslint.config.ts b/eslint.config.ts new file mode 100644 index 0000000..6e42418 --- /dev/null +++ b/eslint.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from "@richardscull/eslint-config"; + +export default defineConfig(); diff --git a/package.json b/package.json index 23be0ee..a79a5f5 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,24 @@ { "name": "observatory", + "type": "module", "version": "1.0.0", + "private": true, "description": "Observatory API", + "module": "server/src/app.js", "scripts": { - "start": "NODE_ENV=production bun run server/src/app.ts", - "dev": "NODE_ENV=development bun run --watch server/src/app.ts", - "lint": "bunx prettier -w ./server/src --config .prettierrc.json", - "test": "docker-compose -f docker-compose.tests.yml up -d && bun test --env-file=.env.test", - "setup": "bun run docker:dev && bun run db:update", - "db:update": "bun run db:generate && bun run db:push", "db:generate": "bun --bun drizzle-kit generate --config server/src/database/config.ts", - "db:push": "bun --bun drizzle-kit push --config server/src/database/config.ts", "db:migration": "bun run server/src/database/migrate.ts", - "docker:dev": "docker-compose -f docker-compose.dev.yml up -d" + "db:push": "bun --bun drizzle-kit push --config server/src/database/config.ts", + "db:update": "bun run db:generate && bun run db:push", + "dev": "NODE_ENV=development bun run --watch server/src/app.ts", + "docker:dev": "docker-compose -f docker-compose.dev.yml up -d", + "lint": "eslint --fix", + "setup": "bun run docker:dev && bun run db:update", + "start": "NODE_ENV=production bun run server/src/app.ts", + "test": "docker-compose -f docker-compose.tests.yml up -d && bun test --env-file=.env.test" + }, + "peerDependencies": { + "typescript": "^5" }, "dependencies": { "@bogeychan/elysia-logger": "^0.1.4", @@ -39,18 +45,24 @@ "rosu-pp-js": "^3.1.0" }, "devDependencies": { + "@richardscull/eslint-config": "^1.0.5", "@types/bun": "^1.3.4", "@types/pg": "^8.11.10", "@types/qs": "^6.9.18", "bun-types": "latest", "drizzle-kit": "^0.26.2", + "eslint": "^9.39.2", + "jiji": "^0.1.7", + "lint-staged": "^16.2.7", "prettier": "^3.3.3", - "tsx": "^4.19.2" - }, - "module": "server/src/app.js", - "type": "module", - "private": true, - "peerDependencies": { + "simple-git-hooks": "^2.13.1", + "tsx": "^4.19.2", "typescript": "^5" + }, + "simple-git-hooks": { + "pre-commit": "npx lint-staged" + }, + "lint-staged": { + "*": "eslint --fix" } } diff --git a/tsconfig.json b/tsconfig.json index 124a55d..f6df89b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,105 +1,105 @@ { - "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - /* Language and Environment */ - "target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + /* Language and Environment */ + "target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - /* Modules */ - "module": "ES2022" /* Specify what module code is generated. */, - // "rootDir": "./", /* Specify the root folder within your source files. */ - "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - "types": [ - "bun-types" - ] /* Specify type package names to be included without being referenced in a source file. */, - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* Modules */ + "module": "ES2022" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + "types": [ + "bun-types" + ] /* Specify type package names to be included without being referenced in a source file. */, + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - "noEmit": true /* Disable emitting files from a compilation. */, - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + "noEmit": true /* Disable emitting files from a compilation. */, + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - /* Type Checking */ - "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } } From 327c6438e8e2caf99527c518636ba0983600dbd4 Mon Sep 17 00:00:00 2001 From: richardscull <106016833+richardscull@users.noreply.github.com> Date: Thu, 25 Dec 2025 08:31:09 +0200 Subject: [PATCH 2/3] fix: lint --- server/src/app.ts | 15 +- server/src/config.ts | 193 +- server/src/controllers/api/index.ts | 369 +- server/src/controllers/calculator/index.ts | 255 +- server/src/controllers/d/index.ts | 55 +- server/src/controllers/index.ts | 63 +- server/src/controllers/osu/index.ts | 47 +- .../core/abstracts/api/base-api.abstract.ts | 444 +- .../src/core/abstracts/api/base-api.types.ts | 24 +- .../abstracts/client/base-client.abstract.ts | 278 +- .../abstracts/client/base-client.types.ts | 79 +- .../ratelimiter/rate-limiter.abstract.ts | 703 +- .../ratelimiter/rate-limiter.types.ts | 26 +- .../beatmaps.download/osulabs-client.types.ts | 32 +- .../beatmaps.download/osulabs.client.ts | 344 +- .../domains/catboy.best/mino-client.types.ts | 32 +- .../core/domains/catboy.best/mino.client.ts | 406 +- .../core/domains/gatari.pw/gatari.client.ts | 64 +- server/src/core/domains/index.ts | 6 +- .../domains/nerinyan.moe/nerinyan.client.ts | 74 +- .../domains/osu.direct/direct-client.types.ts | 5 +- .../core/domains/osu.direct/direct.client.ts | 213 +- .../osu.ppy.sh/bancho-client.service.ts | 94 +- .../domains/osu.ppy.sh/bancho-client.types.ts | 52 +- .../core/domains/osu.ppy.sh/bancho.client.ts | 333 +- .../beatmaps/beatmaps-manager.types.ts | 10 +- .../managers/beatmaps/beatmaps.manager.ts | 598 +- .../managers/calculator/calculator.manager.ts | 139 +- .../managers/calculator/calculator.service.ts | 130 +- .../managers/calculator/calculator.types.ts | 29 +- .../mirrors/mirrors-manager.service.ts | 254 +- .../core/managers/mirrors/mirrors.manager.ts | 643 +- .../managers/storage/storage-cache.service.ts | 437 +- .../managers/storage/storage-files.service.ts | 396 +- .../core/managers/storage/storage.manager.ts | 353 +- server/src/core/services/compare.service.ts | 212 +- server/src/core/services/convert.service.ts | 360 +- server/src/core/services/stats.service.ts | 66 +- server/src/database/client.ts | 54 +- server/src/database/config.ts | 39 +- server/src/database/migrate.ts | 26 +- .../migrations/meta/0000_snapshot.json | 296 +- .../migrations/meta/0001_snapshot.json | 2 +- .../migrations/meta/0002_snapshot.json | 2 +- .../migrations/meta/0003_snapshot.json | 2 +- .../migrations/meta/0004_snapshot.json | 2 +- .../migrations/meta/0005_snapshot.json | 2 +- .../migrations/meta/0006_snapshot.json | 2 +- .../migrations/meta/0007_snapshot.json | 2 +- .../migrations/meta/0008_snapshot.json | 2 +- .../migrations/meta/0009_snapshot.json | 2 +- .../migrations/meta/0010_snapshot.json | 2 +- .../database/migrations/meta/_journal.json | 2 +- server/src/database/models/beatmap.ts | 275 +- server/src/database/models/beatmapOsuFile.ts | 122 +- server/src/database/models/beatmapset.ts | 473 +- server/src/database/models/beatmapsetFile.ts | 128 +- server/src/database/models/mirrors.ts | 30 +- server/src/database/models/requests.ts | 134 +- server/src/database/schema.ts | 276 +- server/src/plugins/beatmapManager.ts | 13 +- server/src/plugins/calculatorManager.ts | 9 +- server/src/plugins/redisInstance.ts | 15 +- server/src/plugins/statsService.ts | 9 +- server/src/setup.ts | 202 +- server/src/types/benchmark.ts | 4 +- server/src/types/general/beatmap.ts | 258 +- server/src/types/general/failtimes.ts | 8 +- server/src/types/general/gameMod.ts | 64 +- server/src/types/general/gameMode.ts | 32 +- server/src/types/general/rankStatus.ts | 56 +- server/src/types/general/user.ts | 559 +- server/src/types/redis.ts | 14 +- server/src/types/stats.ts | 12 +- server/src/types/utils.ts | 10 +- server/src/utils/array.ts | 29 +- server/src/utils/beatmap.ts | 8 +- server/src/utils/date.ts | 4 +- server/src/utils/logger.ts | 31 +- server/src/utils/mirrors-stats.ts | 110 +- server/src/utils/stats.ts | 30 +- server/tests/calculator.service.test.ts | 778 +- server/tests/compitability.production.test.ts | 598 +- server/tests/data/mirror.tests.json | 6417 +++++++++++++---- server/tests/mirrors.manager.test.ts | 2805 +++---- server/tests/stats.endpoint.test.ts | 623 +- server/tests/utils/faker.generator.ts | 418 +- server/tests/utils/mocker.ts | 457 +- 88 files changed, 13579 insertions(+), 9702 deletions(-) diff --git a/server/src/app.ts b/server/src/app.ts index 4633097..28c7f1f 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -1,16 +1,15 @@ -import { Elysia } from 'elysia'; +import { Elysia } from "elysia"; -import log from './utils/logger'; -import setup from './setup'; -import config from './config'; +import config from "./config"; +import setup from "./setup"; +import log from "./utils/logger"; const port = config.PORT; const app = new Elysia() - .use(setup()) - .listen({ port }, ({ hostname, port }) => - log.info(`🔭 Observatory is running at http://${hostname}:${port}`), - ); + .use(setup()) + .listen({ port }, ({ hostname, port }) => + log.info(`🔭 Observatory is running at http://${hostname}:${port}`)); export { app }; export type App = typeof app; diff --git a/server/src/config.ts b/server/src/config.ts index 5fb7a08..7a2d497 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -1,119 +1,120 @@ -import { color } from 'bun'; -import dotenv from 'dotenv'; -import { exit } from 'process'; +import { exit } from "node:process"; + +import { color } from "bun"; +import dotenv from "dotenv"; dotenv.config(); -export type Mirror = - | 'direct' - | 'bancho' - | 'mino' - | 'osulabs' - | 'gatari' - | 'nerinyan'; +export type Mirror + = | "direct" + | "bancho" + | "mino" + | "osulabs" + | "gatari" + | "nerinyan"; const { - PORT, - POSTGRES_USER, - POSTGRES_PASSWORD, - POSTGRES_HOST, - POSTGRES_PORT, - POSTGRES_DB, - REDIS_PORT, - REDIS_HOST, - BANCHO_CLIENT_SECRET, - BANCHO_CLIENT_ID, - DEBUG_MODE, - LOKI_HOST, - IGNORE_RATELIMIT_KEY, - RATELIMIT_CALLS_PER_WINDOW, - RATELIMIT_TIME_WINDOW, - OSZ_FILES_LIFE_SPAN, - MIRRORS_TO_IGNORE, - DISABLE_SAFE_RATELIMIT_MODE, - DISABLE_DAILY_RATE_LIMIT, - ENABLE_CRON_TO_CLEAR_OUTDATED_BEATMAPS, + PORT, + POSTGRES_USER, + POSTGRES_PASSWORD, + POSTGRES_HOST, + POSTGRES_PORT, + POSTGRES_DB, + REDIS_PORT, + REDIS_HOST, + BANCHO_CLIENT_SECRET, + BANCHO_CLIENT_ID, + DEBUG_MODE, + LOKI_HOST, + IGNORE_RATELIMIT_KEY, + RATELIMIT_CALLS_PER_WINDOW, + RATELIMIT_TIME_WINDOW, + OSZ_FILES_LIFE_SPAN, + MIRRORS_TO_IGNORE, + DISABLE_SAFE_RATELIMIT_MODE, + DISABLE_DAILY_RATE_LIMIT, + ENABLE_CRON_TO_CLEAR_OUTDATED_BEATMAPS, } = process.env; if (!POSTGRES_USER || !POSTGRES_PASSWORD) { - console.error( - `${color('#ff0000')} Missing required environment variables for Postgres`, - ); - exit(1); + console.error( + `${color("#ff0000")} Missing required environment variables for Postgres`, + ); + exit(1); } if (!BANCHO_CLIENT_SECRET || !BANCHO_CLIENT_ID) { - console.error( + console.error( `${color( - '#ff0000', + "#ff0000", )} Missing required environment variables for osu! Bancho. It will be disabled`, - ); + ); } const config: { - PORT: string; - POSTGRES_USER: string; - POSTGRES_PASSWORD: string; - POSTGRES_HOST: string; - POSTGRES_PORT: string; - POSTGRES_DB: string; - REDIS_PORT: number; - BANCHO_CLIENT_SECRET?: string; - BANCHO_CLIENT_ID?: string; - REDIS_HOST?: string; - LOKI_HOST?: string; - IGNORE_RATELIMIT_KEY?: string; - RATELIMIT_CALLS_PER_WINDOW: number; - RATELIMIT_TIME_WINDOW: number; - OSZ_FILES_LIFE_SPAN: number; - IsProduction: boolean; - IsAutomatedTesting: boolean; - IsDebug: boolean; - UseBancho: boolean; - MirrorsToIgnore: string[]; - DisableSafeRatelimitMode: boolean; - DisableDailyRateLimit: boolean; - EnableCronToClearOutdatedBeatmaps: boolean; + PORT: string; + POSTGRES_USER: string; + POSTGRES_PASSWORD: string; + POSTGRES_HOST: string; + POSTGRES_PORT: string; + POSTGRES_DB: string; + REDIS_PORT: number; + BANCHO_CLIENT_SECRET?: string; + BANCHO_CLIENT_ID?: string; + REDIS_HOST?: string; + LOKI_HOST?: string; + IGNORE_RATELIMIT_KEY?: string; + RATELIMIT_CALLS_PER_WINDOW: number; + RATELIMIT_TIME_WINDOW: number; + OSZ_FILES_LIFE_SPAN: number; + IsProduction: boolean; + IsAutomatedTesting: boolean; + IsDebug: boolean; + UseBancho: boolean; + MirrorsToIgnore: string[]; + DisableSafeRatelimitMode: boolean; + DisableDailyRateLimit: boolean; + EnableCronToClearOutdatedBeatmaps: boolean; } = { - PORT: PORT || '3000', - POSTGRES_USER: POSTGRES_USER || 'admin', - POSTGRES_PASSWORD: POSTGRES_PASSWORD || 'admin', - POSTGRES_HOST: POSTGRES_HOST || '0.0.0.0', - POSTGRES_PORT: POSTGRES_PORT || '5432', - POSTGRES_DB: POSTGRES_DB || 'observatory', - REDIS_PORT: Number(REDIS_PORT) || 6379, - BANCHO_CLIENT_SECRET, - BANCHO_CLIENT_ID, - REDIS_HOST, - LOKI_HOST: LOKI_HOST, - IGNORE_RATELIMIT_KEY: IGNORE_RATELIMIT_KEY, - RATELIMIT_CALLS_PER_WINDOW: Number(RATELIMIT_CALLS_PER_WINDOW) || 100, - RATELIMIT_TIME_WINDOW: Number(RATELIMIT_TIME_WINDOW) || 20 * 1000, - OSZ_FILES_LIFE_SPAN: Number(OSZ_FILES_LIFE_SPAN) || 24, - IsProduction: Bun.env.NODE_ENV === 'production', - IsAutomatedTesting: Bun.env.NODE_ENV === 'test', - IsDebug: DEBUG_MODE === 'true', - UseBancho: BANCHO_CLIENT_SECRET && BANCHO_CLIENT_ID ? true : false, - MirrorsToIgnore: MIRRORS_TO_IGNORE?.split(',').map((v) => v.trim()) ?? [], - DisableSafeRatelimitMode: DISABLE_SAFE_RATELIMIT_MODE === 'true', - DisableDailyRateLimit: DISABLE_DAILY_RATE_LIMIT === 'true', - EnableCronToClearOutdatedBeatmaps: - ENABLE_CRON_TO_CLEAR_OUTDATED_BEATMAPS === 'true', + PORT: PORT || "3000", + POSTGRES_USER: POSTGRES_USER || "admin", + POSTGRES_PASSWORD: POSTGRES_PASSWORD || "admin", + POSTGRES_HOST: POSTGRES_HOST || "0.0.0.0", + POSTGRES_PORT: POSTGRES_PORT || "5432", + POSTGRES_DB: POSTGRES_DB || "observatory", + REDIS_PORT: Number(REDIS_PORT) || 6379, + BANCHO_CLIENT_SECRET, + BANCHO_CLIENT_ID, + REDIS_HOST, + LOKI_HOST, + IGNORE_RATELIMIT_KEY, + RATELIMIT_CALLS_PER_WINDOW: Number(RATELIMIT_CALLS_PER_WINDOW) || 100, + RATELIMIT_TIME_WINDOW: Number(RATELIMIT_TIME_WINDOW) || 20 * 1000, + OSZ_FILES_LIFE_SPAN: Number(OSZ_FILES_LIFE_SPAN) || 24, + IsProduction: Bun.env.NODE_ENV === "production", + IsAutomatedTesting: Bun.env.NODE_ENV === "test", + IsDebug: DEBUG_MODE === "true", + UseBancho: BANCHO_CLIENT_SECRET && BANCHO_CLIENT_ID ? true : false, + MirrorsToIgnore: MIRRORS_TO_IGNORE?.split(",").map(v => v.trim()) ?? [], + DisableSafeRatelimitMode: DISABLE_SAFE_RATELIMIT_MODE === "true", + DisableDailyRateLimit: DISABLE_DAILY_RATE_LIMIT === "true", + EnableCronToClearOutdatedBeatmaps: + ENABLE_CRON_TO_CLEAR_OUTDATED_BEATMAPS === "true", }; export const observationaryConfigPublic = { - RATELIMIT_CALLS_PER_WINDOW: Number(RATELIMIT_CALLS_PER_WINDOW) || 100, - RATELIMIT_TIME_WINDOW: Number(RATELIMIT_TIME_WINDOW) || 20 * 1000, - OSZ_FILES_LIFE_SPAN: Number(OSZ_FILES_LIFE_SPAN) || 24, - IsProduction: Bun.env.NODE_ENV === 'production', - IsAutomatedTesting: Bun.env.NODE_ENV === 'test', - IsDebug: DEBUG_MODE === 'true', - UseBancho: BANCHO_CLIENT_SECRET && BANCHO_CLIENT_ID ? true : false, - MirrorsToIgnore: MIRRORS_TO_IGNORE?.split(',').map((v) => v.trim()) ?? [], - DisableSafeRatelimitMode: DISABLE_SAFE_RATELIMIT_MODE === 'true', - DisableDailyRateLimit: DISABLE_DAILY_RATE_LIMIT === 'true', - EnableCronToClearOutdatedBeatmaps: - ENABLE_CRON_TO_CLEAR_OUTDATED_BEATMAPS === 'true', + RATELIMIT_CALLS_PER_WINDOW: Number(RATELIMIT_CALLS_PER_WINDOW) || 100, + RATELIMIT_TIME_WINDOW: Number(RATELIMIT_TIME_WINDOW) || 20 * 1000, + OSZ_FILES_LIFE_SPAN: Number(OSZ_FILES_LIFE_SPAN) || 24, + IsProduction: Bun.env.NODE_ENV === "production", + IsAutomatedTesting: Bun.env.NODE_ENV === "test", + IsDebug: DEBUG_MODE === "true", + UseBancho: BANCHO_CLIENT_SECRET && BANCHO_CLIENT_ID ? true : false, + MirrorsToIgnore: MIRRORS_TO_IGNORE?.split(",").map(v => v.trim()) ?? [], + DisableSafeRatelimitMode: DISABLE_SAFE_RATELIMIT_MODE === "true", + DisableDailyRateLimit: DISABLE_DAILY_RATE_LIMIT === "true", + EnableCronToClearOutdatedBeatmaps: + ENABLE_CRON_TO_CLEAR_OUTDATED_BEATMAPS === "true", }; export default config; diff --git a/server/src/controllers/api/index.ts b/server/src/controllers/api/index.ts index abfddd8..c7a31b9 100644 --- a/server/src/controllers/api/index.ts +++ b/server/src/controllers/api/index.ts @@ -1,188 +1,191 @@ -import { t } from 'elysia'; -import { App } from '../../app'; -import { BeatmapsManagerPlugin } from '../../plugins/beatmapManager'; +import { t } from "elysia"; + +import type { App } from "../../app"; +import { BeatmapsManagerPlugin } from "../../plugins/beatmapManager"; export default (app: App) => { - app.use(BeatmapsManagerPlugin) - .get( - 'v2/b/:id', - async ({ - BeatmapsManagerInstance, - params: { id }, - query: { full, allowMissingNonBeatmapValues }, - set, - }) => { - const beatmap = await BeatmapsManagerInstance.getBeatmap({ - beatmapId: id, - allowMissingNonBeatmapValues: + app.use(BeatmapsManagerPlugin) + .get( + "v2/b/:id", + async ({ + BeatmapsManagerInstance, + params: { id }, + query: { full, allowMissingNonBeatmapValues }, + set, + }) => { + const beatmap = await BeatmapsManagerInstance.getBeatmap({ + beatmapId: id, + allowMissingNonBeatmapValues: full || allowMissingNonBeatmapValues, - }); - - if (beatmap.source) { - set.headers['X-Data-Source'] = beatmap.source; - } - - const { source: _, ...responseBeatmap } = beatmap; - - if (!full) return responseBeatmap?.data ?? responseBeatmap; - - const beatmapset = await BeatmapsManagerInstance.getBeatmapSet({ - beatmapSetId: responseBeatmap.data?.beatmapset_id, - }); - - if (beatmapset.source) { - set.headers['X-Data-Source'] = beatmapset.source; - } - - const { source: __, ...responseBeatmapset } = beatmapset; - - return responseBeatmapset?.data ?? responseBeatmapset; - }, - { - params: t.Object({ - id: t.Numeric(), - }), - query: t.Object({ - full: t.Optional(t.Boolean()), - allowMissingNonBeatmapValues: t.Optional(t.Boolean()), // TODO: Ideally, we should have shortBeatmap endpoint which would return only beatmap values. - }), - tags: ['v2'], - }, - ) - .get( - 'v2/md5/:hash', - async ({ - BeatmapsManagerInstance, - params: { hash }, - query: { full, allowMissingNonBeatmapValues }, - set, - }) => { - const beatmap = await BeatmapsManagerInstance.getBeatmap({ - beatmapHash: hash, - allowMissingNonBeatmapValues: + }); + + if (beatmap.source) { + set.headers["X-Data-Source"] = beatmap.source; + } + + const { source: _, ...responseBeatmap } = beatmap; + + if (!full) + return responseBeatmap?.data ?? responseBeatmap; + + const beatmapset = await BeatmapsManagerInstance.getBeatmapSet({ + beatmapSetId: responseBeatmap.data?.beatmapset_id, + }); + + if (beatmapset.source) { + set.headers["X-Data-Source"] = beatmapset.source; + } + + const { source: __, ...responseBeatmapset } = beatmapset; + + return responseBeatmapset?.data ?? responseBeatmapset; + }, + { + params: t.Object({ + id: t.Numeric(), + }), + query: t.Object({ + full: t.Optional(t.Boolean()), + allowMissingNonBeatmapValues: t.Optional(t.Boolean()), // TODO: Ideally, we should have shortBeatmap endpoint which would return only beatmap values. + }), + tags: ["v2"], + }, + ) + .get( + "v2/md5/:hash", + async ({ + BeatmapsManagerInstance, + params: { hash }, + query: { full, allowMissingNonBeatmapValues }, + set, + }) => { + const beatmap = await BeatmapsManagerInstance.getBeatmap({ + beatmapHash: hash, + allowMissingNonBeatmapValues: full || allowMissingNonBeatmapValues, - }); - - if (beatmap.source) { - set.headers['X-Data-Source'] = beatmap.source; - } - - const { source: _, ...responseBeatmap } = beatmap; - if (!full) return responseBeatmap?.data ?? responseBeatmap; - - const beatmapset = await BeatmapsManagerInstance.getBeatmapSet({ - beatmapSetId: beatmap.data?.beatmapset_id, - }); - - if (beatmapset.source) { - set.headers['X-Data-Source'] = beatmapset.source; - } - - const { source: __, ...responseBeatmapset } = beatmapset; - return responseBeatmapset?.data ?? responseBeatmapset; - }, - { - params: t.Object({ - hash: t.String(), - }), - query: t.Object({ - full: t.Optional(t.BooleanString()), - allowMissingNonBeatmapValues: t.Optional(t.Boolean()), // TODO: Ideally, we should have shortBeatmap endpoint which would return only beatmap values. - }), - tags: ['v2'], - }, - ) - .get( - 'v2/s/:id', - async ({ BeatmapsManagerInstance, params: { id }, set }) => { - const data = await BeatmapsManagerInstance.getBeatmapSet({ - beatmapSetId: id, - }); - - if (data.source) { - set.headers['X-Data-Source'] = data.source; - } - - const { source: _, ...response } = data; - return response?.data ?? response; - }, - { - params: t.Object({ - id: t.Numeric(), - }), - tags: ['v2'], - }, - ) - .get( - 'v2/search', - async ({ BeatmapsManagerInstance, query, set }) => { - // TODO: Add another search endpoint which would parse cursors instead of pages, to create compatibility with bancho api; - - const data = await BeatmapsManagerInstance.searchBeatmapsets({ - ...query, - }); - - if (data.source) { - set.headers['X-Data-Source'] = data.source; - } - - const { source: _, ...response } = data; - return response?.data ?? response; - }, - { - query: t.Object({ - query: t.Optional(t.String()), - limit: t.Optional(t.Numeric()), - offset: t.Optional(t.Numeric()), - status: t.Optional(t.Array(t.Numeric())), - mode: t.Optional(t.Numeric()), - }), - tags: ['v2'], - }, - ) - .get( - 'v2/beatmaps', - async ({ BeatmapsManagerInstance, query, set }) => { - const data = await BeatmapsManagerInstance.getBeatmaps({ - ids: query.ids, - }); - - if (data.source) { - set.headers['X-Data-Source'] = data.source; - } - - const { source: _, ...response } = data; - return response?.data ?? response; - }, - { - query: t.Object({ - ids: t.Array(t.Numeric()), - }), - tags: ['v2'], - }, - ) - .get( - 'v2/beatmapsets', - async ({ BeatmapsManagerInstance, query, set }) => { - const data = - await BeatmapsManagerInstance.getBeatmapsetsByBeatmapIds({ - beatmapIds: query.beatmapIds, - }); - - if (data.source) { - set.headers['X-Data-Source'] = data.source; - } - - const { source: _, ...response } = data; - return response?.data ?? response; - }, - { - query: t.Object({ - beatmapIds: t.Array(t.Numeric()), - }), - tags: ['v2'], - }, - ); - - return app; + }); + + if (beatmap.source) { + set.headers["X-Data-Source"] = beatmap.source; + } + + const { source: _, ...responseBeatmap } = beatmap; + if (!full) + return responseBeatmap?.data ?? responseBeatmap; + + const beatmapset = await BeatmapsManagerInstance.getBeatmapSet({ + beatmapSetId: beatmap.data?.beatmapset_id, + }); + + if (beatmapset.source) { + set.headers["X-Data-Source"] = beatmapset.source; + } + + const { source: __, ...responseBeatmapset } = beatmapset; + return responseBeatmapset?.data ?? responseBeatmapset; + }, + { + params: t.Object({ + hash: t.String(), + }), + query: t.Object({ + full: t.Optional(t.BooleanString()), + allowMissingNonBeatmapValues: t.Optional(t.Boolean()), // TODO: Ideally, we should have shortBeatmap endpoint which would return only beatmap values. + }), + tags: ["v2"], + }, + ) + .get( + "v2/s/:id", + async ({ BeatmapsManagerInstance, params: { id }, set }) => { + const data = await BeatmapsManagerInstance.getBeatmapSet({ + beatmapSetId: id, + }); + + if (data.source) { + set.headers["X-Data-Source"] = data.source; + } + + const { source: _, ...response } = data; + return response?.data ?? response; + }, + { + params: t.Object({ + id: t.Numeric(), + }), + tags: ["v2"], + }, + ) + .get( + "v2/search", + async ({ BeatmapsManagerInstance, query, set }) => { + // TODO: Add another search endpoint which would parse cursors instead of pages, to create compatibility with bancho api; + + const data = await BeatmapsManagerInstance.searchBeatmapsets({ + ...query, + }); + + if (data.source) { + set.headers["X-Data-Source"] = data.source; + } + + const { source: _, ...response } = data; + return response?.data ?? response; + }, + { + query: t.Object({ + query: t.Optional(t.String()), + limit: t.Optional(t.Numeric()), + offset: t.Optional(t.Numeric()), + status: t.Optional(t.Array(t.Numeric())), + mode: t.Optional(t.Numeric()), + }), + tags: ["v2"], + }, + ) + .get( + "v2/beatmaps", + async ({ BeatmapsManagerInstance, query, set }) => { + const data = await BeatmapsManagerInstance.getBeatmaps({ + ids: query.ids, + }); + + if (data.source) { + set.headers["X-Data-Source"] = data.source; + } + + const { source: _, ...response } = data; + return response?.data ?? response; + }, + { + query: t.Object({ + ids: t.Array(t.Numeric()), + }), + tags: ["v2"], + }, + ) + .get( + "v2/beatmapsets", + async ({ BeatmapsManagerInstance, query, set }) => { + const data + = await BeatmapsManagerInstance.getBeatmapsetsByBeatmapIds({ + beatmapIds: query.beatmapIds, + }); + + if (data.source) { + set.headers["X-Data-Source"] = data.source; + } + + const { source: _, ...response } = data; + return response?.data ?? response; + }, + { + query: t.Object({ + beatmapIds: t.Array(t.Numeric()), + }), + tags: ["v2"], + }, + ); + + return app; }; diff --git a/server/src/controllers/calculator/index.ts b/server/src/controllers/calculator/index.ts index 4247728..62d7002 100644 --- a/server/src/controllers/calculator/index.ts +++ b/server/src/controllers/calculator/index.ts @@ -1,137 +1,138 @@ -import { t } from 'elysia'; -import { App } from '../../app'; -import { CalculatorManagerPlugin } from '../../plugins/calculatorManager'; -import { - Score, - ScoreShort, -} from '../../core/managers/calculator/calculator.types'; -import { TryConvertToGamemode } from '../../utils/beatmap'; +import { t } from "elysia"; + +import type { App } from "../../app"; +import type { + Score, + ScoreShort, +} from "../../core/managers/calculator/calculator.types"; +import { CalculatorManagerPlugin } from "../../plugins/calculatorManager"; +import { TryConvertToGamemode } from "../../utils/beatmap"; export default (app: App) => { - app.use(CalculatorManagerPlugin) - .get( - '/beatmap/:id', - async ({ - CalculatorManagerInstance, - params: { id }, - query: { - acc, - mode, - mods, - combo, - misses, - isScoreFailed, - isPlayedOnLazer, - }, - }) => { - const scores: ScoreShort[] = []; - const beatmapMode = TryConvertToGamemode(mode); + app.use(CalculatorManagerPlugin) + .get( + "/beatmap/:id", + async ({ + CalculatorManagerInstance, + params: { id }, + query: { + acc, + mode, + mods, + combo, + misses, + isScoreFailed, + isPlayedOnLazer, + }, + }) => { + const scores: ScoreShort[] = []; + const beatmapMode = TryConvertToGamemode(mode); - for (let accuracy of acc ?? [100]) { - scores.push({ - accuracy, - mode: beatmapMode, - mods: mods, - combo, - misses, - isScoreFailed: isScoreFailed ?? false, - isLazer: isPlayedOnLazer ?? false, - }); - } + for (const accuracy of acc ?? [100]) { + scores.push({ + accuracy, + mode: beatmapMode, + mods, + combo, + misses, + isScoreFailed: isScoreFailed ?? false, + isLazer: isPlayedOnLazer ?? false, + }); + } - const results = - await CalculatorManagerInstance.CalculateBeatmapPerformances( - id, - scores, - ); + const results + = await CalculatorManagerInstance.CalculateBeatmapPerformances( + id, + scores, + ); - return results; - }, - { - params: t.Object({ - id: t.Number(), - }), - query: t.Object({ - acc: t.Optional( - t.Array(t.Numeric(), { minItems: 1, maxItems: 5 }), - ), - mode: t.Optional(t.Numeric()), - mods: t.Optional(t.Numeric()), - combo: t.Optional(t.Numeric()), - misses: t.Optional(t.Numeric()), - isScoreFailed: t.Optional(t.BooleanString()), - isPlayedOnLazer: t.Optional(t.BooleanString()), - }), - tags: ['Calculators'], - }, - ) - .post( - '/score', - async ({ - CalculatorManagerInstance, - body: { - beatmapId, - beatmapHash, - mode, - acc, - mods, - combo, - n300, - nGeki, - n100, - nKatu, - n50, - misses, - isScoreFailed, - isPlayedOnLazer, - }, - }) => { - const beatmapMode = TryConvertToGamemode(mode); + return results; + }, + { + params: t.Object({ + id: t.Number(), + }), + query: t.Object({ + acc: t.Optional( + t.Array(t.Numeric(), { minItems: 1, maxItems: 5 }), + ), + mode: t.Optional(t.Numeric()), + mods: t.Optional(t.Numeric()), + combo: t.Optional(t.Numeric()), + misses: t.Optional(t.Numeric()), + isScoreFailed: t.Optional(t.BooleanString()), + isPlayedOnLazer: t.Optional(t.BooleanString()), + }), + tags: ["Calculators"], + }, + ) + .post( + "/score", + async ({ + CalculatorManagerInstance, + body: { + beatmapId, + beatmapHash, + mode, + acc, + mods, + combo, + n300, + nGeki, + n100, + nKatu, + n50, + misses, + isScoreFailed, + isPlayedOnLazer, + }, + }) => { + const beatmapMode = TryConvertToGamemode(mode); - const score: Score = { - accuracy: acc, - mode: beatmapMode, - mods: mods, - n300: n300, - nGeki: nGeki, - n100: n100, - nKatu: nKatu, - n50: n50, - combo, - misses, - isScoreFailed: isScoreFailed ?? false, - isLazer: isPlayedOnLazer ?? false, - }; + const score: Score = { + accuracy: acc, + mode: beatmapMode, + mods, + n300, + nGeki, + n100, + nKatu, + n50, + combo, + misses, + isScoreFailed: isScoreFailed ?? false, + isLazer: isPlayedOnLazer ?? false, + }; - const results = - await CalculatorManagerInstance.CalculateScorePerformance( - beatmapId, - score, - beatmapHash, - ); + const results + = await CalculatorManagerInstance.CalculateScorePerformance( + beatmapId, + score, + beatmapHash, + ); - return results; - }, - { - body: t.Object({ - beatmapId: t.Numeric(), - beatmapHash: t.Optional(t.String()), - acc: t.Optional(t.Numeric()), - combo: t.Optional(t.Numeric()), - n300: t.Optional(t.Numeric()), - nGeki: t.Optional(t.Numeric()), - n100: t.Optional(t.Numeric()), - nKatu: t.Optional(t.Numeric()), - n50: t.Optional(t.Numeric()), - misses: t.Optional(t.Numeric()), - mode: t.Optional(t.Numeric()), - mods: t.Optional(t.Numeric()), - isScoreFailed: t.Optional(t.Boolean()), - isPlayedOnLazer: t.Optional(t.Boolean()), - }), - tags: ['Calculators'], - }, - ); + return results; + }, + { + body: t.Object({ + beatmapId: t.Numeric(), + beatmapHash: t.Optional(t.String()), + acc: t.Optional(t.Numeric()), + combo: t.Optional(t.Numeric()), + n300: t.Optional(t.Numeric()), + nGeki: t.Optional(t.Numeric()), + n100: t.Optional(t.Numeric()), + nKatu: t.Optional(t.Numeric()), + n50: t.Optional(t.Numeric()), + misses: t.Optional(t.Numeric()), + mode: t.Optional(t.Numeric()), + mods: t.Optional(t.Numeric()), + isScoreFailed: t.Optional(t.Boolean()), + isPlayedOnLazer: t.Optional(t.Boolean()), + }), + tags: ["Calculators"], + }, + ); - return app; + return app; }; diff --git a/server/src/controllers/d/index.ts b/server/src/controllers/d/index.ts index e63705b..b7fafcb 100644 --- a/server/src/controllers/d/index.ts +++ b/server/src/controllers/d/index.ts @@ -1,32 +1,33 @@ -import { t } from 'elysia'; -import { App } from '../../app'; -import { BeatmapsManagerPlugin } from '../../plugins/beatmapManager'; +import { t } from "elysia"; + +import type { App } from "../../app"; +import { BeatmapsManagerPlugin } from "../../plugins/beatmapManager"; export default (app: App) => { - app.use(BeatmapsManagerPlugin).get( - '/:id', - async ({ BeatmapsManagerInstance, params: { id }, query, set }) => - BeatmapsManagerInstance.downloadBeatmapSet({ - beatmapSetId: id, - noVideo: query.noVideo, - }).then((res) => { - if (res.source) { - set.headers['X-Data-Source'] = res.source; - } + app.use(BeatmapsManagerPlugin).get( + "/:id", + async ({ BeatmapsManagerInstance, params: { id }, query, set }) => + BeatmapsManagerInstance.downloadBeatmapSet({ + beatmapSetId: id, + noVideo: query.noVideo, + }).then((res) => { + if (res.source) { + set.headers["X-Data-Source"] = res.source; + } - const { source: _, ...response } = res; - return response?.data ?? response; - }), - { - params: t.Object({ - id: t.Number(), - }), - query: t.Object({ - noVideo: t.Optional(t.BooleanString()), - }), - tags: ['Files'], - }, - ); + const { source: _, ...response } = res; + return response?.data ?? response; + }), + { + params: t.Object({ + id: t.Number(), + }), + query: t.Object({ + noVideo: t.Optional(t.BooleanString()), + }), + tags: ["Files"], + }, + ); - return app; + return app; }; diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts index 6c3000b..974ce4d 100644 --- a/server/src/controllers/index.ts +++ b/server/src/controllers/index.ts @@ -1,37 +1,38 @@ -import type { App } from '../app'; -import { StatsServicePlugin } from '../plugins/statsService'; -import { HttpStatusCode } from 'axios'; -import { BeatmapsManagerPlugin } from '../plugins/beatmapManager'; -import { observationaryConfigPublic } from '../config'; +import { HttpStatusCode } from "axios"; + +import type { App } from "../app"; +import { observationaryConfigPublic } from "../config"; +import { BeatmapsManagerPlugin } from "../plugins/beatmapManager"; +import { StatsServicePlugin } from "../plugins/statsService"; export default (app: App) => { - app.get('/', ({ redirect }) => { - return redirect('/docs'); - }); + app.get("/", ({ redirect }) => { + return redirect("/docs"); + }); - app.use(StatsServicePlugin) - .use(BeatmapsManagerPlugin) - .get( - '/stats', - async ({ StatsServiceInstance, BeatmapsManagerInstance }) => { - const serverStats = StatsServiceInstance.getServerStatistics(); - const managerStats = - await BeatmapsManagerInstance.getManagerStats(); - const serverConfig = observationaryConfigPublic; + app.use(StatsServicePlugin) + .use(BeatmapsManagerPlugin) + .get( + "/stats", + async ({ StatsServiceInstance, BeatmapsManagerInstance }) => { + const serverStats = StatsServiceInstance.getServerStatistics(); + const managerStats + = await BeatmapsManagerInstance.getManagerStats(); + const serverConfig = observationaryConfigPublic; - return { - status: HttpStatusCode.Ok, - data: { - config: serverConfig, - server: serverStats, - manager: managerStats, - }, - }; - }, - { - tags: ['Statistics'], - }, - ); + return { + status: HttpStatusCode.Ok, + data: { + config: serverConfig, + server: serverStats, + manager: managerStats, + }, + }; + }, + { + tags: ["Statistics"], + }, + ); - return app; + return app; }; diff --git a/server/src/controllers/osu/index.ts b/server/src/controllers/osu/index.ts index a199686..703fe52 100644 --- a/server/src/controllers/osu/index.ts +++ b/server/src/controllers/osu/index.ts @@ -1,28 +1,29 @@ -import { t } from 'elysia'; -import { App } from '../../app'; -import { BeatmapsManagerPlugin } from '../../plugins/beatmapManager'; +import { t } from "elysia"; + +import type { App } from "../../app"; +import { BeatmapsManagerPlugin } from "../../plugins/beatmapManager"; export default (app: App) => { - app.use(BeatmapsManagerPlugin).get( - '/:id', - ({ BeatmapsManagerInstance, params: { id }, set }) => - BeatmapsManagerInstance.downloadOsuBeatmap({ - beatmapId: id, - }).then((res) => { - if (res.source) { - set.headers['X-Data-Source'] = res.source; - } + app.use(BeatmapsManagerPlugin).get( + "/:id", + ({ BeatmapsManagerInstance, params: { id }, set }) => + BeatmapsManagerInstance.downloadOsuBeatmap({ + beatmapId: id, + }).then((res) => { + if (res.source) { + set.headers["X-Data-Source"] = res.source; + } - const { source: _, ...response } = res; - return response?.data ?? response; - }), - { - params: t.Object({ - id: t.Number(), - }), - tags: ['Files'], - }, - ); + const { source: _, ...response } = res; + return response?.data ?? response; + }), + { + params: t.Object({ + id: t.Number(), + }), + tags: ["Files"], + }, + ); - return app; + return app; }; diff --git a/server/src/core/abstracts/api/base-api.abstract.ts b/server/src/core/abstracts/api/base-api.abstract.ts index 939249d..cdc3bc7 100644 --- a/server/src/core/abstracts/api/base-api.abstract.ts +++ b/server/src/core/abstracts/api/base-api.abstract.ts @@ -1,234 +1,244 @@ -import config from '../../../config'; -import { createRequest } from '../../../database/models/requests'; -import { logExternalRequest } from '../../../utils/logger'; -import { AxiosResponseLog, BaseApiOptions } from './base-api.types'; -import { Axios, AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'; +import type { Axios, AxiosRequestConfig, AxiosResponse } from "axios"; +import { AxiosError } from "axios"; + +import config from "../../../config"; +import { createRequest } from "../../../database/models/requests"; +import { logExternalRequest } from "../../../utils/logger"; +import type { AxiosResponseLog, BaseApiOptions } from "./base-api.types"; export class BaseApi { - constructor( - private readonly axios: Axios, - private readonly config: AxiosRequestConfig, - ) { - axios.interceptors.request.use((config) => { - config.headers['request-startTime'] = new Date().getTime(); - return config; - }); - - axios.interceptors.response.use((response) => { - const currentTime = new Date().getTime(); - const startTime = response.config.headers['request-startTime']; - response.headers['request-duration'] = currentTime - startTime; - return response; - }); + constructor( + private readonly axios: Axios, + private readonly config: AxiosRequestConfig, + ) { + axios.interceptors.request.use((config) => { + config.headers["request-startTime"] = Date.now(); + return config; + }); + + axios.interceptors.response.use((response) => { + const currentTime = Date.now(); + const startTime = response.config.headers["request-startTime"]; + response.headers["request-duration"] = currentTime - startTime; + return response; + }); + } + + private throwIfAutomatedTesting() { + if (config.IsAutomatedTesting) { + throw new Error( + "Please mock the API request for automated testing", + ); } - - private throwIfAutomatedTesting() { - if (config.IsAutomatedTesting) { - throw new Error( - 'Please mock the API request for automated testing', - ); - } + } + + public async get< + Q, + B extends Record = Record, + >(endpoint: string, + options?: BaseApiOptions, + ) { + const formedUrl = this.createUrl(endpoint); + const formedUrlWithAttachedParams = this.attachParams( + formedUrl, + options?.body, + ); + const formedConfig = this.formConfig(options?.config); + + this.throwIfAutomatedTesting(); + + try { + const res = await this.axios.get( + formedUrlWithAttachedParams, + formedConfig, + ); + + this.handleResponse(res); + return res; } - - public async get< - Q, - B extends Record = Record, - >(endpoint: string, options?: BaseApiOptions) { - const formedUrl = this.createUrl(endpoint); - const formedUrlWithAttachedParams = this.attachParams( - formedUrl, - options?.body, - ); - const formedConfig = this.formConfig(options?.config); - - this.throwIfAutomatedTesting(); - - try { - const res = await this.axios.get( - formedUrlWithAttachedParams, - formedConfig, - ); - - this.handleResponse(res); - return res; - } catch (e: unknown) { - if (e instanceof AxiosError && e.response) { - this.handleResponse(e.response); - return e.response as AxiosResponse; - } - this.handleResponse(e); - return null; - } + catch (e: unknown) { + if (e instanceof AxiosError && e.response) { + this.handleResponse(e.response); + return e.response as AxiosResponse; + } + this.handleResponse(e); + return null; } - - public async post>( - endpoint: string, - options?: BaseApiOptions, - ) { - const formedUrl = this.createUrl(endpoint); - const formedConfig = this.formConfig(options?.config); - - this.throwIfAutomatedTesting(); - - try { - const res = await this.axios.post( - formedUrl, - options?.body, - formedConfig, - ); - - this.handleResponse(res); - return res; - } catch (e: unknown) { - if (e instanceof AxiosError && e.response) { - this.handleResponse(e.response); - return e.response as AxiosResponse; - } - this.handleResponse(e); - return null; - } + } + + public async post>( + endpoint: string, + options?: BaseApiOptions, + ) { + const formedUrl = this.createUrl(endpoint); + const formedConfig = this.formConfig(options?.config); + + this.throwIfAutomatedTesting(); + + try { + const res = await this.axios.post( + formedUrl, + options?.body, + formedConfig, + ); + + this.handleResponse(res); + return res; } - - public async put>( - endpoint: string, - options?: BaseApiOptions, - ) { - const formedUrl = this.createUrl(endpoint); - const formedConfig = this.formConfig(options?.config); - - this.throwIfAutomatedTesting(); - - try { - const res = await this.axios.put( - formedUrl, - options?.body, - formedConfig, - ); - - this.handleResponse(res); - return res; - } catch (e: unknown) { - if (e instanceof AxiosError && e.response) { - this.handleResponse(e.response); - return e.response as AxiosResponse; - } - this.handleResponse(e); - return null; - } + catch (e: unknown) { + if (e instanceof AxiosError && e.response) { + this.handleResponse(e.response); + return e.response as AxiosResponse; + } + this.handleResponse(e); + return null; } - - public async patch>( - endpoint: string, - options?: BaseApiOptions, - ) { - const formedUrl = this.createUrl(endpoint); - const formedConfig = this.formConfig(options?.config); - - this.throwIfAutomatedTesting(); - - try { - const res = await this.axios.patch( - formedUrl, - options?.body, - formedConfig, - ); - - this.handleResponse(res); - return res; - } catch (e: unknown) { - if (e instanceof AxiosError && e.response) { - this.handleResponse(e.response); - return e.response as AxiosResponse; - } - this.handleResponse(e); - return null; - } + } + + public async put>( + endpoint: string, + options?: BaseApiOptions, + ) { + const formedUrl = this.createUrl(endpoint); + const formedConfig = this.formConfig(options?.config); + + this.throwIfAutomatedTesting(); + + try { + const res = await this.axios.put( + formedUrl, + options?.body, + formedConfig, + ); + + this.handleResponse(res); + return res; } - - public async delete>( - endpoint: string, - options?: BaseApiOptions, - ) { - const formedUrl = this.createUrl(endpoint); - const formedConfig = this.formConfig(options?.config); - - this.throwIfAutomatedTesting(); - - try { - const res = await this.axios.delete(formedUrl, formedConfig); - - this.handleResponse(res); - return res; - } catch (e: unknown) { - if (e instanceof AxiosError && e.response) { - this.handleResponse(e.response); - return e.response as AxiosResponse; - } - this.handleResponse(e); - return null; - } + catch (e: unknown) { + if (e instanceof AxiosError && e.response) { + this.handleResponse(e.response); + return e.response as AxiosResponse; + } + this.handleResponse(e); + return null; } - - public get axiosConfig() { - return this.config; + } + + public async patch>( + endpoint: string, + options?: BaseApiOptions, + ) { + const formedUrl = this.createUrl(endpoint); + const formedConfig = this.formConfig(options?.config); + + this.throwIfAutomatedTesting(); + + try { + const res = await this.axios.patch( + formedUrl, + options?.body, + formedConfig, + ); + + this.handleResponse(res); + return res; } - - private handleResponse(res: any) { - if (!res) return; - const isAxiosError = res instanceof AxiosError; - - const data: AxiosResponseLog = { - status: isAxiosError ? 500 : res.status, - url: res.config.url, - baseUrl: this.config.baseURL ?? 'localhost', - method: res.config.method, - latency: isAxiosError - ? -1 - : (res.headers['request-duration'] ?? -1), - contentType: isAxiosError - ? 'application/json' - : res.headers['content-type']?.split(';')[0], - contentLength: isAxiosError - ? '-1' - : (res.headers.getContentLength() ?? '-1'), - data: isAxiosError ? res : res.data, - }; - - if (!isAxiosError && res.config.responseType === 'arraybuffer') { - const downloadFileLength = res?.data?.byteLength || 0; - - data.downloadSpeed = Math.round( - (downloadFileLength || 0) / 1024 / (data.latency / 1000), - ); // KB/s - } - - // Save request to database - createRequest({ - ...data, - data: data.status !== 200 ? data.data : undefined, - }); - - // Log request to console - logExternalRequest(data); + catch (e: unknown) { + if (e instanceof AxiosError && e.response) { + this.handleResponse(e.response); + return e.response as AxiosResponse; + } + this.handleResponse(e); + return null; } + } - private createUrl(endpoint: string): string { - return `${this.config.baseURL}/${endpoint}`; - } + public async delete>( + endpoint: string, + options?: BaseApiOptions, + ) { + const formedUrl = this.createUrl(endpoint); + const formedConfig = this.formConfig(options?.config); - private formConfig(config: AxiosRequestConfig = {}) { - return { ...this.config, ...(config ?? {}) }; - } + this.throwIfAutomatedTesting(); - private attachParams( - url: string, - params: Record | undefined, - ): string { - const formedSearchParams = new URLSearchParams(params ?? {}); - const formedQuery = formedSearchParams.toString() - ? `?${formedSearchParams.toString()}` - : ''; + try { + const res = await this.axios.delete(formedUrl, formedConfig); - return `${url}${formedQuery}`; + this.handleResponse(res); + return res; } + catch (e: unknown) { + if (e instanceof AxiosError && e.response) { + this.handleResponse(e.response); + return e.response as AxiosResponse; + } + this.handleResponse(e); + return null; + } + } + + public get axiosConfig() { + return this.config; + } + + private handleResponse(res: any) { + if (!res) + return; + const isAxiosError = res instanceof AxiosError; + + const data: AxiosResponseLog = { + status: isAxiosError ? 500 : res.status, + url: res.config.url, + baseUrl: this.config.baseURL ?? "localhost", + method: res.config.method, + latency: isAxiosError + ? -1 + : (res.headers["request-duration"] ?? -1), + contentType: isAxiosError + ? "application/json" + : res.headers["content-type"]?.split(";")[0], + contentLength: isAxiosError + ? "-1" + : (res.headers.getContentLength() ?? "-1"), + data: isAxiosError ? res : res.data, + }; + + if (!isAxiosError && res.config.responseType === "arraybuffer") { + const downloadFileLength = res?.data?.byteLength || 0; + + data.downloadSpeed = Math.round( + (downloadFileLength || 0) / 1024 / (data.latency / 1000), + ); // KB/s + } + + // Save request to database + createRequest({ + ...data, + data: data.status !== 200 ? data.data : undefined, + }); + + // Log request to console + logExternalRequest(data); + } + + private createUrl(endpoint: string): string { + return `${this.config.baseURL}/${endpoint}`; + } + + private formConfig(config: AxiosRequestConfig = {}) { + return { ...this.config, ...config }; + } + + private attachParams( + url: string, + params: Record | undefined, + ): string { + const formedSearchParams = new URLSearchParams(params ?? {}); + const formedQuery = formedSearchParams.toString() + ? `?${formedSearchParams.toString()}` + : ""; + + return `${url}${formedQuery}`; + } } diff --git a/server/src/core/abstracts/api/base-api.types.ts b/server/src/core/abstracts/api/base-api.types.ts index 4150ff3..7c481a7 100644 --- a/server/src/core/abstracts/api/base-api.types.ts +++ b/server/src/core/abstracts/api/base-api.types.ts @@ -1,18 +1,18 @@ -import { AxiosRequestConfig } from 'axios'; +import type { AxiosRequestConfig } from "axios"; export interface BaseApiOptions> { - body?: B; - config?: AxiosRequestConfig; + body?: B; + config?: AxiosRequestConfig; } export type AxiosResponseLog = { - status: number; - url: string; - baseUrl: string; - method: string; - latency: number; - contentType: string; - contentLength: string; - downloadSpeed?: number; - data?: any; + status: number; + url: string; + baseUrl: string; + method: string; + latency: number; + contentType: string; + contentLength: string; + downloadSpeed?: number; + data?: any; }; diff --git a/server/src/core/abstracts/client/base-client.abstract.ts b/server/src/core/abstracts/client/base-client.abstract.ts index 0e6fc07..2c9c5f2 100644 --- a/server/src/core/abstracts/client/base-client.abstract.ts +++ b/server/src/core/abstracts/client/base-client.abstract.ts @@ -1,144 +1,148 @@ -import axios from 'axios'; +/* eslint-disable unused-imports/no-unused-vars -- Abstract class with unimplemented methods */ +import axios from "axios"; + +import type { Beatmap, Beatmapset } from "../../../types/general/beatmap"; +import type { StorageManager } from "../../managers/storage/storage.manager"; +import { ConvertService } from "../../services/convert.service"; +import { BaseApi } from "../api/base-api.abstract"; +import { ApiRateLimiter } from "../ratelimiter/rate-limiter.abstract"; +import type { RateLimitOptions } from "../ratelimiter/rate-limiter.types"; +import type { + ClientOptions, + DownloadBeatmapSetOptions, + DownloadOsuBeatmap, + GetBeatmapOptions, + GetBeatmapSetOptions, + GetBeatmapsetsByBeatmapIdsOptions, + GetBeatmapsOptions, + ResultWithStatus, + SearchBeatmapsetsOptions, +} from "./base-client.types"; import { - ClientAbilities, - ClientOptions, - DownloadBeatmapSetOptions, - DownloadOsuBeatmap, - GetBeatmapOptions, - GetBeatmapSetOptions, - GetBeatmapsetsByBeatmapIdsOptions, - GetBeatmapsOptions, - ResultWithStatus, - SearchBeatmapsetsOptions, -} from './base-client.types'; -import { BaseApi } from '../api/base-api.abstract'; -import { RateLimit, RateLimitOptions } from '../ratelimiter/rate-limiter.types'; -import { ApiRateLimiter } from '../ratelimiter/rate-limiter.abstract'; -import { Beatmap, Beatmapset } from '../../../types/general/beatmap'; -import { ConvertService } from '../../services/convert.service'; -import { StorageManager } from '../../managers/storage/storage.manager'; + ClientAbilities, +} from "./base-client.types"; export class BaseClient { - protected storageManager?: StorageManager; - - protected config: ClientOptions; - protected api: ApiRateLimiter; - protected baseApi: BaseApi; - - protected convertService: ConvertService; - - constructor( - config: ClientOptions, - rateLimitConfig: RateLimitOptions, - storageManager?: StorageManager, - ) { - this.config = config; - - this.storageManager = storageManager; - - this.baseApi = new BaseApi(axios.create(), { - baseURL: this.config.baseUrl, - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - }); - - this.convertService = new ConvertService(this.config.baseUrl); - - const domainHash = Bun.hash(this.config.baseUrl).toString(); - this.api = new ApiRateLimiter( - domainHash, - this.baseApi, - rateLimitConfig, - ); - } - - async getBeatmapSet( - ctx: GetBeatmapSetOptions, - ): Promise> { - throw new Error('Method not implemented.'); - } - - async getBeatmaps( - ctx: GetBeatmapsOptions, - ): Promise> { - throw new Error('Method not implemented.'); - } - - async searchBeatmapsets( - ctx: SearchBeatmapsetsOptions, - ): Promise> { - throw new Error('Method not implemented.'); - } - - async getBeatmap( - ctx: GetBeatmapOptions, - ): Promise> { - throw new Error('Method not implemented.'); - } - - async getBeatmapsetsByBeatmapIds( - ctx: GetBeatmapsetsByBeatmapIdsOptions, - ): Promise> { - throw new Error('Method not implemented.'); - } - - async downloadBeatmapSet( - ctx: DownloadBeatmapSetOptions, - ): Promise> { - throw new Error('Method not implemented.'); - } - - async downloadOsuBeatmap( - ctx: DownloadOsuBeatmap, - ): Promise> { - throw new Error('Method not implemented.'); - } - - getCapacity(ability: ClientAbilities): { - limit: number; - remaining: number; - } { - const limit = - this.api.limiterConfig.rateLimits.find((rateLimit) => - rateLimit.abilities?.includes(ability), - ) || - this.api.limiterConfig.rateLimits.find((rateLimit) => - rateLimit.routes.includes('/'), - ); - - if (!limit) { - throw new Error( + protected storageManager?: StorageManager; + + protected config: ClientOptions; + protected api: ApiRateLimiter; + protected baseApi: BaseApi; + + protected convertService: ConvertService; + + constructor( + config: ClientOptions, + rateLimitConfig: RateLimitOptions, + storageManager?: StorageManager, + ) { + this.config = config; + + this.storageManager = storageManager; + + this.baseApi = new BaseApi(axios.create(), { + baseURL: this.config.baseUrl, + headers: { + "Accept": "application/json", + "Content-Type": "application/json", + }, + }); + + this.convertService = new ConvertService(this.config.baseUrl); + + const domainHash = Bun.hash(this.config.baseUrl).toString(); + this.api = new ApiRateLimiter( + domainHash, + this.baseApi, + rateLimitConfig, + ); + } + + async getBeatmapSet( + ctx: GetBeatmapSetOptions, + ): Promise> { + throw new Error("Method not implemented."); + } + + async getBeatmaps( + ctx: GetBeatmapsOptions, + ): Promise> { + throw new Error("Method not implemented."); + } + + async searchBeatmapsets( + ctx: SearchBeatmapsetsOptions, + ): Promise> { + throw new Error("Method not implemented."); + } + + async getBeatmap( + ctx: GetBeatmapOptions, + ): Promise> { + throw new Error("Method not implemented."); + } + + async getBeatmapsetsByBeatmapIds( + ctx: GetBeatmapsetsByBeatmapIdsOptions, + ): Promise> { + throw new Error("Method not implemented."); + } + + async downloadBeatmapSet( + ctx: DownloadBeatmapSetOptions, + ): Promise> { + throw new Error("Method not implemented."); + } + + async downloadOsuBeatmap( + ctx: DownloadOsuBeatmap, + ): Promise> { + throw new Error("Method not implemented."); + } + + getCapacity(ability: ClientAbilities): { + limit: number; + remaining: number; + } { + const limit + = this.api.limiterConfig.rateLimits.find(rateLimit => + rateLimit.abilities?.includes(ability), + ) + || this.api.limiterConfig.rateLimits.find(rateLimit => + rateLimit.routes.includes("/"), + ); + + if (!limit) { + throw new Error( `No rate limit found for ${this.config.baseUrl} / ${ability}`, - ); - } - - return this.api.getCapacity(limit); + ); } - getCapacities(): { - ability: string; - limit: number; - remaining: number; - }[] { - const rateLimits = this.api.limiterConfig.rateLimits; - const capacities = rateLimits.flatMap((rateLimit) => - rateLimit.abilities.map((ability) => ({ - ability: ClientAbilities[ability], - limit: this.getCapacity(ability).limit, - remaining: this.getCapacity(ability).remaining, - })), - ); - - return capacities; - } - - onCooldownUntil(): number | undefined { - return this.api.limiterConfig.onCooldownUntil; - } - - get clientConfig(): ClientOptions { - return this.config; - } + return this.api.getCapacity(limit); + } + + getCapacities(): Array<{ + ability: string; + limit: number; + remaining: number; + }> { + const { rateLimits } = this.api.limiterConfig; + const capacities = rateLimits.flatMap(rateLimit => + rateLimit.abilities.map(ability => ({ + ability: ClientAbilities[ability], + limit: this.getCapacity(ability).limit, + remaining: this.getCapacity(ability).remaining, + })), + ); + + return capacities; + } + + onCooldownUntil(): number | undefined { + return this.api.limiterConfig.onCooldownUntil; + } + + get clientConfig(): ClientOptions { + return this.config; + } } diff --git a/server/src/core/abstracts/client/base-client.types.ts b/server/src/core/abstracts/client/base-client.types.ts index 834d4ad..6bd7a97 100644 --- a/server/src/core/abstracts/client/base-client.types.ts +++ b/server/src/core/abstracts/client/base-client.types.ts @@ -1,72 +1,73 @@ -import { HttpStatusCode } from 'axios'; -import { BaseClient } from './base-client.abstract'; -import { RankStatusInt } from '../../../types/general/rankStatus'; -import { GameModeInt } from '../../../types/general/gameMode'; +import type { HttpStatusCode } from "axios"; + +import type { GameModeInt } from "../../../types/general/gameMode"; +import type { RankStatusInt } from "../../../types/general/rankStatus"; +import type { BaseClient } from "./base-client.abstract"; export type ClientOptions = { - baseUrl: string; - abilities: ClientAbilities[]; + baseUrl: string; + abilities: ClientAbilities[]; }; export type SearchBeatmapsetsOptions = { - query?: string; - limit?: number; - offset?: number; - status?: RankStatusInt[]; - mode?: GameModeInt; + query?: string; + limit?: number; + offset?: number; + status?: RankStatusInt[]; + mode?: GameModeInt; }; export type GetBeatmapSetOptions = { - beatmapSetId?: number; + beatmapSetId?: number; }; export type GetBeatmapsOptions = { - ids: number[]; + ids: number[]; }; export type GetBeatmapsetsByBeatmapIdsOptions = { - beatmapIds: number[]; + beatmapIds: number[]; }; export type DownloadBeatmapSetOptions = { - beatmapSetId: number; - noVideo?: boolean; + beatmapSetId: number; + noVideo?: boolean; }; export type DownloadOsuBeatmap = { - beatmapId: number; + beatmapId: number; }; export type GetBeatmapOptions = { - beatmapId?: number; - beatmapHash?: string; - allowMissingNonBeatmapValues?: boolean; + beatmapId?: number; + beatmapHash?: string; + allowMissingNonBeatmapValues?: boolean; }; export type ResultWithStatus = { - result: T | null; - status: HttpStatusCode; + result: T | null; + status: HttpStatusCode; }; export enum ClientAbilities { - GetBeatmapSetById = 1 << 0, // 1 - GetBeatmapById = 1 << 3, // 8 - GetBeatmapByHash = 1 << 5, // 32 - DownloadBeatmapSetById = 1 << 6, // 64 - DownloadBeatmapSetByIdNoVideo = 1 << 7, // 128 - SearchBeatmapsets = 1 << 8, // 256 - GetBeatmaps = 1 << 9, // 512 - DownloadOsuBeatmap = 1 << 10, // 1024 - GetBeatmapsetsByBeatmapIds = 1 << 11, // 2048 - GetBeatmapByIdWithSomeNonBeatmapValues = 1 << 12, // 4096 - GetBeatmapByHashWithSomeNonBeatmapValues = 1 << 13, // 8192 + GetBeatmapSetById = 1 << 0, // 1 + GetBeatmapById = 1 << 3, // 8 + GetBeatmapByHash = 1 << 5, // 32 + DownloadBeatmapSetById = 1 << 6, // 64 + DownloadBeatmapSetByIdNoVideo = 1 << 7, // 128 + SearchBeatmapsets = 1 << 8, // 256 + GetBeatmaps = 1 << 9, // 512 + DownloadOsuBeatmap = 1 << 10, // 1024 + GetBeatmapsetsByBeatmapIds = 1 << 11, // 2048 + GetBeatmapByIdWithSomeNonBeatmapValues = 1 << 12, // 4096 + GetBeatmapByHashWithSomeNonBeatmapValues = 1 << 13, // 8192 } export type MirrorClient = { - client: T; - weights: { - API: number; - download: number; - failrate: number; - }; + client: T; + weights: { + API: number; + download: number; + failrate: number; + }; }; diff --git a/server/src/core/abstracts/ratelimiter/rate-limiter.abstract.ts b/server/src/core/abstracts/ratelimiter/rate-limiter.abstract.ts index cb7a7f2..6120a35 100644 --- a/server/src/core/abstracts/ratelimiter/rate-limiter.abstract.ts +++ b/server/src/core/abstracts/ratelimiter/rate-limiter.abstract.ts @@ -1,420 +1,429 @@ -import { AxiosError, AxiosResponse } from 'axios'; -import logger from '../../../utils/logger'; -import { BaseApi } from '../api/base-api.abstract'; -import { BaseApiOptions } from '../api/base-api.types'; -import { RateLimit, RateLimitOptions } from './rate-limiter.types'; -import config from '../../../config'; -import Redis from 'ioredis'; -import { RedisInstance } from '../../../plugins/redisInstance'; -import { RedisKeys } from '../../../types/redis'; -import { ClientAbilities } from '../client/base-client.types'; +import type { AxiosResponse } from "axios"; +import { AxiosError } from "axios"; +import type Redis from "ioredis"; + +import config from "../../../config"; +import { RedisInstance } from "../../../plugins/redisInstance"; +import { RedisKeys } from "../../../types/redis"; +import logger from "../../../utils/logger"; +import type { BaseApi } from "../api/base-api.abstract"; +import type { BaseApiOptions } from "../api/base-api.types"; +import { ClientAbilities } from "../client/base-client.types"; +import type { RateLimit, RateLimitOptions } from "./rate-limiter.types"; const DEFAULT_RATE_LIMIT = { - abilities: Object.values(ClientAbilities).filter( - (item) => !isNaN(Number(item)), - ) as ClientAbilities[], - routes: ['/'], - limit: 60, - reset: 60, + abilities: Object.values(ClientAbilities).filter( + item => !Number.isNaN(Number(item)), + ) as ClientAbilities[], + routes: ["/"], + limit: 60, + reset: 60, }; export class ApiRateLimiter { - protected api: BaseApi; - protected _config: RateLimitOptions; - - public get config(): RateLimitOptions { - return { - ...this._config, - rateLimits: this._config.rateLimits.map((data) => ({ - ...data, - limit: !config.DisableSafeRatelimitMode - ? Math.floor(data.limit * 0.9) - : data.limit, - })), - dailyRateLimit: config.DisableDailyRateLimit - ? undefined - : this._config.dailyRateLimit && - !config.DisableSafeRatelimitMode - ? Math.floor(this._config.dailyRateLimit * 0.9) - : this._config.dailyRateLimit, - }; + protected api: BaseApi; + protected _config: RateLimitOptions; + + public get config(): RateLimitOptions { + return { + ...this._config, + rateLimits: this._config.rateLimits.map(data => ({ + ...data, + limit: !config.DisableSafeRatelimitMode + ? Math.floor(data.limit * 0.9) + : data.limit, + })), + dailyRateLimit: config.DisableDailyRateLimit + ? undefined + : this._config.dailyRateLimit + && !config.DisableSafeRatelimitMode + ? Math.floor(this._config.dailyRateLimit * 0.9) + : this._config.dailyRateLimit, + }; + } + + private readonly redis: Redis = RedisInstance; + + private readonly requests = new Map>(); + + private readonly redisDailyLimitsKey: string; + + private dailyLimit?: { + requestsLeft: number; + expiresAt: number; + } | null; + + constructor(domainHash: string, api: BaseApi, config: RateLimitOptions) { + this.api = api; + this._config = config; + this.redisDailyLimitsKey = `${RedisKeys.DAILY_RATE_LIMIT}${domainHash}`; + + if (this.config.dailyRateLimit) { + this.dailyLimit = null; } - private readonly redis: Redis = RedisInstance; - - private readonly requests = new Map>(); - - private readonly redisDailyLimitsKey: string; - - private dailyLimit?: { - requestsLeft: number; - expiresAt: number; - } | null; - - constructor(domainHash: string, api: BaseApi, config: RateLimitOptions) { - this.api = api; - this._config = config; - this.redisDailyLimitsKey = `${RedisKeys.DAILY_RATE_LIMIT}${domainHash}`; - - if (this.config.dailyRateLimit) { - this.dailyLimit = null; - } - - if ( - !this.config.rateLimits.find((limit) => limit.routes.includes('/')) - ) { - this._config.rateLimits.push(DEFAULT_RATE_LIMIT); - } - - this.config.rateLimits.forEach((limit) => { - this.requests.set(limit.routes, new Map()); - }); + if ( + !this.config.rateLimits.some(limit => limit.routes.includes("/")) + ) { + this._config.rateLimits.push(DEFAULT_RATE_LIMIT); } - public async get< - Q, - B extends Record = Record, - >(endpoint: string, options?: BaseApiOptions) { - const isOnCooldown = await this.isOnCooldown(endpoint); - if (isOnCooldown) return null; - - const requestUid = this.addNewRequest(endpoint); + this.config.rateLimits.forEach((limit) => { + this.requests.set(limit.routes, new Map()); + }); + } + + public async get< + Q, + B extends Record = Record, + >(endpoint: string, + options?: BaseApiOptions, + ) { + const isOnCooldown = await this.isOnCooldown(endpoint); + if (isOnCooldown) + return null; + + const requestUid = this.addNewRequest(endpoint); + + return await this.api.get(endpoint, options).then((res) => { + this.checkRateLimit(endpoint, res); + this.addNewRequest(endpoint, requestUid); + + return res; + }); + } + + public async post>( + endpoint: string, + options?: BaseApiOptions, + ) { + const isOnCooldown = await this.isOnCooldown(endpoint); + if (isOnCooldown) + return null; + + const requestUid = this.addNewRequest(endpoint); + + return await this.api.post(endpoint, options).then((res) => { + this.checkRateLimit(endpoint, res); + this.addNewRequest(endpoint, requestUid); + + return res; + }); + } + + public async put>( + endpoint: string, + options?: BaseApiOptions, + ) { + const isOnCooldown = await this.isOnCooldown(endpoint); + if (isOnCooldown) + return null; + + const requestUid = this.addNewRequest(endpoint); + + return await this.api.put(endpoint, options).then((res) => { + this.checkRateLimit(endpoint, res); + this.addNewRequest(endpoint, requestUid); + + return res; + }); + } + + public async patch>( + endpoint: string, + options?: BaseApiOptions, + ) { + const isOnCooldown = await this.isOnCooldown(endpoint); + if (isOnCooldown) + return null; + + const requestUid = this.addNewRequest(endpoint); + + return await this.api.patch(endpoint, options).then((res) => { + this.checkRateLimit(endpoint, res); + this.addNewRequest(endpoint, requestUid); + + return res; + }); + } + + public async delete>( + endpoint: string, + options?: BaseApiOptions, + ) { + const isOnCooldown = await this.isOnCooldown(endpoint); + if (isOnCooldown) + return null; + + const requestUid = this.addNewRequest(endpoint); + + return await this.api.delete(endpoint, options).then((res) => { + this.checkRateLimit(endpoint, res); + this.addNewRequest(endpoint, requestUid); + + return res; + }); + } + + public getCapacity(limit: RateLimit) { + return { + limit: limit.limit, + remaining: this.getRemainingRequests(limit), + }; + } + + /** + * @deprecated Use {@link config} instead + */ + public get limiterConfig() { + return this.config; + } - return await this.api.get(endpoint, options).then((res) => { - this.checkRateLimit(endpoint, res); - this.addNewRequest(endpoint, requestUid); + private async isOnCooldown(route: string) { + const limit = this.getRateLimit(route); + const dailyLimit = await this.getDailyRateLimitRemaining(); - return res; - }); + if (dailyLimit && dailyLimit.requestsLeft <= 0) { + this.log( + `Tried to make request to ${route} while on daily cooldown. Ignored`, + "warn", + ); + return true; } - public async post>( - endpoint: string, - options?: BaseApiOptions, + if ( + this.config.onCooldownUntil + && this.config.onCooldownUntil > Date.now() ) { - const isOnCooldown = await this.isOnCooldown(endpoint); - if (isOnCooldown) return null; - - const requestUid = this.addNewRequest(endpoint); - - return await this.api.post(endpoint, options).then((res) => { - this.checkRateLimit(endpoint, res); - this.addNewRequest(endpoint, requestUid); - - return res; - }); + this.log( + `Tried to make request to ${route} while on cooldown. Ignored`, + "warn", + ); + return true; } - public async put>( - endpoint: string, - options?: BaseApiOptions, - ) { - const isOnCooldown = await this.isOnCooldown(endpoint); - if (isOnCooldown) return null; - - const requestUid = this.addNewRequest(endpoint); - - return await this.api.put(endpoint, options).then((res) => { - this.checkRateLimit(endpoint, res); - this.addNewRequest(endpoint, requestUid); - - return res; - }); + if (this.getRemainingRequests(limit) <= 0) { + this.log( + `Tried to make request to ${route} while exceeding rate limit. Ignored`, + "warn", + ); + return true; } - public async patch>( - endpoint: string, - options?: BaseApiOptions, - ) { - const isOnCooldown = await this.isOnCooldown(endpoint); - if (isOnCooldown) return null; + return false; + } - const requestUid = this.addNewRequest(endpoint); + private async checkRateLimit( + route: string, + response: AxiosResponse | AxiosError | null, + ) { + const limit = this.getRateLimit(route); - return await this.api.patch(endpoint, options).then((res) => { - this.checkRateLimit(endpoint, res); - this.addNewRequest(endpoint, requestUid); + const isAxiosError = response instanceof AxiosError; - return res; - }); - } + const headerRemaining = isAxiosError + ? this.getRemainingRequests(limit) + : (response?.headers[ + this.config.headers?.remaining ?? "x-ratelimit-remaining" + ] ?? this.getRemainingRequests(limit)); - public async delete>( - endpoint: string, - options?: BaseApiOptions, - ) { - const isOnCooldown = await this.isOnCooldown(endpoint); - if (isOnCooldown) return null; + let remaining = this.getRemainingRequests(limit); - const requestUid = this.addNewRequest(endpoint); + if (headerRemaining < remaining) { + this.log( + "Header's remaining requests is lower than actual. Adding missing requests", + "warn", + ); - return await this.api.delete(endpoint, options).then((res) => { - this.checkRateLimit(endpoint, res); - this.addNewRequest(endpoint, requestUid); + for (let i = 0; i < remaining - headerRemaining; i++) { + this.addRequest(limit); + } - return res; - }); + remaining = this.getRemainingRequests(limit); } - public getCapacity(limit: RateLimit) { - return { - limit: limit.limit, - remaining: this.getRemainingRequests(limit), - }; - } + const logMessage + = `${this.api.axiosConfig.baseURL}/${route} | Routes: [${limit.routes.join(", ")}] | Remaining requests: ${remaining}/${limit.limit}${ + this.dailyLimit && this.config.dailyRateLimit + ? ` | Remaining daily requests: ${this.dailyLimit.requestsLeft}/${this.config.dailyRateLimit}, refresh at ${new Date(this.dailyLimit.expiresAt).toLocaleString()}` + : ""}`; - /** - * @deprecated Use {@link config} instead - */ - public get limiterConfig() { - return this.config; - } - - private async isOnCooldown(route: string) { - const limit = this.getRateLimit(route); - const dailyLimit = await this.getDailyRateLimitRemaining(); + this.log(logMessage); - if (dailyLimit) { - if (dailyLimit.requestsLeft <= 0) { - this.log( - `Tried to make request to ${route} while on daily cooldown. Ignored`, - 'warn', - ); - return true; - } - } - - if ( - this.config.onCooldownUntil && - this.config.onCooldownUntil > new Date().getTime() - ) { - this.log( - `Tried to make request to ${route} while on cooldown. Ignored`, - 'warn', - ); - return true; - } - - if (this.getRemainingRequests(limit) <= 0) { - this.log( - `Tried to make request to ${route} while exceeding rate limit. Ignored`, - 'warn', - ); - return true; - } - - return false; + if (remaining <= 0) { + this.log(`Rate limit reached for ${route}`, "warn"); } - private async checkRateLimit( - route: string, - response: AxiosResponse | AxiosError | null, - ) { - const limit = this.getRateLimit(route); - - const isAxiosError = response instanceof AxiosError; - - const headerRemaining = isAxiosError - ? this.getRemainingRequests(limit) - : (response?.headers[ - this.config.headers?.remaining ?? 'x-ratelimit-remaining' - ] ?? this.getRemainingRequests(limit)); - - let remaining = this.getRemainingRequests(limit); - - if (headerRemaining < remaining) { - this.log( - "Header's remaining requests is lower than actual. Adding missing requests", - 'warn', - ); - - for (let i = 0; i < remaining - headerRemaining; i++) { - this.addRequest(limit); - } - - remaining = this.getRemainingRequests(limit); - } - - const logMessage = - `${this.api.axiosConfig.baseURL}/${route} | Routes: [${limit.routes.join(', ')}] | Remaining requests: ${remaining}/${limit.limit}` + - (this.dailyLimit && this.config.dailyRateLimit - ? ` | Remaining daily requests: ${this.dailyLimit.requestsLeft}/${this.config.dailyRateLimit}, refresh at ${new Date(this.dailyLimit.expiresAt).toLocaleString()}` - : ''); - - this.log(logMessage); - - if (remaining <= 0) { - this.log(`Rate limit reached for ${route}`, 'warn'); - } - - if (isAxiosError) { - this.log( + if (isAxiosError) { + this.log( `Got axios error while making request to ${route}. Setting cooldown of 5 minutes`, - 'warn', - ); - this._config.onCooldownUntil = Date.now() + 5 * 60 * 1000; // 5 minutes - } + "warn", + ); + this._config.onCooldownUntil = Date.now() + 5 * 60 * 1000; // 5 minutes + } - if (response?.status === 429) { - this.log( + if (response?.status === 429) { + this.log( `Rate limit exceeded for ${route}. Setting cooldown`, - 'warn', - ); - this._config.onCooldownUntil = Date.now() + limit.reset * 1000; - } + "warn", + ); + this._config.onCooldownUntil = Date.now() + limit.reset * 1000; + } - if (response?.status === 403) { - this.log( + if (response?.status === 403) { + this.log( `Got forbidden status for ${route}. Setting cooldown of 1 hour`, - 'warn', - ); - this._config.onCooldownUntil = Date.now() + 60 * 60 * 1000; // 1 hour - } + "warn", + ); + this._config.onCooldownUntil = Date.now() + 60 * 60 * 1000; // 1 hour + } - if (response?.status && response.status >= 502) { - this.log( + if (response?.status && response.status >= 502) { + this.log( `Server error (${response.status}) for ${route}. Setting cooldown of 5 minutes`, - 'warn', - ); - this._config.onCooldownUntil = Date.now() + 5 * 60 * 1000; // 5 minutes - } + "warn", + ); + this._config.onCooldownUntil = Date.now() + 5 * 60 * 1000; // 5 minutes } + } - private getRateLimit(route: string) { - const limit = - this.config.rateLimits.find((limit) => - limit.routes.some((r) => route.startsWith(r)), - ) || - this.config.rateLimits.find((limit) => limit.routes.includes('/')); + private getRateLimit(route: string) { + const limit + = this.config.rateLimits.find(limit => + limit.routes.some(r => route.startsWith(r)), + ) + || this.config.rateLimits.find(limit => limit.routes.includes("/")); - if (!limit) { - throw new Error( + if (!limit) { + throw new Error( `ApiRateLimiter: Rate limit not found for ${route}`, - ); - } + ); + } - return limit; + return limit; + } + + private async getDailyRateLimitRemaining(): Promise<{ + requestsLeft: number; + expiresAt: number; + } | null> { + const isDailyLimitExists = this.config.dailyRateLimit; + if (!isDailyLimitExists) + return null; + + if (this.dailyLimit && this.dailyLimit.expiresAt > Date.now()) + return this.dailyLimit; + + const result = await this.redis + .multi() + .hget(this.redisDailyLimitsKey, "value") + .ttl(this.redisDailyLimitsKey) + .exec(); + + if (!result || result.some(([_, v]) => v === null)) { + await this.updateDailyRateLimitRemaining(0, true); + return await this.getDailyRateLimitRemaining(); } - private async getDailyRateLimitRemaining(): Promise<{ - requestsLeft: number; - expiresAt: number; - } | null> { - const isDailyLimitExists = this.config.dailyRateLimit; - if (!isDailyLimitExists) return null; + const [[, rawValue], [, ttlSeconds]] = result; - if (this.dailyLimit && this.dailyLimit.expiresAt > Date.now()) - return this.dailyLimit; + const value = Number.isNaN(Number(rawValue)) ? 0 : Number(rawValue); + const expiresAt = Number.isNaN(Number(ttlSeconds)) + ? 0 + : Number(ttlSeconds) * 1000; - const result = await this.redis - .multi() - .hget(this.redisDailyLimitsKey, 'value') - .ttl(this.redisDailyLimitsKey) - .exec(); + this.dailyLimit = { + requestsLeft: value, + expiresAt: Date.now() + expiresAt, + }; - if (!result || result.some(([_, v]) => v === null)) { - await this.updateDailyRateLimitRemaining(0, true); - return await this.getDailyRateLimitRemaining(); - } + return this.dailyLimit; + } - const [[, rawValue], [, ttlSeconds]] = result; + private async updateDailyRateLimitRemaining( + limitSpent: number, + resetTTL = false, + ) { + const dailyLimit = this.config.dailyRateLimit; + if (!dailyLimit) + return null; - const value = isNaN(Number(rawValue)) ? 0 : Number(rawValue); - const expiresAt = isNaN(Number(ttlSeconds)) - ? 0 - : Number(ttlSeconds) * 1000; + let currentDailyLimit = dailyLimit; - this.dailyLimit = { - requestsLeft: value, - expiresAt: Date.now() + expiresAt, - }; + if (this.dailyLimit && this.dailyLimit.expiresAt > Date.now()) { + currentDailyLimit = this.dailyLimit.requestsLeft - limitSpent; - return this.dailyLimit; + this.dailyLimit = { + ...this.dailyLimit, + requestsLeft: currentDailyLimit, + }; } - private async updateDailyRateLimitRemaining( - limitSpent: number, - resetTTL: boolean = false, - ) { - const dailyLimit = this.config.dailyRateLimit; - if (!dailyLimit) return null; - - let currentDailyLimit = dailyLimit; - - if (this.dailyLimit && this.dailyLimit.expiresAt > Date.now()) { - currentDailyLimit = this.dailyLimit.requestsLeft - limitSpent; + await this.redis.hset( + this.redisDailyLimitsKey, + "value", + currentDailyLimit, + ); - this.dailyLimit = { - ...this.dailyLimit, - requestsLeft: currentDailyLimit, - }; - } + if (resetTTL) + await this.redis.expire(this.redisDailyLimitsKey, 86400); // 24 hours + } - await this.redis.hset( - this.redisDailyLimitsKey, - 'value', - currentDailyLimit, - ); + private getRemainingRequests(limit: RateLimit) { + const requests = this.getRequestsArray(limit.routes); - if (resetTTL) await this.redis.expire(this.redisDailyLimitsKey, 86400); // 24 hours + this.clearOutdatedRequests(requests, limit); - return; - } + return limit.limit - requests.size; + } - private getRemainingRequests(limit: RateLimit) { - const requests = this.getRequestsArray(limit.routes); + private clearOutdatedRequests( + requests: Map, + limit: RateLimit, + ) { + requests.forEach((date, uid) => { + if (Date.now() - date.getTime() > limit.reset * 1000) { + requests.delete(uid); + } + }); + } - this.clearOutdatedRequests(requests, limit); + private addNewRequest(route: string, replaceUid?: string) { + const limit = this.getRateLimit(route); - return limit.limit - requests.size; - } + return this.addRequest(limit, replaceUid); + } - private clearOutdatedRequests( - requests: Map, - limit: RateLimit, - ) { - requests.forEach((date, uid) => { - if (new Date().getTime() - date.getTime() > limit.reset * 1000) { - requests.delete(uid); - } - }); - } + private addRequest(limit: RateLimit, replaceUid?: string) { + const requests = this.getRequestsArray(limit.routes); - private addNewRequest(route: string, replaceUid?: string) { - const limit = this.getRateLimit(route); + if (replaceUid) + requests.delete(replaceUid); - return this.addRequest(limit, replaceUid); + if (this.config.dailyRateLimit && replaceUid == null) { + this.updateDailyRateLimitRemaining(1); } - private addRequest(limit: RateLimit, replaceUid?: string) { - const requests = this.getRequestsArray(limit.routes); + const uid = crypto.randomUUID(); + requests.set(uid, new Date()); - if (replaceUid) requests.delete(replaceUid); + return uid; + } - if (this.config.dailyRateLimit && replaceUid == null) { - this.updateDailyRateLimitRemaining(1); - } + private getRequestsArray(routes: string[]) { + const map = this.requests.get(routes); - const uid = crypto.randomUUID(); - requests.set(uid, new Date()); - - return uid; + if (map) { + return map; } - private getRequestsArray(routes: string[]) { - const map = this.requests.get(routes); - - if (!map) { - return ( - this.requests.set(routes, new Map()).get(routes) ?? - new Map() - ); - } else { - return map; - } - } + return ( + this.requests.set(routes, new Map()).get(routes) + ?? new Map() + ); + } - private log(message: string, level: 'info' | 'warn' | 'error' = 'info') { - logger[level](`ApiRateLimiter: ${message}`); - } + private log(message: string, level: "info" | "warn" | "error" = "info") { + logger[level](`ApiRateLimiter: ${message}`); + } } diff --git a/server/src/core/abstracts/ratelimiter/rate-limiter.types.ts b/server/src/core/abstracts/ratelimiter/rate-limiter.types.ts index 2f7f081..c19b16c 100644 --- a/server/src/core/abstracts/ratelimiter/rate-limiter.types.ts +++ b/server/src/core/abstracts/ratelimiter/rate-limiter.types.ts @@ -1,19 +1,19 @@ -import { ClientAbilities } from '../client/base-client.types'; +import type { ClientAbilities } from "../client/base-client.types"; export type RateLimitOptions = { - headers?: { - remaining: string; - limit?: string; - reset?: string; - }; - dailyRateLimit?: number; - rateLimits: RateLimit[]; - onCooldownUntil?: number; // Active only if we got 429 status code before + headers?: { + remaining: string; + limit?: string; + reset?: string; + }; + dailyRateLimit?: number; + rateLimits: RateLimit[]; + onCooldownUntil?: number; // Active only if we got 429 status code before }; export type RateLimit = { - abilities: ClientAbilities[]; - routes: string[]; // ! Make sure this matches the "defaultUrl + route + value" logic - limit: number; - reset: number; + abilities: ClientAbilities[]; + routes: string[]; // ! Make sure this matches the "defaultUrl + route + value" logic + limit: number; + reset: number; }; diff --git a/server/src/core/domains/beatmaps.download/osulabs-client.types.ts b/server/src/core/domains/beatmaps.download/osulabs-client.types.ts index b1ccfad..6eb50a4 100644 --- a/server/src/core/domains/beatmaps.download/osulabs-client.types.ts +++ b/server/src/core/domains/beatmaps.download/osulabs-client.types.ts @@ -1,23 +1,23 @@ -import { Beatmap, Beatmapset } from '../../../types/general/beatmap'; -import { UserCompact } from '../../../types/general/user'; +import type { Beatmap, Beatmapset } from "../../../types/general/beatmap"; +import type { UserCompact } from "../../../types/general/user"; export interface OsulabsBeatmapset extends Beatmapset { - beatmaps?: OsulabsBeatmap[]; - converts?: OsulabsBeatmap[]; + beatmaps?: OsulabsBeatmap[]; + converts?: OsulabsBeatmap[]; - ratings?: number[]; - next_update?: number; - last_checked?: number; - has_favourited?: boolean; - recent_favourites?: UserCompact[]; - rating?: number; - /** number */ - last_updated: string; + ratings?: number[]; + next_update?: number; + last_checked?: number; + has_favourited?: boolean; + recent_favourites?: UserCompact[]; + rating?: number; + /** number */ + last_updated: string; } export interface OsulabsBeatmap extends Beatmap { - set?: null | OsulabsBeatmapset; - last_checked?: number; - /** number */ - last_updated: string; + set?: null | OsulabsBeatmapset; + last_checked?: number; + /** number */ + last_updated: string; } diff --git a/server/src/core/domains/beatmaps.download/osulabs.client.ts b/server/src/core/domains/beatmaps.download/osulabs.client.ts index 9304b55..b96d011 100644 --- a/server/src/core/domains/beatmaps.download/osulabs.client.ts +++ b/server/src/core/domains/beatmaps.download/osulabs.client.ts @@ -1,197 +1,199 @@ -import { BaseClient } from '../../abstracts/client/base-client.abstract'; +import type { Beatmap, Beatmapset } from "../../../types/general/beatmap"; +import logger from "../../../utils/logger"; +import { BaseClient } from "../../abstracts/client/base-client.abstract"; +import type { + DownloadBeatmapSetOptions, + GetBeatmapOptions, + GetBeatmapSetOptions, + ResultWithStatus, + SearchBeatmapsetsOptions, +} from "../../abstracts/client/base-client.types"; import { - ClientAbilities, - DownloadBeatmapSetOptions, - DownloadOsuBeatmap, - GetBeatmapOptions, - GetBeatmapSetOptions, - ResultWithStatus, - SearchBeatmapsetsOptions, -} from '../../abstracts/client/base-client.types'; -import logger from '../../../utils/logger'; -import { Beatmap, Beatmapset } from '../../../types/general/beatmap'; -import { OsulabsBeatmap, OsulabsBeatmapset } from './osulabs-client.types'; -import { StorageManager } from '../../managers/storage/storage.manager'; + ClientAbilities, +} from "../../abstracts/client/base-client.types"; +import type { StorageManager } from "../../managers/storage/storage.manager"; +import type { OsulabsBeatmap, OsulabsBeatmapset } from "./osulabs-client.types"; export class OsulabsClient extends BaseClient { - constructor(storageManager: StorageManager) { - super( + constructor(storageManager: StorageManager) { + super( + { + baseUrl: "https://beatmaps.download", + abilities: [ + ClientAbilities.GetBeatmapById, + ClientAbilities.GetBeatmapSetById, + ClientAbilities.DownloadBeatmapSetByIdNoVideo, + ClientAbilities.DownloadBeatmapSetById, + ClientAbilities.SearchBeatmapsets, + ], + }, + { + dailyRateLimit: 10000, + headers: { + remaining: "x-ratelimit-remaining", + }, + rateLimits: [ + { + abilities: [ + ClientAbilities.DownloadBeatmapSetByIdNoVideo, + ClientAbilities.DownloadBeatmapSetById, + ], + routes: ["d/"], + limit: 120, + reset: 60, + }, + { + abilities: [ClientAbilities.SearchBeatmapsets], + routes: ["api/v2/search"], + limit: 500, + reset: 60, + }, + { + abilities: [ + ClientAbilities.GetBeatmapById, + ClientAbilities.GetBeatmapSetById, + ], + routes: ["api/v2/s/", "api/v2/b/"], + limit: 500, + reset: 60, + }, + ], + }, + storageManager, + ); + + logger.info("OsulabsClient initialized"); + } + + async downloadBeatmapSet( + ctx: DownloadBeatmapSetOptions, + ): Promise> { + const result = await this.api.get( + `d/${ctx.beatmapSetId}${ctx.noVideo ? "n" : ""}`, { - baseUrl: 'https://beatmaps.download', - abilities: [ - ClientAbilities.GetBeatmapById, - ClientAbilities.GetBeatmapSetById, - ClientAbilities.DownloadBeatmapSetByIdNoVideo, - ClientAbilities.DownloadBeatmapSetById, - ClientAbilities.SearchBeatmapsets, - ], + config: { + responseType: "arraybuffer", + }, }, - { - dailyRateLimit: 10000, - headers: { - remaining: 'x-ratelimit-remaining', - }, - rateLimits: [ - { - abilities: [ - ClientAbilities.DownloadBeatmapSetByIdNoVideo, - ClientAbilities.DownloadBeatmapSetById, - ], - routes: ['d/'], - limit: 120, - reset: 60, - }, - { - abilities: [ClientAbilities.SearchBeatmapsets], - routes: ['api/v2/search'], - limit: 500, - reset: 60, - }, - { - abilities: [ - ClientAbilities.GetBeatmapById, - ClientAbilities.GetBeatmapSetById, - ], - routes: ['api/v2/s/', 'api/v2/b/'], - limit: 500, - reset: 60, - }, - ], - }, - storageManager, - ); + ); - logger.info('OsulabsClient initialized'); + if (!result || result.status !== 200 || !result.data) { + return { result: null, status: result?.status ?? 500 }; } - async downloadBeatmapSet( - ctx: DownloadBeatmapSetOptions, - ): Promise> { - const result = await this.api.get( - `d/${ctx.beatmapSetId}${ctx.noVideo ? 'n' : ''}`, - { - config: { - responseType: 'arraybuffer', - }, - }, - ); - - if (!result || result.status !== 200 || !result.data) { - return { result: null, status: result?.status ?? 500 }; - } + return { result: result.data, status: result.status }; + } - return { result: result.data, status: result.status }; + async getBeatmapSet( + ctx: GetBeatmapSetOptions, + ): Promise> { + if (ctx.beatmapSetId) { + return await this.getBeatmapSetById(ctx.beatmapSetId); } - async getBeatmapSet( - ctx: GetBeatmapSetOptions, - ): Promise> { - if (ctx.beatmapSetId) { - return await this.getBeatmapSetById(ctx.beatmapSetId); - } - - throw new Error('Invalid arguments'); + throw new Error("Invalid arguments"); + } + + async searchBeatmapsets( + ctx: SearchBeatmapsetsOptions, + ): Promise> { + const result = await this.api.get(`api/v2/search`, { + config: { + params: { + query: ctx.query, + limit: ctx.limit, + offset: ctx.offset, + status: ctx.status, + mode: ctx.mode, + }, + }, + }); + + if (!result || result.status !== 200 || !result.data) { + return { result: null, status: result?.status ?? 500 }; } - async searchBeatmapsets( - ctx: SearchBeatmapsetsOptions, - ): Promise> { - const result = await this.api.get(`api/v2/search`, { - config: { - params: { - query: ctx.query, - limit: ctx.limit, - offset: ctx.offset, - status: ctx.status, - mode: ctx.mode, - }, - }, - }); - - if (!result || result.status !== 200 || !result.data) { - return { result: null, status: result?.status ?? 500 }; - } - - return { - result: result.data.map((b: OsulabsBeatmapset) => - this.convertService.convertBeatmapset(b), - ), - status: result.status, - }; + return { + result: result.data.map((b: OsulabsBeatmapset) => + this.convertService.convertBeatmapset(b), + ), + status: result.status, + }; + } + + async getBeatmap( + ctx: GetBeatmapOptions, + ): Promise> { + if (ctx.beatmapId) { + return await this.getBeatmapById(ctx.beatmapId); + } + else if (ctx.beatmapHash) { + return await this.getBeatmapByHash(ctx.beatmapHash); } - async getBeatmap( - ctx: GetBeatmapOptions, - ): Promise> { - if (ctx.beatmapId) { - return await this.getBeatmapById(ctx.beatmapId); - } else if (ctx.beatmapHash) { - return await this.getBeatmapByHash(ctx.beatmapHash); - } + throw new Error("Invalid arguments"); + } - throw new Error('Invalid arguments'); + private async getBeatmapSetById( + beatmapSetId: number, + ): Promise> { + const result = await this.api.get( + `api/v2/s/${beatmapSetId}`, + ); + + if (!result || result.status !== 200 || !result.data) { + return { result: null, status: result?.status ?? 500 }; } - private async getBeatmapSetById( - beatmapSetId: number, - ): Promise> { - const result = await this.api.get( - `api/v2/s/${beatmapSetId}`, - ); + return { + result: this.convertService.convertBeatmapset(result.data), + status: result.status, + }; + } + + private async getBeatmapById( + beatmapId: number, + ): Promise> { + const result = await this.api.get(`api/v2/b/${beatmapId}`); - if (!result || result.status !== 200 || !result.data) { - return { result: null, status: result?.status ?? 500 }; - } + if (!result || result.status !== 200 || !result.data) { + return { result: null, status: result?.status ?? 500 }; + } - return { - result: this.convertService.convertBeatmapset(result.data), - status: result.status, - }; + const beatmap = result.data as OsulabsBeatmap; + if (beatmap.set) { + const beatmapSet = this.convertService.convertBeatmapset(beatmap.set); + await this.storageManager?.insertBeatmapset(beatmapSet, { + beatmapSetId: beatmapSet.id, + }); } - private async getBeatmapById( - beatmapId: number, - ): Promise> { - const result = await this.api.get(`api/v2/b/${beatmapId}`); - - if (!result || result.status !== 200 || !result.data) { - return { result: null, status: result?.status ?? 500 }; - } - - var beatmap = result.data as OsulabsBeatmap; - if (beatmap.set) { - var beatmapSet = this.convertService.convertBeatmapset(beatmap.set); - await this.storageManager?.insertBeatmapset(beatmapSet, { - beatmapSetId: beatmapSet.id, - }); - } - - return { - result: this.convertService.convertBeatmap(result.data), - status: result.status, - }; + return { + result: this.convertService.convertBeatmap(result.data), + status: result.status, + }; + } + + private async getBeatmapByHash( + beatmapHash: string, + ): Promise> { + const result = await this.api.get(`api/v2/md5/${beatmapHash}`); + + if (!result || result.status !== 200 || !result.data) { + return { result: null, status: result?.status ?? 500 }; } - private async getBeatmapByHash( - beatmapHash: string, - ): Promise> { - const result = await this.api.get(`api/v2/md5/${beatmapHash}`); - - if (!result || result.status !== 200 || !result.data) { - return { result: null, status: result?.status ?? 500 }; - } - - var beatmap = result.data as OsulabsBeatmap; - if (beatmap.set) { - var beatmapSet = this.convertService.convertBeatmapset(beatmap.set); - await this.storageManager?.insertBeatmapset(beatmapSet, { - beatmapSetId: beatmapSet.id, - }); - } - - return { - result: this.convertService.convertBeatmap(result.data), - status: result.status, - }; + const beatmap = result.data as OsulabsBeatmap; + if (beatmap.set) { + const beatmapSet = this.convertService.convertBeatmapset(beatmap.set); + await this.storageManager?.insertBeatmapset(beatmapSet, { + beatmapSetId: beatmapSet.id, + }); } + + return { + result: this.convertService.convertBeatmap(result.data), + status: result.status, + }; + } } diff --git a/server/src/core/domains/catboy.best/mino-client.types.ts b/server/src/core/domains/catboy.best/mino-client.types.ts index 96f37a9..68c25f8 100644 --- a/server/src/core/domains/catboy.best/mino-client.types.ts +++ b/server/src/core/domains/catboy.best/mino-client.types.ts @@ -1,23 +1,23 @@ -import { Beatmap, Beatmapset } from '../../../types/general/beatmap'; -import { UserCompact } from '../../../types/general/user'; +import type { Beatmap, Beatmapset } from "../../../types/general/beatmap"; +import type { UserCompact } from "../../../types/general/user"; export interface MinoBeatmapset extends Beatmapset { - beatmaps?: MinoBeatmap[]; - converts?: MinoBeatmap[]; + beatmaps?: MinoBeatmap[]; + converts?: MinoBeatmap[]; - ratings?: number[]; - next_update?: number; - last_checked?: number; - has_favourited?: boolean; - recent_favourites?: UserCompact[]; - rating?: number; - /** number */ - last_updated: string; + ratings?: number[]; + next_update?: number; + last_checked?: number; + has_favourited?: boolean; + recent_favourites?: UserCompact[]; + rating?: number; + /** number */ + last_updated: string; } export interface MinoBeatmap extends Beatmap { - set?: null | MinoBeatmapset; - last_checked?: number; - /** number */ - last_updated: string; + set?: null | MinoBeatmapset; + last_checked?: number; + /** number */ + last_updated: string; } diff --git a/server/src/core/domains/catboy.best/mino.client.ts b/server/src/core/domains/catboy.best/mino.client.ts index d0e72dd..f30230b 100644 --- a/server/src/core/domains/catboy.best/mino.client.ts +++ b/server/src/core/domains/catboy.best/mino.client.ts @@ -1,231 +1,235 @@ -import { BaseClient } from '../../abstracts/client/base-client.abstract'; +import qs from "qs"; + +import type { Beatmap, Beatmapset } from "../../../types/general/beatmap"; +import logger from "../../../utils/logger"; +import { BaseClient } from "../../abstracts/client/base-client.abstract"; +import type { + DownloadBeatmapSetOptions, + DownloadOsuBeatmap, + GetBeatmapOptions, + GetBeatmapSetOptions, + ResultWithStatus, + SearchBeatmapsetsOptions, +} from "../../abstracts/client/base-client.types"; import { - ClientAbilities, - DownloadBeatmapSetOptions, - DownloadOsuBeatmap, - GetBeatmapOptions, - GetBeatmapSetOptions, - ResultWithStatus, - SearchBeatmapsetsOptions, -} from '../../abstracts/client/base-client.types'; -import logger from '../../../utils/logger'; -import { Beatmap, Beatmapset } from '../../../types/general/beatmap'; -import { MinoBeatmap, MinoBeatmapset } from './mino-client.types'; -import qs from 'qs'; -import { StorageManager } from '../../managers/storage/storage.manager'; + ClientAbilities, +} from "../../abstracts/client/base-client.types"; +import type { StorageManager } from "../../managers/storage/storage.manager"; +import type { MinoBeatmap, MinoBeatmapset } from "./mino-client.types"; export class MinoClient extends BaseClient { - constructor(storageManager: StorageManager) { - super( + constructor(storageManager: StorageManager) { + super( + { + baseUrl: "https://catboy.best", + abilities: [ + ClientAbilities.GetBeatmapById, + ClientAbilities.GetBeatmapSetById, + ClientAbilities.GetBeatmapByHash, + ClientAbilities.DownloadBeatmapSetByIdNoVideo, + ClientAbilities.DownloadBeatmapSetById, + ClientAbilities.SearchBeatmapsets, + ClientAbilities.DownloadOsuBeatmap, + ], + }, + { + dailyRateLimit: 10000, + headers: { + remaining: "x-ratelimit-remaining", + }, + rateLimits: [ + { + abilities: [ + ClientAbilities.DownloadBeatmapSetByIdNoVideo, + ClientAbilities.DownloadBeatmapSetById, + ], + routes: ["d/"], + limit: 120, + reset: 60, + }, + { + abilities: [ClientAbilities.DownloadOsuBeatmap], + routes: ["osu/"], + limit: 120, + reset: 60, + }, + { + abilities: [ClientAbilities.SearchBeatmapsets], + routes: ["api/v2/search"], + limit: 500, + reset: 60, + }, + { + abilities: [ + ClientAbilities.GetBeatmapById, + ClientAbilities.GetBeatmapByHash, + ClientAbilities.GetBeatmapSetById, + ], + routes: [ + "api/v2/s/", + "api/v2/b/", + "api/v2/md5/", + "api/v2/beatmaps", + "api/v2/beatmapsets", + ], + limit: 500, + reset: 60, + }, + ], + }, + storageManager, + ); + + logger.info("MinoClient initialized"); + } + + async downloadBeatmapSet( + ctx: DownloadBeatmapSetOptions, + ): Promise> { + const result = await this.api.get( + `d/${ctx.beatmapSetId}${ctx.noVideo ? "n" : ""}`, { - baseUrl: 'https://catboy.best', - abilities: [ - ClientAbilities.GetBeatmapById, - ClientAbilities.GetBeatmapSetById, - ClientAbilities.GetBeatmapByHash, - ClientAbilities.DownloadBeatmapSetByIdNoVideo, - ClientAbilities.DownloadBeatmapSetById, - ClientAbilities.SearchBeatmapsets, - ClientAbilities.DownloadOsuBeatmap, - ], + config: { + responseType: "arraybuffer", + }, }, - { - dailyRateLimit: 10000, - headers: { - remaining: 'x-ratelimit-remaining', - }, - rateLimits: [ - { - abilities: [ - ClientAbilities.DownloadBeatmapSetByIdNoVideo, - ClientAbilities.DownloadBeatmapSetById, - ], - routes: ['d/'], - limit: 120, - reset: 60, - }, - { - abilities: [ClientAbilities.DownloadOsuBeatmap], - routes: ['osu/'], - limit: 120, - reset: 60, - }, - { - abilities: [ClientAbilities.SearchBeatmapsets], - routes: ['api/v2/search'], - limit: 500, - reset: 60, - }, - { - abilities: [ - ClientAbilities.GetBeatmapById, - ClientAbilities.GetBeatmapByHash, - ClientAbilities.GetBeatmapSetById, - ], - routes: [ - 'api/v2/s/', - 'api/v2/b/', - 'api/v2/md5/', - 'api/v2/beatmaps', - 'api/v2/beatmapsets', - ], - limit: 500, - reset: 60, - }, - ], - }, - storageManager, - ); + ); - logger.info('MinoClient initialized'); + if (!result || result.status !== 200 || !result.data) { + return { result: null, status: result?.status ?? 500 }; } - async downloadBeatmapSet( - ctx: DownloadBeatmapSetOptions, - ): Promise> { - const result = await this.api.get( - `d/${ctx.beatmapSetId}${ctx.noVideo ? 'n' : ''}`, - { - config: { - responseType: 'arraybuffer', - }, - }, - ); + return { result: result.data, status: result.status }; + } - if (!result || result.status !== 200 || !result.data) { - return { result: null, status: result?.status ?? 500 }; - } + async downloadOsuBeatmap( + ctx: DownloadOsuBeatmap, + ): Promise> { + const result = await this.api.get(`osu/${ctx.beatmapId}`, { + config: { + responseType: "arraybuffer", + }, + }); - return { result: result.data, status: result.status }; + if (!result || result.status !== 200 || !result.data) { + return { result: null, status: result?.status ?? 500 }; } - async downloadOsuBeatmap( - ctx: DownloadOsuBeatmap, - ): Promise> { - const result = await this.api.get(`osu/${ctx.beatmapId}`, { - config: { - responseType: 'arraybuffer', - }, - }); + return { result: result.data, status: result.status }; + } - if (!result || result.status !== 200 || !result.data) { - return { result: null, status: result?.status ?? 500 }; - } - - return { result: result.data, status: result.status }; + async getBeatmapSet( + ctx: GetBeatmapSetOptions, + ): Promise> { + if (ctx.beatmapSetId) { + return await this.getBeatmapSetById(ctx.beatmapSetId); } - async getBeatmapSet( - ctx: GetBeatmapSetOptions, - ): Promise> { - if (ctx.beatmapSetId) { - return await this.getBeatmapSetById(ctx.beatmapSetId); - } - - throw new Error('Invalid arguments'); + throw new Error("Invalid arguments"); + } + + async searchBeatmapsets( + ctx: SearchBeatmapsetsOptions, + ): Promise> { + const result = await this.api.get(`api/v2/search`, { + config: { + params: { + query: ctx.query, + limit: ctx.limit, + offset: ctx.offset, + status: ctx.status, + mode: ctx.mode, + }, + paramsSerializer: params => + qs.stringify(params, { indices: false }), + }, + }); + + if (!result || result.status !== 200 || !result.data) { + return { result: null, status: result?.status ?? 500 }; } - async searchBeatmapsets( - ctx: SearchBeatmapsetsOptions, - ): Promise> { - const result = await this.api.get(`api/v2/search`, { - config: { - params: { - query: ctx.query, - limit: ctx.limit, - offset: ctx.offset, - status: ctx.status, - mode: ctx.mode, - }, - paramsSerializer: (params) => - qs.stringify(params, { indices: false }), - }, - }); - - if (!result || result.status !== 200 || !result.data) { - return { result: null, status: result?.status ?? 500 }; - } - - return { - result: result.data.map((b: MinoBeatmapset) => - this.convertService.convertBeatmapset(b), - ), - status: result.status, - }; + return { + result: result.data.map((b: MinoBeatmapset) => + this.convertService.convertBeatmapset(b), + ), + status: result.status, + }; + } + + async getBeatmap( + ctx: GetBeatmapOptions, + ): Promise> { + if (ctx.beatmapId) { + return await this.getBeatmapById(ctx.beatmapId); + } + else if (ctx.beatmapHash) { + return await this.getBeatmapByHash(ctx.beatmapHash); } - async getBeatmap( - ctx: GetBeatmapOptions, - ): Promise> { - if (ctx.beatmapId) { - return await this.getBeatmapById(ctx.beatmapId); - } else if (ctx.beatmapHash) { - return await this.getBeatmapByHash(ctx.beatmapHash); - } + throw new Error("Invalid arguments"); + } - throw new Error('Invalid arguments'); + private async getBeatmapSetById( + beatmapSetId: number, + ): Promise> { + const result = await this.api.get( + `api/v2/s/${beatmapSetId}`, + ); + + if (!result || result.status !== 200 || !result.data) { + return { result: null, status: result?.status ?? 500 }; } - private async getBeatmapSetById( - beatmapSetId: number, - ): Promise> { - const result = await this.api.get( - `api/v2/s/${beatmapSetId}`, - ); + return { + result: this.convertService.convertBeatmapset(result.data), + status: result.status, + }; + } + + private async getBeatmapById( + beatmapId: number, + ): Promise> { + const result = await this.api.get(`api/v2/b/${beatmapId}`); - if (!result || result.status !== 200 || !result.data) { - return { result: null, status: result?.status ?? 500 }; - } + if (!result || result.status !== 200 || !result.data) { + return { result: null, status: result?.status ?? 500 }; + } - return { - result: this.convertService.convertBeatmapset(result.data), - status: result.status, - }; + const beatmap = result.data as MinoBeatmap; + if (beatmap.set) { + const beatmapSet = this.convertService.convertBeatmapset(beatmap.set); + await this.storageManager?.insertBeatmapset(beatmapSet, { + beatmapSetId: beatmapSet.id, + }); } - private async getBeatmapById( - beatmapId: number, - ): Promise> { - const result = await this.api.get(`api/v2/b/${beatmapId}`); - - if (!result || result.status !== 200 || !result.data) { - return { result: null, status: result?.status ?? 500 }; - } - - var beatmap = result.data as MinoBeatmap; - if (beatmap.set) { - var beatmapSet = this.convertService.convertBeatmapset(beatmap.set); - await this.storageManager?.insertBeatmapset(beatmapSet, { - beatmapSetId: beatmapSet.id, - }); - } - - return { - result: this.convertService.convertBeatmap(result.data), - status: result.status, - }; + return { + result: this.convertService.convertBeatmap(result.data), + status: result.status, + }; + } + + private async getBeatmapByHash( + beatmapHash: string, + ): Promise> { + const result = await this.api.get(`api/v2/md5/${beatmapHash}`); + + if (!result || result.status !== 200 || !result.data) { + return { result: null, status: result?.status ?? 500 }; } - private async getBeatmapByHash( - beatmapHash: string, - ): Promise> { - const result = await this.api.get(`api/v2/md5/${beatmapHash}`); - - if (!result || result.status !== 200 || !result.data) { - return { result: null, status: result?.status ?? 500 }; - } - - var beatmap = result.data as MinoBeatmap; - if (beatmap.set) { - var beatmapSet = this.convertService.convertBeatmapset(beatmap.set); - await this.storageManager?.insertBeatmapset(beatmapSet, { - beatmapSetId: beatmapSet.id, - }); - } - - return { - result: this.convertService.convertBeatmap(result.data), - status: result.status, - }; + const beatmap = result.data as MinoBeatmap; + if (beatmap.set) { + const beatmapSet = this.convertService.convertBeatmapset(beatmap.set); + await this.storageManager?.insertBeatmapset(beatmapSet, { + beatmapSetId: beatmapSet.id, + }); } + + return { + result: this.convertService.convertBeatmap(result.data), + status: result.status, + }; + } } diff --git a/server/src/core/domains/gatari.pw/gatari.client.ts b/server/src/core/domains/gatari.pw/gatari.client.ts index 4efd992..265b8a8 100644 --- a/server/src/core/domains/gatari.pw/gatari.client.ts +++ b/server/src/core/domains/gatari.pw/gatari.client.ts @@ -1,42 +1,44 @@ -import { BaseClient } from '../../abstracts/client/base-client.abstract'; +import logger from "../../../utils/logger"; +import { BaseClient } from "../../abstracts/client/base-client.abstract"; +import type { + DownloadBeatmapSetOptions, + ResultWithStatus, +} from "../../abstracts/client/base-client.types"; import { - ClientAbilities, - DownloadBeatmapSetOptions, - ResultWithStatus, -} from '../../abstracts/client/base-client.types'; -import logger from '../../../utils/logger'; + ClientAbilities, +} from "../../abstracts/client/base-client.types"; export class GatariClient extends BaseClient { - constructor() { - super( - { - baseUrl: 'https://osu.gatari.pw', - abilities: [ClientAbilities.DownloadBeatmapSetByIdNoVideo], - }, - { - rateLimits: [], - }, - ); + constructor() { + super( + { + baseUrl: "https://osu.gatari.pw", + abilities: [ClientAbilities.DownloadBeatmapSetByIdNoVideo], + }, + { + rateLimits: [], + }, + ); - logger.info('GatariClient initialized'); - } + logger.info("GatariClient initialized"); + } - async downloadBeatmapSet( - ctx: DownloadBeatmapSetOptions, - ): Promise> { - const result = await this.api.get( + async downloadBeatmapSet( + ctx: DownloadBeatmapSetOptions, + ): Promise> { + const result = await this.api.get( `d/${ctx.beatmapSetId}`, { - config: { - responseType: 'arraybuffer', - }, + config: { + responseType: "arraybuffer", + }, }, - ); - - if (!result || result.status !== 200 || !result.data) { - return { result: null, status: result?.status ?? 500 }; - } + ); - return { result: result.data, status: result.status }; + if (!result || result.status !== 200 || !result.data) { + return { result: null, status: result?.status ?? 500 }; } + + return { result: result.data, status: result.status }; + } } diff --git a/server/src/core/domains/index.ts b/server/src/core/domains/index.ts index cb0d951..814bd3c 100644 --- a/server/src/core/domains/index.ts +++ b/server/src/core/domains/index.ts @@ -1,4 +1,4 @@ -import { DirectClient } from './osu.direct/direct.client'; -import { BanchoClient } from './osu.ppy.sh/bancho.client'; +import { DirectClient } from "./osu.direct/direct.client"; +import { BanchoClient } from "./osu.ppy.sh/bancho.client"; -export { DirectClient, BanchoClient }; +export { BanchoClient, DirectClient }; diff --git a/server/src/core/domains/nerinyan.moe/nerinyan.client.ts b/server/src/core/domains/nerinyan.moe/nerinyan.client.ts index 22ad9de..5b6c859 100644 --- a/server/src/core/domains/nerinyan.moe/nerinyan.client.ts +++ b/server/src/core/domains/nerinyan.moe/nerinyan.client.ts @@ -1,48 +1,50 @@ -import { BaseClient } from '../../abstracts/client/base-client.abstract'; +import logger from "../../../utils/logger"; +import { BaseClient } from "../../abstracts/client/base-client.abstract"; +import type { + DownloadBeatmapSetOptions, + ResultWithStatus, +} from "../../abstracts/client/base-client.types"; import { - ClientAbilities, - DownloadBeatmapSetOptions, - ResultWithStatus, -} from '../../abstracts/client/base-client.types'; -import logger from '../../../utils/logger'; + ClientAbilities, +} from "../../abstracts/client/base-client.types"; export class NerinyanClient extends BaseClient { - constructor() { - super( - { - baseUrl: 'https://api.nerinyan.moe', - abilities: [ - ClientAbilities.DownloadBeatmapSetByIdNoVideo, - ClientAbilities.DownloadBeatmapSetById, - ], - }, - { - rateLimits: [], - }, - ); + constructor() { + super( + { + baseUrl: "https://api.nerinyan.moe", + abilities: [ + ClientAbilities.DownloadBeatmapSetByIdNoVideo, + ClientAbilities.DownloadBeatmapSetById, + ], + }, + { + rateLimits: [], + }, + ); - logger.info('NerinyanClient initialized'); - } + logger.info("NerinyanClient initialized"); + } - async downloadBeatmapSet( - ctx: DownloadBeatmapSetOptions, - ): Promise> { - const result = await this.api.get( + async downloadBeatmapSet( + ctx: DownloadBeatmapSetOptions, + ): Promise> { + const result = await this.api.get( `d/${ctx.beatmapSetId}`, { - config: { - responseType: 'arraybuffer', - params: { - noVideo: ctx.noVideo ? true : false, - }, + config: { + responseType: "arraybuffer", + params: { + noVideo: ctx.noVideo ? true : false, }, + }, }, - ); - - if (!result || result.status !== 200 || !result.data) { - return { result: null, status: result?.status ?? 500 }; - } + ); - return { result: result.data, status: result.status }; + if (!result || result.status !== 200 || !result.data) { + return { result: null, status: result?.status ?? 500 }; } + + return { result: result.data, status: result.status }; + } } diff --git a/server/src/core/domains/osu.direct/direct-client.types.ts b/server/src/core/domains/osu.direct/direct-client.types.ts index a9f463f..d49986e 100644 --- a/server/src/core/domains/osu.direct/direct-client.types.ts +++ b/server/src/core/domains/osu.direct/direct-client.types.ts @@ -1,4 +1,3 @@ -import { Beatmap, Beatmapset } from '../../../types/general/beatmap'; -import { UserCompact } from '../../../types/general/user'; +import type { Beatmap } from "../../../types/general/beatmap"; -export interface DirectBeatmap extends Omit {} +export interface DirectBeatmap extends Omit {} diff --git a/server/src/core/domains/osu.direct/direct.client.ts b/server/src/core/domains/osu.direct/direct.client.ts index 12272df..aa32cd3 100644 --- a/server/src/core/domains/osu.direct/direct.client.ts +++ b/server/src/core/domains/osu.direct/direct.client.ts @@ -1,130 +1,133 @@ -import { BaseClient } from '../../abstracts/client/base-client.abstract'; +import type { Beatmap } from "../../../types/general/beatmap"; +import logger from "../../../utils/logger"; +import { BaseClient } from "../../abstracts/client/base-client.abstract"; +import type { + DownloadBeatmapSetOptions, + DownloadOsuBeatmap, + GetBeatmapOptions, + ResultWithStatus, +} from "../../abstracts/client/base-client.types"; import { - ClientAbilities, - DownloadBeatmapSetOptions, - DownloadOsuBeatmap, - GetBeatmapOptions, - ResultWithStatus, -} from '../../abstracts/client/base-client.types'; -import logger from '../../../utils/logger'; -import { Beatmap } from '../../../types/general/beatmap'; + ClientAbilities, +} from "../../abstracts/client/base-client.types"; export class DirectClient extends BaseClient { - constructor() { - super( - { - baseUrl: 'https://osu.direct/api', - abilities: [ - ClientAbilities.DownloadBeatmapSetById, - ClientAbilities.DownloadBeatmapSetByIdNoVideo, - ClientAbilities.DownloadOsuBeatmap, - ClientAbilities.GetBeatmapByIdWithSomeNonBeatmapValues, - ClientAbilities.GetBeatmapByHashWithSomeNonBeatmapValues, - ], - }, - { - headers: { - remaining: 'ratelimit-remaining', - reset: 'ratelimit-reset', - limit: 'ratelimit-limit', - }, - rateLimits: [ - { - abilities: [ - ClientAbilities.DownloadBeatmapSetById, - ClientAbilities.DownloadBeatmapSetByIdNoVideo, - ClientAbilities.DownloadOsuBeatmap, - ClientAbilities.GetBeatmapByIdWithSomeNonBeatmapValues, - ClientAbilities.GetBeatmapByHashWithSomeNonBeatmapValues, - ], - routes: ['/'], - limit: 50, - reset: 60, - }, - ], - }, - ); - - logger.info('DirectClient initialized'); + constructor() { + super( + { + baseUrl: "https://osu.direct/api", + abilities: [ + ClientAbilities.DownloadBeatmapSetById, + ClientAbilities.DownloadBeatmapSetByIdNoVideo, + ClientAbilities.DownloadOsuBeatmap, + ClientAbilities.GetBeatmapByIdWithSomeNonBeatmapValues, + ClientAbilities.GetBeatmapByHashWithSomeNonBeatmapValues, + ], + }, + { + headers: { + remaining: "ratelimit-remaining", + reset: "ratelimit-reset", + limit: "ratelimit-limit", + }, + rateLimits: [ + { + abilities: [ + ClientAbilities.DownloadBeatmapSetById, + ClientAbilities.DownloadBeatmapSetByIdNoVideo, + ClientAbilities.DownloadOsuBeatmap, + ClientAbilities.GetBeatmapByIdWithSomeNonBeatmapValues, + ClientAbilities.GetBeatmapByHashWithSomeNonBeatmapValues, + ], + routes: ["/"], + limit: 50, + reset: 60, + }, + ], + }, + ); + + logger.info("DirectClient initialized"); + } + + async getBeatmap( + ctx: GetBeatmapOptions, + ): Promise> { + if (ctx.beatmapId) { + return await this.getBeatmapById(ctx.beatmapId); } - - async getBeatmap( - ctx: GetBeatmapOptions, - ): Promise> { - if (ctx.beatmapId) { - return await this.getBeatmapById(ctx.beatmapId); - } else if (ctx.beatmapHash) { - return await this.getBeatmapByHash(ctx.beatmapHash); - } - - throw new Error('Invalid arguments'); + else if (ctx.beatmapHash) { + return await this.getBeatmapByHash(ctx.beatmapHash); } - private async getBeatmapById( - beatmapId: number, - ): Promise> { - const result = await this.api.get(`v2/b/${beatmapId}`); + throw new Error("Invalid arguments"); + } - if (!result || result.status !== 200 || !result.data) { - return { result: null, status: result?.status ?? 500 }; - } + private async getBeatmapById( + beatmapId: number, + ): Promise> { + const result = await this.api.get(`v2/b/${beatmapId}`); - return { - result: this.convertService.convertBeatmap(result.data), - status: result.status, - }; + if (!result || result.status !== 200 || !result.data) { + return { result: null, status: result?.status ?? 500 }; } - private async getBeatmapByHash( - beatmapHash: string, - ): Promise> { - const result = await this.api.get(`v2/md5/${beatmapHash}`); + return { + result: this.convertService.convertBeatmap(result.data), + status: result.status, + }; + } - if (!result || result.status !== 200 || !result.data) { - return { result: null, status: result?.status ?? 500 }; - } + private async getBeatmapByHash( + beatmapHash: string, + ): Promise> { + const result = await this.api.get(`v2/md5/${beatmapHash}`); - return { - result: this.convertService.convertBeatmap(result.data), - status: result.status, - }; + if (!result || result.status !== 200 || !result.data) { + return { result: null, status: result?.status ?? 500 }; } - async downloadBeatmapSet( - ctx: DownloadBeatmapSetOptions, - ): Promise> { - const result = await this.api.get( + return { + result: this.convertService.convertBeatmap(result.data), + status: result.status, + }; + } + + async downloadBeatmapSet( + ctx: DownloadBeatmapSetOptions, + ): Promise> { + const result = await this.api.get( `d/${ctx.beatmapSetId}`, { - config: { - responseType: 'arraybuffer', - params: { - noVideo: ctx.noVideo ? true : undefined, - }, + config: { + responseType: "arraybuffer", + params: { + noVideo: ctx.noVideo ? true : undefined, }, + }, }, - ); + ); - if (!result || result.status !== 200 || !result.data) { - return { result: null, status: result?.status ?? 500 }; - } - - return { result: result.data, status: result.status }; + if (!result || result.status !== 200 || !result.data) { + return { result: null, status: result?.status ?? 500 }; } - async downloadOsuBeatmap( - ctx: DownloadOsuBeatmap, - ): Promise> { - const result = await this.api.get(`osu/${ctx.beatmapId}`, { - config: { - responseType: 'arraybuffer', - }, - }); + return { result: result.data, status: result.status }; + } - if (!result || result.status !== 200 || !result.data) { - return { result: null, status: result?.status ?? 500 }; - } + async downloadOsuBeatmap( + ctx: DownloadOsuBeatmap, + ): Promise> { + const result = await this.api.get(`osu/${ctx.beatmapId}`, { + config: { + responseType: "arraybuffer", + }, + }); - return { result: result.data, status: result.status }; + if (!result || result.status !== 200 || !result.data) { + return { result: null, status: result?.status ?? 500 }; } + + return { result: result.data, status: result.status }; + } } diff --git a/server/src/core/domains/osu.ppy.sh/bancho-client.service.ts b/server/src/core/domains/osu.ppy.sh/bancho-client.service.ts index c6517bb..8608573 100644 --- a/server/src/core/domains/osu.ppy.sh/bancho-client.service.ts +++ b/server/src/core/domains/osu.ppy.sh/bancho-client.service.ts @@ -1,58 +1,58 @@ -import { - OsuApiCredentials, - OsuApiCredentialsRequest, - OsuApiCredentialsResponse, -} from './bancho-client.types'; -import { BaseApi } from '../../abstracts/api/base-api.abstract'; -import config from '../../../config'; +import config from "../../../config"; +import type { BaseApi } from "../../abstracts/api/base-api.abstract"; +import type { + OsuApiCredentials, + OsuApiCredentialsRequest, + OsuApiCredentialsResponse, +} from "./bancho-client.types"; export class BanchoService { - private readonly api: BaseApi; - private clientCredentials: OsuApiCredentials | null = null; - - constructor(api: BaseApi) { - this.api = api; + private readonly api: BaseApi; + private clientCredentials: OsuApiCredentials | null = null; + + constructor(api: BaseApi) { + this.api = api; + } + + async getBanchoClientToken(): Promise { + if ( + !this.clientCredentials + || this.clientCredentials.expires_on <= Date.now() + ) { + this.clientCredentials = await this._fetchBanchoClientToken(); } - async getBanchoClientToken(): Promise { - if ( - !this.clientCredentials || - this.clientCredentials.expires_on <= new Date().getTime() - ) { - this.clientCredentials = await this._fetchBanchoClientToken(); - } + return this.clientCredentials.access_token; + } - return this.clientCredentials.access_token; + private async _fetchBanchoClientToken(): Promise { + if (!config.BANCHO_CLIENT_ID || !config.BANCHO_CLIENT_SECRET) { + throw new Error( + "Requesting Bancho client token without client ID/secret", + ); } - private async _fetchBanchoClientToken(): Promise { - if (!config.BANCHO_CLIENT_ID || !config.BANCHO_CLIENT_SECRET) { - throw new Error( - 'Requesting Bancho client token without client ID/secret', - ); - } - - const result = await this.api.post< + const result = await this.api.post< OsuApiCredentialsResponse, OsuApiCredentialsRequest - >('oauth/token', { - body: { - client_id: config.BANCHO_CLIENT_ID, - client_secret: config.BANCHO_CLIENT_SECRET, - grant_type: 'client_credentials', - scope: 'public', - }, - }); - - if (!result || result.status !== 200 || !result.data) { - throw new Error( - 'BanchoService: Failed to fetch Bancho client token. Please check your client ID/secret', - ); - } - - return { - ...result.data, - expires_on: Date.now() + result.data.expires_in * 1000, - }; + >("oauth/token", { + body: { + client_id: config.BANCHO_CLIENT_ID, + client_secret: config.BANCHO_CLIENT_SECRET, + grant_type: "client_credentials", + scope: "public", + }, + }); + + if (!result || result.status !== 200 || !result.data) { + throw new Error( + "BanchoService: Failed to fetch Bancho client token. Please check your client ID/secret", + ); } + + return { + ...result.data, + expires_on: Date.now() + result.data.expires_in * 1000, + }; + } } diff --git a/server/src/core/domains/osu.ppy.sh/bancho-client.types.ts b/server/src/core/domains/osu.ppy.sh/bancho-client.types.ts index 49d114f..138ee11 100644 --- a/server/src/core/domains/osu.ppy.sh/bancho-client.types.ts +++ b/server/src/core/domains/osu.ppy.sh/bancho-client.types.ts @@ -1,44 +1,44 @@ -import { Beatmap, Beatmapset } from '../../../types/general/beatmap'; -import { UserCompact } from '../../../types/general/user'; +import type { Beatmap, Beatmapset } from "../../../types/general/beatmap"; +import type { UserCompact } from "../../../types/general/user"; export type OsuApiCredentialsRequest = { - client_id: string; - client_secret: string; - grant_type: string; - scope: string; + client_id: string; + client_secret: string; + grant_type: string; + scope: string; }; export type OsuApiCredentialsResponse = { - access_token: string; - expires_in: number; - token_type: 'Bearer'; + access_token: string; + expires_in: number; + token_type: "Bearer"; }; export type OsuApiCredentials = { - access_token: string; - expires_in: number; - token_type: 'Bearer'; - expires_on: number; + access_token: string; + expires_in: number; + token_type: "Bearer"; + expires_on: number; }; export interface BanchoBeatmapset extends Beatmapset { - current_user_attributes?: unknown; - discussions?: unknown; - events?: unknown; - recent_favourites?: UserCompact[]; - beatmaps?: BanchoBeatmap[]; - converts?: BanchoBeatmap[]; + current_user_attributes?: unknown; + discussions?: unknown; + events?: unknown; + recent_favourites?: UserCompact[]; + beatmaps?: BanchoBeatmap[]; + converts?: BanchoBeatmap[]; } export interface BanchoBeatmap extends Beatmap { - /** `null` if the beatmap doesn't have associated beatmapset (e.g. deleted). */ - beatmapset?: null | BanchoBeatmapset; - current_user_attributes?: unknown; - discussions?: unknown; - events?: unknown; - recent_favourites?: UserCompact[]; + /** `null` if the beatmap doesn't have associated beatmapset (e.g. deleted). */ + beatmapset?: null | BanchoBeatmapset; + current_user_attributes?: unknown; + discussions?: unknown; + events?: unknown; + recent_favourites?: UserCompact[]; } export interface BanchoBeatmapsetSearchResult { - beatmapsets: BanchoBeatmapset[]; + beatmapsets: BanchoBeatmapset[]; } diff --git a/server/src/core/domains/osu.ppy.sh/bancho.client.ts b/server/src/core/domains/osu.ppy.sh/bancho.client.ts index e97719f..35b4917 100644 --- a/server/src/core/domains/osu.ppy.sh/bancho.client.ts +++ b/server/src/core/domains/osu.ppy.sh/bancho.client.ts @@ -1,202 +1,205 @@ -import { Beatmap, Beatmapset } from '../../../types/general/beatmap'; -import logger from '../../../utils/logger'; -import { BaseClient } from '../../abstracts/client/base-client.abstract'; +import type { Beatmap, Beatmapset } from "../../../types/general/beatmap"; +import logger from "../../../utils/logger"; +import { BaseClient } from "../../abstracts/client/base-client.abstract"; +import type { + DownloadOsuBeatmap, + GetBeatmapOptions, + GetBeatmapSetOptions, + GetBeatmapsetsByBeatmapIdsOptions, + GetBeatmapsOptions, + ResultWithStatus, +} from "../../abstracts/client/base-client.types"; import { - ClientAbilities, - DownloadOsuBeatmap, - GetBeatmapOptions, - GetBeatmapSetOptions, - GetBeatmapsetsByBeatmapIdsOptions, - GetBeatmapsOptions, - ResultWithStatus, -} from '../../abstracts/client/base-client.types'; -import { BanchoService } from './bancho-client.service'; -import { BanchoBeatmap, BanchoBeatmapset } from './bancho-client.types'; + ClientAbilities, +} from "../../abstracts/client/base-client.types"; +import { BanchoService } from "./bancho-client.service"; +import type { BanchoBeatmap, BanchoBeatmapset } from "./bancho-client.types"; export class BanchoClient extends BaseClient { - private readonly banchoService = new BanchoService(this.baseApi); - - constructor() { - super( - { - baseUrl: 'https://osu.ppy.sh', - abilities: [ - ClientAbilities.GetBeatmapById, - ClientAbilities.GetBeatmapSetById, - ClientAbilities.GetBeatmaps, - ClientAbilities.DownloadOsuBeatmap, - ClientAbilities.GetBeatmapsetsByBeatmapIds, - ], - }, - { - rateLimits: [ - { - abilities: [ - ClientAbilities.GetBeatmapById, - ClientAbilities.GetBeatmapSetById, - ClientAbilities.GetBeatmaps, - ClientAbilities.DownloadOsuBeatmap, - ClientAbilities.GetBeatmapsetsByBeatmapIds, - ], - routes: ['/'], - limit: 1200, - reset: 60, - }, - ], - }, - ); - - logger.info('BanchoClient: initialized'); + private readonly banchoService = new BanchoService(this.baseApi); + + constructor() { + super( + { + baseUrl: "https://osu.ppy.sh", + abilities: [ + ClientAbilities.GetBeatmapById, + ClientAbilities.GetBeatmapSetById, + ClientAbilities.GetBeatmaps, + ClientAbilities.DownloadOsuBeatmap, + ClientAbilities.GetBeatmapsetsByBeatmapIds, + ], + }, + { + rateLimits: [ + { + abilities: [ + ClientAbilities.GetBeatmapById, + ClientAbilities.GetBeatmapSetById, + ClientAbilities.GetBeatmaps, + ClientAbilities.DownloadOsuBeatmap, + ClientAbilities.GetBeatmapsetsByBeatmapIds, + ], + routes: ["/"], + limit: 1200, + reset: 60, + }, + ], + }, + ); + + logger.info("BanchoClient: initialized"); + } + + async getBeatmapSet( + ctx: GetBeatmapSetOptions, + ): Promise> { + if (ctx.beatmapSetId) { + return await this.getBeatmapSetById(ctx.beatmapSetId); } - async getBeatmapSet( - ctx: GetBeatmapSetOptions, - ): Promise> { - if (ctx.beatmapSetId) { - return await this.getBeatmapSetById(ctx.beatmapSetId); - } + throw new Error("Invalid arguments"); + } - throw new Error('Invalid arguments'); + async getBeatmap( + ctx: GetBeatmapOptions, + ): Promise> { + if (ctx.beatmapId) { + return await this.getBeatmapById(ctx.beatmapId); } - async getBeatmap( - ctx: GetBeatmapOptions, - ): Promise> { - if (ctx.beatmapId) { - return await this.getBeatmapById(ctx.beatmapId); - } + throw new Error("Invalid arguments"); + } - throw new Error('Invalid arguments'); - } - - async getBeatmaps( - ctx: GetBeatmapsOptions, - ): Promise> { - const { ids } = ctx; + async getBeatmaps( + ctx: GetBeatmapsOptions, + ): Promise> { + const { ids } = ctx; - const result = await this.api.get<{ beatmaps: BanchoBeatmap[] }>( - `api/v2/beatmaps?${ids.map((id) => `ids[]=${id}`).join('&')}`, + const result = await this.api.get<{ beatmaps: BanchoBeatmap[] }>( + `api/v2/beatmaps?${ids.map(id => `ids[]=${id}`).join("&")}`, { - config: { - headers: { - Authorization: `Bearer ${await this.osuApiKey}`, - }, + config: { + headers: { + Authorization: `Bearer ${await this.osuApiKey}`, }, + }, }, - ); - - if (!result || result.status !== 200) { - return { result: null, status: result?.status ?? 500 }; - } - - return { - result: result.data?.beatmaps?.map((b: BanchoBeatmap) => - this.convertService.convertBeatmap(b), - ), - status: result.status, - }; - } + ); - async getBeatmapsetsByBeatmapIds( - ctx: GetBeatmapsetsByBeatmapIdsOptions, - ): Promise> { - const { beatmapIds } = ctx; + if (!result || result.status !== 200) { + return { result: null, status: result?.status ?? 500 }; + } - const result = await this.api.get<{ beatmaps: BanchoBeatmap[] }>( - `api/v2/beatmaps?${beatmapIds.map((id) => `ids[]=${id}`).join('&')}`, + return { + result: result.data?.beatmaps?.map((b: BanchoBeatmap) => + this.convertService.convertBeatmap(b), + ), + status: result.status, + }; + } + + async getBeatmapsetsByBeatmapIds( + ctx: GetBeatmapsetsByBeatmapIdsOptions, + ): Promise> { + const { beatmapIds } = ctx; + + const result = await this.api.get<{ beatmaps: BanchoBeatmap[] }>( + `api/v2/beatmaps?${beatmapIds.map(id => `ids[]=${id}`).join("&")}`, { - config: { - headers: { - Authorization: `Bearer ${await this.osuApiKey}`, - }, + config: { + headers: { + Authorization: `Bearer ${await this.osuApiKey}`, }, + }, }, - ); - - if (!result || result.status !== 200) { - return { result: null, status: result?.status ?? 500 }; - } - - return { - result: result.data?.beatmaps?.map((b: BanchoBeatmap) => - this.convertService.convertBeatmapset({ - ...b.beatmapset, - ...(b.convert - ? { converts: [b.convert] } - : { beatmaps: [b] }), - } as BanchoBeatmapset), - ), - status: result.status, - }; - } + ); - async downloadOsuBeatmap( - ctx: DownloadOsuBeatmap, - ): Promise> { - const result = await this.api.get(`osu/${ctx.beatmapId}`, { - config: { - responseType: 'arraybuffer', - }, - }); - - if (!result || result.status !== 200 || !result.data) { - return { result: null, status: result?.status ?? 500 }; - } else if (result.data.byteLength === 0) { - return { result: null, status: 404 }; - } + if (!result || result.status !== 200) { + return { result: null, status: result?.status ?? 500 }; + } - return { result: result.data, status: result.status }; + return { + result: result.data?.beatmaps?.map((b: BanchoBeatmap) => + this.convertService.convertBeatmapset({ + ...b.beatmapset, + ...(b.convert + ? { converts: [b.convert] } + : { beatmaps: [b] }), + } as BanchoBeatmapset), + ), + status: result.status, + }; + } + + async downloadOsuBeatmap( + ctx: DownloadOsuBeatmap, + ): Promise> { + const result = await this.api.get(`osu/${ctx.beatmapId}`, { + config: { + responseType: "arraybuffer", + }, + }); + + if (!result || result.status !== 200 || !result.data) { + return { result: null, status: result?.status ?? 500 }; } + else if (result.data.byteLength === 0) { + return { result: null, status: 404 }; + } + + return { result: result.data, status: result.status }; + } - private async getBeatmapSetById( - beatmapSetId: number, - ): Promise> { - const result = await this.api.get( + private async getBeatmapSetById( + beatmapSetId: number, + ): Promise> { + const result = await this.api.get( `api/v2/beatmapsets/${beatmapSetId}`, { - config: { - headers: { - Authorization: `Bearer ${await this.osuApiKey}`, - }, + config: { + headers: { + Authorization: `Bearer ${await this.osuApiKey}`, }, + }, }, - ); + ); - if (!result || result.status !== 200 || !result.data) { - return { result: null, status: result?.status ?? 500 }; - } - - return { - result: this.convertService.convertBeatmapset(result.data), - status: result.status, - }; + if (!result || result.status !== 200 || !result.data) { + return { result: null, status: result?.status ?? 500 }; } - private async getBeatmapById( - beatmapId: number, - ): Promise> { - const result = await this.api.get( + return { + result: this.convertService.convertBeatmapset(result.data), + status: result.status, + }; + } + + private async getBeatmapById( + beatmapId: number, + ): Promise> { + const result = await this.api.get( `api/v2/beatmaps/${beatmapId}`, { - config: { - headers: { - Authorization: `Bearer ${await this.osuApiKey}`, - }, + config: { + headers: { + Authorization: `Bearer ${await this.osuApiKey}`, }, + }, }, - ); - - if (!result || result.status !== 200 || !result.data) { - return { result: null, status: result?.status ?? 500 }; - } + ); - return { - result: this.convertService.convertBeatmap(result.data), - status: result.status, - }; + if (!result || result.status !== 200 || !result.data) { + return { result: null, status: result?.status ?? 500 }; } - private get osuApiKey() { - return this.banchoService.getBanchoClientToken(); - } + return { + result: this.convertService.convertBeatmap(result.data), + status: result.status, + }; + } + + private get osuApiKey() { + return this.banchoService.getBanchoClientToken(); + } } diff --git a/server/src/core/managers/beatmaps/beatmaps-manager.types.ts b/server/src/core/managers/beatmaps/beatmaps-manager.types.ts index e2c14d5..af7e6c2 100644 --- a/server/src/core/managers/beatmaps/beatmaps-manager.types.ts +++ b/server/src/core/managers/beatmaps/beatmaps-manager.types.ts @@ -1,8 +1,8 @@ -import { HttpStatusCode } from 'axios'; +import type { HttpStatusCode } from "axios"; export type ServerResponse = { - data: T | null; - status: HttpStatusCode; - message?: string; - source: 'storage' | 'mirror' | null; + data: T | null; + status: HttpStatusCode; + message?: string; + source: "storage" | "mirror" | null; }; diff --git a/server/src/core/managers/beatmaps/beatmaps.manager.ts b/server/src/core/managers/beatmaps/beatmaps.manager.ts index 2af9e3d..1ead94b 100644 --- a/server/src/core/managers/beatmaps/beatmaps.manager.ts +++ b/server/src/core/managers/beatmaps/beatmaps.manager.ts @@ -1,328 +1,330 @@ -import { HttpStatusCode } from 'axios'; - -import { - GetBeatmapOptions, - GetBeatmapSetOptions, - DownloadBeatmapSetOptions, - GetBeatmapsOptions, - DownloadOsuBeatmap, - ResultWithStatus, - SearchBeatmapsetsOptions, - GetBeatmapsetsByBeatmapIdsOptions, -} from '../../abstracts/client/base-client.types'; -import { MirrorsManager } from '../mirrors/mirrors.manager'; -import { ServerResponse } from './beatmaps-manager.types'; -import { Beatmap, Beatmapset } from '../../../types/general/beatmap'; -import { StorageManager } from '../storage/storage.manager'; +import { HttpStatusCode } from "axios"; + +import type { Beatmap, Beatmapset } from "../../../types/general/beatmap"; +import type { + DownloadBeatmapSetOptions, + DownloadOsuBeatmap, + GetBeatmapOptions, + GetBeatmapSetOptions, + GetBeatmapsetsByBeatmapIdsOptions, + GetBeatmapsOptions, + ResultWithStatus, + SearchBeatmapsetsOptions, +} from "../../abstracts/client/base-client.types"; +import { MirrorsManager } from "../mirrors/mirrors.manager"; +import { StorageManager } from "../storage/storage.manager"; +import type { ServerResponse } from "./beatmaps-manager.types"; const INTERNAL_ERROR_RESPONSE = { - data: null, - status: HttpStatusCode.InternalServerError, - message: - 'An unexpected error occurred. Please check the status code for more details and try again later. >.<', - source: null, + data: null, + status: HttpStatusCode.InternalServerError, + message: + "An unexpected error occurred. Please check the status code for more details and try again later. >.<", + source: null, }; export class BeatmapsManager { - private readonly MirrorsManager: MirrorsManager; - private readonly StorageManager: StorageManager; + private readonly MirrorsManager: MirrorsManager; + private readonly StorageManager: StorageManager; + + constructor() { + this.StorageManager = new StorageManager(); + this.MirrorsManager = new MirrorsManager(this.StorageManager); + } + + async getBeatmap(ctx: GetBeatmapOptions): Promise> { + const beatmap = await this.StorageManager.getBeatmap(ctx); + if (beatmap || beatmap === null) { + return { + data: beatmap, + status: beatmap ? HttpStatusCode.Ok : HttpStatusCode.NotFound, + message: !beatmap ? "Beatmap not found" : undefined, + source: "storage", + }; + } + + const result = await this.MirrorsManager.getBeatmap(ctx); - constructor() { - this.StorageManager = new StorageManager(); - this.MirrorsManager = new MirrorsManager(this.StorageManager); + if (result.status >= 500) { + return this.formatResultAsServerError(result); } - async getBeatmap(ctx: GetBeatmapOptions): Promise> { - const beatmap = await this.StorageManager.getBeatmap(ctx); - if (beatmap || beatmap === null) { - return { - data: beatmap, - status: beatmap ? HttpStatusCode.Ok : HttpStatusCode.NotFound, - message: !beatmap ? 'Beatmap not found' : undefined, - source: 'storage', - }; - } - - const result = await this.MirrorsManager.getBeatmap(ctx); - - if (result.status >= 500) { - return this.formatResultAsServerError(result); - } - - this.StorageManager.insertBeatmap(result.result, ctx); - - return { - data: result.result, - status: result.status, - message: result.status === 404 ? 'Beatmap not found' : undefined, - source: 'mirror', - }; + this.StorageManager.insertBeatmap(result.result, ctx); + + return { + data: result.result, + status: result.status, + message: result.status === 404 ? "Beatmap not found" : undefined, + source: "mirror", + }; + } + + async getBeatmapSet( + ctx: GetBeatmapSetOptions, + ): Promise> { + const beatmapset = await this.StorageManager.getBeatmapSet(ctx); + if (beatmapset || beatmapset === null) { + return { + data: beatmapset, + status: beatmapset + ? HttpStatusCode.Ok + : HttpStatusCode.NotFound, + message: !beatmapset ? "Beatmapset not found" : undefined, + source: "storage", + }; } - async getBeatmapSet( - ctx: GetBeatmapSetOptions, - ): Promise> { - const beatmapset = await this.StorageManager.getBeatmapSet(ctx); - if (beatmapset || beatmapset === null) { - return { - data: beatmapset, - status: beatmapset - ? HttpStatusCode.Ok - : HttpStatusCode.NotFound, - message: !beatmapset ? 'Beatmapset not found' : undefined, - source: 'storage', - }; - } - - const result = await this.MirrorsManager.getBeatmapSet(ctx); - - if (result.status >= 500) { - return this.formatResultAsServerError(result); - } - - this.StorageManager.insertBeatmapset(result.result, ctx); - - return { - data: result.result, - status: result.status, - message: result.status === 404 ? 'Beatmapset not found' : undefined, - source: 'mirror', - }; + const result = await this.MirrorsManager.getBeatmapSet(ctx); + + if (result.status >= 500) { + return this.formatResultAsServerError(result); } - async searchBeatmapsets( - ctx: SearchBeatmapsetsOptions, - ): Promise> { - const searchResult = await this.StorageManager.getSearchResult(ctx); - - if (searchResult) { - return { - data: searchResult, - status: HttpStatusCode.Ok, - message: undefined, - source: 'storage', - }; - } - - const result = await this.MirrorsManager.searchBeatmapsets(ctx); - - if (result.status >= 500) { - return this.formatResultAsServerError(result); - } - - if (result.result && result.status === 200) { - this.StorageManager.insertSearchResult(ctx, result.result); - } - - return { - data: result.result, - status: result.status, - message: - result.status === 404 ? 'Beatmapsets not found' : undefined, - source: 'mirror', - }; + this.StorageManager.insertBeatmapset(result.result, ctx); + + return { + data: result.result, + status: result.status, + message: result.status === 404 ? "Beatmapset not found" : undefined, + source: "mirror", + }; + } + + async searchBeatmapsets( + ctx: SearchBeatmapsetsOptions, + ): Promise> { + const searchResult = await this.StorageManager.getSearchResult(ctx); + + if (searchResult) { + return { + data: searchResult, + status: HttpStatusCode.Ok, + message: undefined, + source: "storage", + }; } - async getBeatmaps( - ctx: GetBeatmapsOptions, - ): Promise> { - const result = await this.MirrorsManager.getBeatmaps(ctx); - - if (result.status >= 500) { - return this.formatResultAsServerError(result); - } - - if (result.result && result.result.length > 0) { - for (const beatmap of result.result) { - this.StorageManager.insertBeatmap(beatmap, { - beatmapId: beatmap.id, - }); - } - } - - return { - data: result.result, - status: result.status, - message: result.status === 404 ? 'Beatmaps not found' : undefined, - source: 'mirror', - }; + const result = await this.MirrorsManager.searchBeatmapsets(ctx); + + if (result.status >= 500) { + return this.formatResultAsServerError(result); } - async getBeatmapsetsByBeatmapIds( - ctx: GetBeatmapsetsByBeatmapIdsOptions, - ): Promise> { - const beatmapsets = - await this.StorageManager.getBeatmapSetsByBeatmapIds(ctx); - - const missingIds = - ctx.beatmapIds.filter( - (id) => - beatmapsets !== undefined && - !beatmapsets - ?.flatMap( - (set) => set.beatmaps?.map((map) => map.id) ?? [], - ) - .includes(id), - ) ?? ctx.beatmapIds; - - if (beatmapsets && missingIds.length === 0) { - return { - data: beatmapsets, - status: HttpStatusCode.Ok, - message: undefined, - source: 'storage', - }; - } - - let result = await this.MirrorsManager.getBeatmapsetsByBeatmapIds({ - beatmapIds: missingIds, + if (result.result && result.status === 200) { + this.StorageManager.insertSearchResult(ctx, result.result); + } + + return { + data: result.result, + status: result.status, + message: + result.status === 404 ? "Beatmapsets not found" : undefined, + source: "mirror", + }; + } + + async getBeatmaps( + ctx: GetBeatmapsOptions, + ): Promise> { + const result = await this.MirrorsManager.getBeatmaps(ctx); + + if (result.status >= 500) { + return this.formatResultAsServerError(result); + } + + if (result.result && result.result.length > 0) { + for (const beatmap of result.result) { + this.StorageManager.insertBeatmap(beatmap, { + beatmapId: beatmap.id, }); + } + } + + return { + data: result.result, + status: result.status, + message: result.status === 404 ? "Beatmaps not found" : undefined, + source: "mirror", + }; + } + + async getBeatmapsetsByBeatmapIds( + ctx: GetBeatmapsetsByBeatmapIdsOptions, + ): Promise> { + const beatmapsets + = await this.StorageManager.getBeatmapSetsByBeatmapIds(ctx); + + const missingIds + = ctx.beatmapIds.filter( + id => + beatmapsets !== undefined + && !beatmapsets + ?.flatMap( + set => set.beatmaps?.map(map => map.id) ?? [], + ) + .includes(id), + ) ?? ctx.beatmapIds; + + if (beatmapsets && missingIds.length === 0) { + return { + data: beatmapsets, + status: HttpStatusCode.Ok, + message: undefined, + source: "storage", + }; + } + + const result = await this.MirrorsManager.getBeatmapsetsByBeatmapIds({ + beatmapIds: missingIds, + }); - if (result.status >= 500) { - return this.formatResultAsServerError(result); - } - - // ! NOTE: Results from getBeatmapsetsByBeatmapIds will include not all beatmaps, so we need to fetch beatmapsets again to fetch all beatmaps with them - result.result = await Promise.all( - result.result?.map( - async (beatmapset) => - await this.getBeatmapSet({ - beatmapSetId: beatmapset.id, - }), - ) ?? [], - ).then( - (results) => - results - .map((result) => result.data ?? null) - .filter((result) => result !== null) as Beatmapset[], - ); - - if (result.result && result.result.length > 0) { - for (const beatmapset of result.result) { - this.StorageManager.insertBeatmapset(beatmapset, { - beatmapSetId: beatmapset.id, - }); - } - } - - const missingIdsFromResult = - result.result?.filter( - (set) => - !beatmapsets - ?.flatMap((set) => set.beatmaps?.map((map) => map.id)) - .includes(set.id), - ) ?? []; - - return { - data: [...(beatmapsets ?? []), ...(result.result ?? [])], - status: result.status, - message: + if (result.status >= 500) { + return this.formatResultAsServerError(result); + } + + // ! NOTE: Results from getBeatmapsetsByBeatmapIds will include not all beatmaps, so we need to fetch beatmapsets again to fetch all beatmaps with them + result.result = await Promise.all( + result.result?.map( + async beatmapset => + await this.getBeatmapSet({ + beatmapSetId: beatmapset.id, + }), + ) ?? [], + ).then( + results => + results + .map(result => result.data ?? null) + .filter(result => result !== null) as Beatmapset[], + ); + + if (result.result && result.result.length > 0) { + for (const beatmapset of result.result) { + this.StorageManager.insertBeatmapset(beatmapset, { + beatmapSetId: beatmapset.id, + }); + } + } + + const missingIdsFromResult + = result.result?.filter( + set => + !beatmapsets + ?.flatMap(set => set.beatmaps?.map(map => map.id)) + .includes(set.id), + ) ?? []; + + return { + data: [...(beatmapsets ?? []), ...(result.result ?? [])], + status: result.status, + message: missingIdsFromResult.length > 0 - ? `Some beatmapsets not found: ${missingIdsFromResult.join(', ')}` - : undefined, - source: 'mirror', - }; + ? `Some beatmapsets not found: ${missingIdsFromResult.join(", ")}` + : undefined, + source: "mirror", + }; + } + + async downloadBeatmapSet( + ctx: DownloadBeatmapSetOptions, + ): Promise> { + const beatmapsetFile = await this.StorageManager.getBeatmapsetFile(ctx); + + if (beatmapsetFile) { + return { + data: beatmapsetFile, + status: HttpStatusCode.Ok, + source: "storage", + }; + } + else if (beatmapsetFile === null) { + return { + data: null, + status: HttpStatusCode.NotFound, + message: "Beatmapset not found", + source: null, + }; + } + + const result = await this.MirrorsManager.downloadBeatmapSet(ctx); + + if (result.status >= 500) { + return this.formatResultAsServerError(result); } - async downloadBeatmapSet( - ctx: DownloadBeatmapSetOptions, - ): Promise> { - const beatmapsetFile = await this.StorageManager.getBeatmapsetFile(ctx); - - if (beatmapsetFile) { - return { - data: beatmapsetFile, - status: HttpStatusCode.Ok, - source: 'storage', - }; - } else if (beatmapsetFile === null) { - return { - data: null, - status: HttpStatusCode.NotFound, - message: 'Beatmapset not found', - source: null, - }; - } - - const result = await this.MirrorsManager.downloadBeatmapSet(ctx); - - if (result.status >= 500) { - return this.formatResultAsServerError(result); - } - - this.StorageManager.insertBeatmapsetFile(result.result, ctx); - - if (result.status >= 400 || !result.result) { - return { - data: null, - status: HttpStatusCode.NotFound, - message: 'Beatmapset not found', - source: null, - }; - } - - return { - data: result.result, - status: result.status, - source: 'mirror', - }; + this.StorageManager.insertBeatmapsetFile(result.result, ctx); + + if (result.status >= 400 || !result.result) { + return { + data: null, + status: HttpStatusCode.NotFound, + message: "Beatmapset not found", + source: null, + }; } - async downloadOsuBeatmap( - ctx: DownloadOsuBeatmap, - ): Promise> { - const beatmapOsuFile = await this.StorageManager.getOsuBeatmapFile(ctx); - - if (beatmapOsuFile) { - return { - data: beatmapOsuFile, - status: HttpStatusCode.Ok, - source: 'storage', - }; - } else if (beatmapOsuFile === null) { - return { - data: null, - status: HttpStatusCode.NotFound, - message: 'Osu file not found', - source: 'storage', - }; - } - - const result = await this.MirrorsManager.downloadOsuBeatmap(ctx); - - if (result.status >= 500) { - return this.formatResultAsServerError(result); - } - - this.StorageManager.insertBeatmapOsuFile(result.result, ctx); - - if (result.status >= 400 || !result.result) { - return { - data: null, - status: HttpStatusCode.NotFound, - message: 'Osu file not found', - source: null, - }; - } - - return { - data: result.result, - status: result.status, - source: 'mirror', - }; + return { + data: result.result, + status: result.status, + source: "mirror", + }; + } + + async downloadOsuBeatmap( + ctx: DownloadOsuBeatmap, + ): Promise> { + const beatmapOsuFile = await this.StorageManager.getOsuBeatmapFile(ctx); + + if (beatmapOsuFile) { + return { + data: beatmapOsuFile, + status: HttpStatusCode.Ok, + source: "storage", + }; } + else if (beatmapOsuFile === null) { + return { + data: null, + status: HttpStatusCode.NotFound, + message: "Osu file not found", + source: "storage", + }; + } + + const result = await this.MirrorsManager.downloadOsuBeatmap(ctx); - public async getManagerStats() { - return { - storage: await this.StorageManager.getStorageStatistics(), - mirrors: await this.MirrorsManager.getMirrorsStatistics(), - }; + if (result.status >= 500) { + return this.formatResultAsServerError(result); } - private formatResultAsServerError(result: ResultWithStatus) { - var message = INTERNAL_ERROR_RESPONSE; - message.status = result.status; + this.StorageManager.insertBeatmapOsuFile(result.result, ctx); - return message; + if (result.status >= 400 || !result.result) { + return { + data: null, + status: HttpStatusCode.NotFound, + message: "Osu file not found", + source: null, + }; } + + return { + data: result.result, + status: result.status, + source: "mirror", + }; + } + + public async getManagerStats() { + return { + storage: await this.StorageManager.getStorageStatistics(), + mirrors: await this.MirrorsManager.getMirrorsStatistics(), + }; + } + + private formatResultAsServerError(result: ResultWithStatus) { + const message = INTERNAL_ERROR_RESPONSE; + message.status = result.status; + + return message; + } } diff --git a/server/src/core/managers/calculator/calculator.manager.ts b/server/src/core/managers/calculator/calculator.manager.ts index 527a9dc..aace146 100644 --- a/server/src/core/managers/calculator/calculator.manager.ts +++ b/server/src/core/managers/calculator/calculator.manager.ts @@ -1,85 +1,86 @@ -import { HttpStatusCode } from 'axios'; -import { BeatmapsManagerInstance } from '../../../plugins/beatmapManager'; -import { BeatmapsManager } from '../beatmaps/beatmaps.manager'; -import { CalculatorService } from './calculator.service'; -import { Score, ScoreShort } from './calculator.types'; -import { Beatmap } from 'rosu-pp-js'; +import { HttpStatusCode } from "axios"; +import { Beatmap } from "rosu-pp-js"; -export class CalculatorManager { - private readonly calculatorService: CalculatorService; - private readonly beatmapsManager: BeatmapsManager; +import { BeatmapsManagerInstance } from "../../../plugins/beatmapManager"; +import type { BeatmapsManager } from "../beatmaps/beatmaps.manager"; +import { CalculatorService } from "./calculator.service"; +import type { Score, ScoreShort } from "./calculator.types"; - constructor() { - this.calculatorService = new CalculatorService(); - this.beatmapsManager = BeatmapsManagerInstance; +export class CalculatorManager { + private readonly calculatorService: CalculatorService; + private readonly beatmapsManager: BeatmapsManager; + + constructor() { + this.calculatorService = new CalculatorService(); + this.beatmapsManager = BeatmapsManagerInstance; + } + + public async CalculateBeatmapPerformances( + beatmapId: number, + scores: ScoreShort[], + ) { + const beatmap = await this.GetBeatmapHash(beatmapId); + if (!(beatmap instanceof Beatmap)) { + return beatmap; } - public async CalculateBeatmapPerformances( - beatmapId: number, - scores: ScoreShort[], - ) { - const beatmap = await this.GetBeatmapHash(beatmapId); - if (!(beatmap instanceof Beatmap)) { - return beatmap; - } + const results = this.calculatorService.CalculateBeatmapPerfomance( + beatmap, + scores, + ); - const results = this.calculatorService.CalculateBeatmapPerfomance( - beatmap, - scores, - ); + beatmap.free(); - beatmap.free(); + return results; + } - return results; + public async CalculateScorePerformance( + beatmapId: number, + score: Score, + beatmapHash?: string, + ) { + const beatmap = await this.GetBeatmapHash(beatmapId, beatmapHash); + if (!(beatmap instanceof Beatmap)) { + return beatmap; } - public async CalculateScorePerformance( - beatmapId: number, - score: Score, - beatmapHash?: string, - ) { - const beatmap = await this.GetBeatmapHash(beatmapId, beatmapHash); - if (!(beatmap instanceof Beatmap)) { - return beatmap; - } + const result = this.calculatorService.CalculateScorePerfomance( + beatmap, + score, + ); + + beatmap.free(); - const result = this.calculatorService.CalculateScorePerfomance( - beatmap, - score, - ); + return result; + } - beatmap.free(); + private async GetBeatmapHash(beatmapId: number, beatmapHash?: string) { + const beatmapBuffer = await this.beatmapsManager.downloadOsuBeatmap({ + beatmapId, + }); - return result; + if (beatmapBuffer.data === null) { + return beatmapBuffer; } - private async GetBeatmapHash(beatmapId: number, beatmapHash?: string) { - const beatmapBuffer = await this.beatmapsManager.downloadOsuBeatmap({ - beatmapId, - }); - - if (beatmapBuffer.data === null) { - return beatmapBuffer; - } - - if (beatmapHash) { - const fileHash = this.calculatorService.GetHashOfOsuFile( - beatmapBuffer.data, - ); - - if (fileHash != beatmapHash) { - return { - data: null, - status: HttpStatusCode.NotFound, - message: 'Osu file with provided beatmap hash not found', - }; - } - } - - const beatmap = this.calculatorService.ConvertBufferToBeatmap( - beatmapBuffer.data, - ); - - return beatmap; + if (beatmapHash) { + const fileHash = this.calculatorService.GetHashOfOsuFile( + beatmapBuffer.data, + ); + + if (fileHash !== beatmapHash) { + return { + data: null, + status: HttpStatusCode.NotFound, + message: "Osu file with provided beatmap hash not found", + }; + } } + + const beatmap = this.calculatorService.ConvertBufferToBeatmap( + beatmapBuffer.data, + ); + + return beatmap; + } } diff --git a/server/src/core/managers/calculator/calculator.service.ts b/server/src/core/managers/calculator/calculator.service.ts index c108964..d74278f 100644 --- a/server/src/core/managers/calculator/calculator.service.ts +++ b/server/src/core/managers/calculator/calculator.service.ts @@ -1,78 +1,80 @@ -import * as rosu from 'rosu-pp-js'; -import { GameModBitwise } from '../../../types/general/gameMod'; -import crypto from 'crypto'; -import { Score, ScoreShort } from './calculator.types'; -import { HitResultPriority } from 'rosu-pp-js'; +import crypto from "node:crypto"; -export class CalculatorService { - public CalculateBeatmapPerfomance( - beatmap: rosu.Beatmap, - scores: ScoreShort[], - ) { - const results: rosu.PerformanceAttributes[] = []; +import * as rosu from "rosu-pp-js"; +import { HitResultPriority } from "rosu-pp-js"; + +import { GameModBitwise } from "../../../types/general/gameMod"; +import type { Score, ScoreShort } from "./calculator.types"; - for (const score of scores) { - if (score.mode != undefined && beatmap.mode != score.mode) { - beatmap.convert(score.mode, score.mods); - } +export class CalculatorService { + public CalculateBeatmapPerfomance( + beatmap: rosu.Beatmap, + scores: ScoreShort[], + ) { + const results: rosu.PerformanceAttributes[] = []; - const performance = new rosu.Performance({ - accuracy: score.accuracy, - mods: score.mods ?? GameModBitwise.NoMod, - combo: score.combo, - misses: score.misses, - lazer: score.isLazer, - hitresultPriority: HitResultPriority.Fastest, - }).calculate(beatmap); + for (const score of scores) { + if (score.mode !== undefined && beatmap.mode !== score.mode) { + beatmap.convert(score.mode, score.mods); + } - results.push(performance); - } + const performance = new rosu.Performance({ + accuracy: score.accuracy, + mods: score.mods ?? GameModBitwise.NoMod, + combo: score.combo, + misses: score.misses, + lazer: score.isLazer, + hitresultPriority: HitResultPriority.Fastest, + }).calculate(beatmap); - return results; + results.push(performance); } - public CalculateScorePerfomance(beatmap: rosu.Beatmap, score: Score) { - if (score.mode != undefined && beatmap.mode != score.mode) { - beatmap.convert(score.mode, score.mods); - } + return results; + } + + public CalculateScorePerfomance(beatmap: rosu.Beatmap, score: Score) { + if (score.mode !== undefined && beatmap.mode !== score.mode) { + beatmap.convert(score.mode, score.mods); + } - const isHitresultsProvided = - score.n300 !== undefined || - score.n100 !== undefined || - score.n50 !== undefined || - score.nGeki !== undefined || - score.nKatu !== undefined; + const isHitresultsProvided + = score.n300 !== undefined + || score.n100 !== undefined + || score.n50 !== undefined + || score.nGeki !== undefined + || score.nKatu !== undefined; - const hitresultPriority = isHitresultsProvided - ? score.isScoreFailed - ? rosu.HitResultPriority.WorstCase - : rosu.HitResultPriority.BestCase - : rosu.HitResultPriority.BestCase; + const hitresultPriority = isHitresultsProvided + ? score.isScoreFailed + ? rosu.HitResultPriority.WorstCase + : rosu.HitResultPriority.BestCase + : rosu.HitResultPriority.BestCase; - const performance = new rosu.Performance({ - accuracy: score.accuracy, - mods: score.mods ?? GameModBitwise.NoMod, - combo: score.combo, - n300: score.n300, - nGeki: score.nGeki, - n100: score.n100, - nKatu: score.nKatu, - n50: score.n50, - misses: score.misses, - hitresultPriority: hitresultPriority, - lazer: score.isLazer, - }).calculate(beatmap); + const performance = new rosu.Performance({ + accuracy: score.accuracy, + mods: score.mods ?? GameModBitwise.NoMod, + combo: score.combo, + n300: score.n300, + nGeki: score.nGeki, + n100: score.n100, + nKatu: score.nKatu, + n50: score.n50, + misses: score.misses, + hitresultPriority, + lazer: score.isLazer, + }).calculate(beatmap); - return performance; - } + return performance; + } - public ConvertBufferToBeatmap(buffer: ArrayBuffer) { - return new rosu.Beatmap(new Uint8Array(buffer)); - } + public ConvertBufferToBeatmap(buffer: ArrayBuffer) { + return new rosu.Beatmap(new Uint8Array(buffer)); + } - public GetHashOfOsuFile(arrayBuffer: ArrayBuffer) { - const hash = crypto.createHash('md5'); - hash.update(new Uint8Array(arrayBuffer)); - return hash.digest('hex'); - } + public GetHashOfOsuFile(arrayBuffer: ArrayBuffer) { + const hash = crypto.createHash("md5"); + hash.update(new Uint8Array(arrayBuffer)); + return hash.digest("hex"); + } } diff --git a/server/src/core/managers/calculator/calculator.types.ts b/server/src/core/managers/calculator/calculator.types.ts index a231dbc..5b8495c 100644 --- a/server/src/core/managers/calculator/calculator.types.ts +++ b/server/src/core/managers/calculator/calculator.types.ts @@ -1,20 +1,21 @@ -import * as rosu from 'rosu-pp-js'; -import { GameModBitwise } from '../../../types/general/gameMod'; +import type * as rosu from "rosu-pp-js"; + +import type { GameModBitwise } from "../../../types/general/gameMod"; export interface Score extends ScoreShort { - n300?: number; - nGeki?: number; - n100?: number; - nKatu?: number; - n50?: number; + n300?: number; + nGeki?: number; + n100?: number; + nKatu?: number; + n50?: number; } export interface ScoreShort { - accuracy?: number; - mods?: GameModBitwise; - mode?: rosu.GameMode; - combo?: number; - misses?: number; - isScoreFailed: boolean; - isLazer: boolean; + accuracy?: number; + mods?: GameModBitwise; + mode?: rosu.GameMode; + combo?: number; + misses?: number; + isScoreFailed: boolean; + isLazer: boolean; } diff --git a/server/src/core/managers/mirrors/mirrors-manager.service.ts b/server/src/core/managers/mirrors/mirrors-manager.service.ts index 19bd6f5..6a27bc5 100644 --- a/server/src/core/managers/mirrors/mirrors-manager.service.ts +++ b/server/src/core/managers/mirrors/mirrors-manager.service.ts @@ -1,148 +1,148 @@ -import { getMirrors, createMirror } from '../../../database/models/mirrors'; -import { getRequestsByBaseUrl } from '../../../database/models/requests'; -import { Mirror } from '../../../database/schema'; -import { BenchmarkResult } from '../../../types/benchmark'; -import { splitByCondition } from '../../../utils/array'; -import { getUTCDate } from '../../../utils/date'; -import logger from '../../../utils/logger'; -import { MirrorClient } from '../../abstracts/client/base-client.types'; -import { CompareService } from '../../services/compare.service'; +import { createMirror, getMirrors } from "../../../database/models/mirrors"; +import { getRequestsByBaseUrl } from "../../../database/models/requests"; +import type { Mirror } from "../../../database/schema"; +import type { BenchmarkResult } from "../../../types/benchmark"; +import { splitByCondition } from "../../../utils/array"; +import { getUTCDate } from "../../../utils/date"; +import logger from "../../../utils/logger"; +import type { MirrorClient } from "../../abstracts/client/base-client.types"; +import { CompareService } from "../../services/compare.service"; export class MirrorsManagerService { - private readonly clients: MirrorClient[]; - private readonly compareService: CompareService; + private readonly clients: MirrorClient[]; + private readonly compareService: CompareService; - constructor(clients: MirrorClient[]) { - this.compareService = new CompareService(); - this.clients = clients; + constructor(clients: MirrorClient[]) { + this.compareService = new CompareService(); + this.clients = clients; - setInterval( - () => { - this.fetchMirrorsData(); - }, - 1000 * 60 * 10, - ); // 10 minutes + setInterval( + () => { + this.fetchMirrorsData(); + }, + 1000 * 60 * 10, + ); // 10 minutes - this.log('Initialized'); - } + this.log("Initialized"); + } - public async fetchMirrorsData(): Promise { - const dbClients = await getMirrors(); + public async fetchMirrorsData(): Promise { + const dbClients = await getMirrors(); - this.log( - 'Started updating mirrors data. Perfomance may be affected', - 'warn', - ); + this.log( + "Started updating mirrors data. Perfomance may be affected", + "warn", + ); - for (const client of this.clients) { - let dbClient = dbClients.find( - (c) => c.url === client.client.clientConfig.baseUrl, - ); + for (const client of this.clients) { + let dbClient = dbClients.find( + c => c.url === client.client.clientConfig.baseUrl, + ); - if (!dbClient) { - this.log( + if (!dbClient) { + this.log( `Mirror ${client.client.clientConfig.baseUrl} not found in database, creating new entry`, - 'warn', - ); - - dbClient = await createMirror({ - url: client.client.clientConfig.baseUrl, - }); - } - - const benchmark = await this.getMirrorBenchmark(client, dbClient); - - client.weights = { - API: this.exponentialDecrease(benchmark.latency || Infinity), - download: this.exponentialIncrease( - benchmark.downloadSpeed || 0, - ), - failrate: benchmark.failrate, - }; - } - - this.log('Finished updating mirrors data, current weights:'); - this.clients.forEach((client) => { - this.log( - `${client.client.clientConfig.baseUrl} - API: ${client.weights.API}, download: ${client.weights.download}, failrate: ${client.weights.failrate}`, - ); - }); - } - - private async getMirrorBenchmark( - client: MirrorClient, - dbClient: Mirror, - ): Promise { - const requests = await getRequestsByBaseUrl( - dbClient.url, - getUTCDate().getTime() - 3 * 60 * 60 * 1000, // 3 hours - ); - - const [failedRequests, successfulRequests] = splitByCondition( - requests, - (r) => r.status >= 400 && r.status !== 404, + "warn", ); - const [jsonRequests, downloadRequests] = splitByCondition( - successfulRequests.filter((r) => r.status !== 404), - (r) => r.contentType === 'application/json', - ); - - const failrate = failedRequests.length / requests.length || 0; - - const toBenchmark = this.compareService.abilitiesToBenchmark(client); + dbClient = await createMirror({ + url: client.client.clientConfig.baseUrl, + }); + } - if ( - (toBenchmark.api && jsonRequests.length === 0) || - (toBenchmark.download && downloadRequests.length === 0) - ) { - this.log( - `Not enough data to calculate benchmark, fetching new data from "${dbClient.url}"`, - 'warn', - ); - - const benchmark = await this.fetchMirrorBenchmark(client); - - return { - ...benchmark, - failrate, - }; - } - - const latency = - jsonRequests.reduce((acc, r) => acc + (r.latency ?? 0), 0) / - jsonRequests.length; - - const downloadSpeed = - downloadRequests.reduce( - (acc, r) => acc + (r.downloadSpeed ?? 0), - 0, - ) / downloadRequests.length; - - return { - latency, - downloadSpeed, - failrate, - }; - } + const benchmark = await this.getMirrorBenchmark(client, dbClient); - private exponentialDecrease(x: number): number { - return 10000 * Math.exp((-1 / 10000) * x); + client.weights = { + API: this.exponentialDecrease(benchmark.latency || Infinity), + download: this.exponentialIncrease( + benchmark.downloadSpeed || 0, + ), + failrate: benchmark.failrate, + }; } - private exponentialIncrease(x: number): number { - return 10000 * (1 - Math.exp((-1 / 10000) * x)); - } + this.log("Finished updating mirrors data, current weights:"); + this.clients.forEach((client) => { + this.log( + `${client.client.clientConfig.baseUrl} - API: ${client.weights.API}, download: ${client.weights.download}, failrate: ${client.weights.failrate}`, + ); + }); + } + + private async getMirrorBenchmark( + client: MirrorClient, + dbClient: Mirror, + ): Promise { + const requests = await getRequestsByBaseUrl( + dbClient.url, + getUTCDate().getTime() - 3 * 60 * 60 * 1000, // 3 hours + ); + + const [failedRequests, successfulRequests] = splitByCondition( + requests, + r => r.status >= 400 && r.status !== 404, + ); + + const [jsonRequests, downloadRequests] = splitByCondition( + successfulRequests.filter(r => r.status !== 404), + r => r.contentType === "application/json", + ); + + const failrate = failedRequests.length / requests.length || 0; + + const toBenchmark = this.compareService.abilitiesToBenchmark(client); + + if ( + (toBenchmark.api && jsonRequests.length === 0) + || (toBenchmark.download && downloadRequests.length === 0) + ) { + this.log( + `Not enough data to calculate benchmark, fetching new data from "${dbClient.url}"`, + "warn", + ); - private async fetchMirrorBenchmark( - client: MirrorClient, - ): Promise { - const result = await this.compareService.benchmarkMirror(client); + const benchmark = await this.fetchMirrorBenchmark(client); - return result; + return { + ...benchmark, + failrate, + }; } - private log(message: string, level: 'info' | 'warn' | 'error' = 'info') { - logger[level](`MirrorsManagerService: ${message}`); - } + const latency + = jsonRequests.reduce((acc, r) => acc + (r.latency ?? 0), 0) + / jsonRequests.length; + + const downloadSpeed + = downloadRequests.reduce( + (acc, r) => acc + (r.downloadSpeed ?? 0), + 0, + ) / downloadRequests.length; + + return { + latency, + downloadSpeed, + failrate, + }; + } + + private exponentialDecrease(x: number): number { + return 10000 * Math.exp((-1 / 10000) * x); + } + + private exponentialIncrease(x: number): number { + return 10000 * (1 - Math.exp((-1 / 10000) * x)); + } + + private async fetchMirrorBenchmark( + client: MirrorClient, + ): Promise { + const result = await this.compareService.benchmarkMirror(client); + + return result; + } + + private log(message: string, level: "info" | "warn" | "error" = "info") { + logger[level](`MirrorsManagerService: ${message}`); + } } diff --git a/server/src/core/managers/mirrors/mirrors.manager.ts b/server/src/core/managers/mirrors/mirrors.manager.ts index c7345a4..2ab18be 100644 --- a/server/src/core/managers/mirrors/mirrors.manager.ts +++ b/server/src/core/managers/mirrors/mirrors.manager.ts @@ -1,383 +1,386 @@ +import config from "../../../config"; import { - MirrorClient, - GetBeatmapSetOptions, - ResultWithStatus, - ClientAbilities, - GetBeatmapOptions, - DownloadBeatmapSetOptions, - SearchBeatmapsetsOptions, - GetBeatmapsOptions, - DownloadOsuBeatmap, - GetBeatmapsetsByBeatmapIdsOptions, -} from '../../abstracts/client/base-client.types'; -import { DirectClient, BanchoClient } from '../../domains'; -import { MirrorsManagerService } from './mirrors-manager.service'; -import config from '../../../config'; -import logger from '../../../utils/logger'; -import { Beatmap, Beatmapset } from '../../../types/general/beatmap'; -import { MinoClient } from '../../domains/catboy.best/mino.client'; -import { GatariClient } from '../../domains/gatari.pw/gatari.client'; -import { NerinyanClient } from '../../domains/nerinyan.moe/nerinyan.client'; + getMirrorsRequestsCountForStats, +} from "../../../database/models/requests"; +import type { Beatmap, Beatmapset } from "../../../types/general/beatmap"; +import logger from "../../../utils/logger"; import { - getMirrorsRequestsCountForStats, - getRequestsCount, -} from '../../../database/models/requests'; -import { getUTCDate } from '../../../utils/date'; -import { OsulabsClient } from '../../domains/beatmaps.download/osulabs.client'; -import { StorageManager } from '../storage/storage.manager'; -import { TimeRange } from '../../../types/stats'; + getMirrorsRequestsQueryData, + TIME_RANGES_FOR_MIRRORS_STATS, +} from "../../../utils/mirrors-stats"; +import type { + DownloadBeatmapSetOptions, + DownloadOsuBeatmap, + GetBeatmapOptions, + GetBeatmapSetOptions, + GetBeatmapsetsByBeatmapIdsOptions, + GetBeatmapsOptions, + MirrorClient, + ResultWithStatus, + SearchBeatmapsetsOptions, +} from "../../abstracts/client/base-client.types"; import { - getMirrorsRequestsQueryData, - TIME_RANGES_FOR_MIRRORS_STATS, -} from '../../../utils/mirrors-stats'; + ClientAbilities, +} from "../../abstracts/client/base-client.types"; +import { BanchoClient, DirectClient } from "../../domains"; +import { OsulabsClient } from "../../domains/beatmaps.download/osulabs.client"; +import { MinoClient } from "../../domains/catboy.best/mino.client"; +import { GatariClient } from "../../domains/gatari.pw/gatari.client"; +import { NerinyanClient } from "../../domains/nerinyan.moe/nerinyan.client"; +import type { StorageManager } from "../storage/storage.manager"; +import { MirrorsManagerService } from "./mirrors-manager.service"; const DEFAULT_CLIENT_PROPS = { - weights: { - download: 0, - API: 0, - failrate: 0, - }, + weights: { + download: 0, + API: 0, + failrate: 0, + }, }; export class MirrorsManager { - private readonly managerService: MirrorsManagerService; - private readonly storageManager: StorageManager; + private readonly managerService: MirrorsManagerService; + private readonly storageManager: StorageManager; - private readonly clients: MirrorClient[] = []; + private readonly clients: MirrorClient[] = []; - constructor(storageManager: StorageManager) { - this.clients = []; + constructor(storageManager: StorageManager) { + this.clients = []; - this.storageManager = storageManager; + this.storageManager = storageManager; - if (!config.MirrorsToIgnore.includes('direct')) { - const directClient = new DirectClient(); + if (!config.MirrorsToIgnore.includes("direct")) { + const directClient = new DirectClient(); - this.clients.push({ - client: directClient, - ...DEFAULT_CLIENT_PROPS, - }); - } + this.clients.push({ + client: directClient, + ...DEFAULT_CLIENT_PROPS, + }); + } - if (!config.MirrorsToIgnore.includes('mino')) { - const minoClient = new MinoClient(storageManager); + if (!config.MirrorsToIgnore.includes("mino")) { + const minoClient = new MinoClient(storageManager); - this.clients.push({ - client: minoClient, - ...DEFAULT_CLIENT_PROPS, - }); - } + this.clients.push({ + client: minoClient, + ...DEFAULT_CLIENT_PROPS, + }); + } - if (!config.MirrorsToIgnore.includes('osulabs')) { - const osulabsClient = new OsulabsClient(storageManager); + if (!config.MirrorsToIgnore.includes("osulabs")) { + const osulabsClient = new OsulabsClient(storageManager); - this.clients.push({ - client: osulabsClient, - ...DEFAULT_CLIENT_PROPS, - }); - } + this.clients.push({ + client: osulabsClient, + ...DEFAULT_CLIENT_PROPS, + }); + } - if (!config.MirrorsToIgnore.includes('gatari')) { - const gatariClient = new GatariClient(); + if (!config.MirrorsToIgnore.includes("gatari")) { + const gatariClient = new GatariClient(); - this.clients.push({ - client: gatariClient, - ...DEFAULT_CLIENT_PROPS, - }); - } + this.clients.push({ + client: gatariClient, + ...DEFAULT_CLIENT_PROPS, + }); + } - if (!config.MirrorsToIgnore.includes('nerinyan')) { - const nerinyanClient = new NerinyanClient(); + if (!config.MirrorsToIgnore.includes("nerinyan")) { + const nerinyanClient = new NerinyanClient(); - this.clients.push({ - client: nerinyanClient, - ...DEFAULT_CLIENT_PROPS, - }); - } + this.clients.push({ + client: nerinyanClient, + ...DEFAULT_CLIENT_PROPS, + }); + } - if (!config.MirrorsToIgnore.includes('bancho') && config.UseBancho) { - const banchoClient = new BanchoClient(); + if (!config.MirrorsToIgnore.includes("bancho") && config.UseBancho) { + const banchoClient = new BanchoClient(); - this.clients.push({ - client: banchoClient, - ...DEFAULT_CLIENT_PROPS, - }); - } + this.clients.push({ + client: banchoClient, + ...DEFAULT_CLIENT_PROPS, + }); + } - this.managerService = new MirrorsManagerService(this.clients); + this.managerService = new MirrorsManagerService(this.clients); - this.managerService.fetchMirrorsData().then(() => { - this.log('Initialized'); - }); + this.managerService.fetchMirrorsData().then(() => { + this.log("Initialized"); + }); + } + + async getBeatmapSet( + ctx: GetBeatmapSetOptions, + ): Promise> { + if (!ctx.beatmapSetId) { + throw new Error("beatmapSetId is required to fetch beatmap set"); } - async getBeatmapSet( - ctx: GetBeatmapSetOptions, - ): Promise> { - if (!ctx.beatmapSetId) { - throw new Error('beatmapSetId is required to fetch beatmap set'); - } + const criteria = ClientAbilities.GetBeatmapSetById; - const criteria = ClientAbilities.GetBeatmapSetById; + return await this.useMirror(ctx, criteria, "getBeatmapSet"); + } - return await this.useMirror(ctx, criteria, 'getBeatmapSet'); + async getBeatmap( + ctx: GetBeatmapOptions, + ): Promise> { + if (!ctx.beatmapId && !ctx.beatmapHash) { + throw new Error("Either beatmapId or beatmapHash is required"); } - async getBeatmap( - ctx: GetBeatmapOptions, - ): Promise> { - if (!ctx.beatmapId && !ctx.beatmapHash) { - throw new Error('Either beatmapId or beatmapHash is required'); - } - - let criteria: ClientAbilities; - if (ctx.beatmapId) { - criteria = ctx.allowMissingNonBeatmapValues - ? ClientAbilities.GetBeatmapByIdWithSomeNonBeatmapValues - : ClientAbilities.GetBeatmapById; - } else { - criteria = ctx.allowMissingNonBeatmapValues - ? ClientAbilities.GetBeatmapByHashWithSomeNonBeatmapValues - : ClientAbilities.GetBeatmapByHash; - } - - return await this.useMirror(ctx, criteria, 'getBeatmap'); + let criteria: ClientAbilities; + if (ctx.beatmapId) { + criteria = ctx.allowMissingNonBeatmapValues + ? ClientAbilities.GetBeatmapByIdWithSomeNonBeatmapValues + : ClientAbilities.GetBeatmapById; } - - async searchBeatmapsets( - ctx: SearchBeatmapsetsOptions, - ): Promise> { - const criteria = ClientAbilities.SearchBeatmapsets; - - return await this.useMirror( - ctx, - criteria, - 'searchBeatmapsets', - ); + else { + criteria = ctx.allowMissingNonBeatmapValues + ? ClientAbilities.GetBeatmapByHashWithSomeNonBeatmapValues + : ClientAbilities.GetBeatmapByHash; } - async getBeatmaps( - ctx: GetBeatmapsOptions, - ): Promise> { - const { ids } = ctx; + return await this.useMirror(ctx, criteria, "getBeatmap"); + } - if (!ids || ids.length === 0) { - throw new Error('ids is required to fetch beatmaps'); - } + async searchBeatmapsets( + ctx: SearchBeatmapsetsOptions, + ): Promise> { + const criteria = ClientAbilities.SearchBeatmapsets; - const criteria = ClientAbilities.GetBeatmaps; + return await this.useMirror( + ctx, + criteria, + "searchBeatmapsets", + ); + } - return await this.useMirror(ctx, criteria, 'getBeatmaps'); + async getBeatmaps( + ctx: GetBeatmapsOptions, + ): Promise> { + const { ids } = ctx; + + if (!ids || ids.length === 0) { + throw new Error("ids is required to fetch beatmaps"); } - async getBeatmapsetsByBeatmapIds( - ctx: GetBeatmapsetsByBeatmapIdsOptions, - ): Promise> { - const { beatmapIds } = ctx; + const criteria = ClientAbilities.GetBeatmaps; - if (!beatmapIds || beatmapIds.length === 0) { - throw new Error('beatmapIds is required to fetch beatmapsets'); - } + return await this.useMirror(ctx, criteria, "getBeatmaps"); + } - const criteria = ClientAbilities.GetBeatmapsetsByBeatmapIds; + async getBeatmapsetsByBeatmapIds( + ctx: GetBeatmapsetsByBeatmapIdsOptions, + ): Promise> { + const { beatmapIds } = ctx; - return await this.useMirror( - ctx, - criteria, - 'getBeatmapsetsByBeatmapIds', - ); + if (!beatmapIds || beatmapIds.length === 0) { + throw new Error("beatmapIds is required to fetch beatmapsets"); } - async downloadBeatmapSet( - ctx: DownloadBeatmapSetOptions, - ): Promise> { - if (!ctx.beatmapSetId) { - throw new Error('beatmapSetId is required to download beatmap set'); - } - - let criteria: ClientAbilities; - if (ctx.noVideo) { - criteria = ClientAbilities.DownloadBeatmapSetByIdNoVideo; - } else { - criteria = ClientAbilities.DownloadBeatmapSetById; - } - - return await this.useMirror( - ctx, - criteria, - 'downloadBeatmapSet', - ); - } + const criteria = ClientAbilities.GetBeatmapsetsByBeatmapIds; - async downloadOsuBeatmap( - ctx: DownloadOsuBeatmap, - ): Promise> { - const criteria = ClientAbilities.DownloadOsuBeatmap; + return await this.useMirror( + ctx, + criteria, + "getBeatmapsetsByBeatmapIds", + ); + } - return await this.useMirror( - ctx, - criteria, - 'downloadOsuBeatmap', - ); + async downloadBeatmapSet( + ctx: DownloadBeatmapSetOptions, + ): Promise> { + if (!ctx.beatmapSetId) { + throw new Error("beatmapSetId is required to download beatmap set"); } - async getMirrorsStatistics() { - const data = await getMirrorsRequestsCountForStats( - getMirrorsRequestsQueryData(this.clients), - ); - - const dataMap = new Map(); - for (const item of data) { - const statusKey = - item.statuscodes === null - ? 'null' - : item.statuscodes.includes(200) - ? 'success' - : item.statuscodes.includes(500) - ? 'fail' - : 'other'; - const key = `${item.name}|${item.createdafter}|${statusKey}`; - dataMap.set(key, item.count); - } - - return { - activeMirrors: await Promise.all( - this.clients.map(async (c) => { - const baseUrl = c.client.clientConfig.baseUrl; - const stats = Object.fromEntries( - TIME_RANGES_FOR_MIRRORS_STATS.map(({ name, time }) => [ - name, - { - total: Number( - dataMap.get(`${baseUrl}|${time}|null`) || 0, - ), - successful: Number( - dataMap.get(`${baseUrl}|${time}|success`) || - 0, - ), - failed: Number( - dataMap.get(`${baseUrl}|${time}|fail`) || 0, - ), - }, - ]), - ); - - return { - name: c.client.constructor.name, - url: baseUrl, - onCooldownUntil: c.client.onCooldownUntil(), - rateLimit: c.client.getCapacities(), - requests: stats, - }; - }), - ), - rateLimitsTotal: this.clients.reduce( - (acc, c) => { - const clientCapacities = c.client.getCapacities(); - - for (const capacity of clientCapacities) { - if (!acc[capacity.ability]) { - acc[capacity.ability] = { total: 0, remaining: 0 }; - } - acc[capacity.ability].total += capacity.limit; - acc[capacity.ability].remaining += capacity.remaining; - } - - return acc; - }, - {} as Record, - ), - }; + let criteria: ClientAbilities; + if (ctx.noVideo) { + criteria = ClientAbilities.DownloadBeatmapSetByIdNoVideo; } - - private async useMirror( - ctx: - | DownloadBeatmapSetOptions - | GetBeatmapOptions - | GetBeatmapSetOptions - | SearchBeatmapsetsOptions - | GetBeatmapsOptions - | DownloadOsuBeatmap - | GetBeatmapsetsByBeatmapIdsOptions, - criteria: ClientAbilities, - action: keyof MirrorClient['client'], - ): Promise> { - const usedClients: MirrorClient[] = []; - for (const _ of this.clients) { - const client = this.getClient(criteria, usedClients); - if (!client) return { result: null, status: 501 }; - - const result = await (client.client[action] as Function)(ctx); - if (result.result || result.status === 404) return result; - - usedClients.push(client); - } - return { result: null, status: 502 }; + else { + criteria = ClientAbilities.DownloadBeatmapSetById; } - private getClient( - criteria: ClientAbilities, - ignore?: MirrorClient[], - ): MirrorClient | null { - const clients = this.clients - .filter((client) => - client.client.clientConfig.abilities.includes(criteria), - ) - .filter((client) => !ignore || !ignore.includes(client)); - - const client = this.getClientByWeight(criteria, clients); - - return client; + return await this.useMirror( + ctx, + criteria, + "downloadBeatmapSet", + ); + } + + async downloadOsuBeatmap( + ctx: DownloadOsuBeatmap, + ): Promise> { + const criteria = ClientAbilities.DownloadOsuBeatmap; + + return await this.useMirror( + ctx, + criteria, + "downloadOsuBeatmap", + ); + } + + async getMirrorsStatistics() { + const data = await getMirrorsRequestsCountForStats( + getMirrorsRequestsQueryData(this.clients), + ); + + const dataMap = new Map(); + for (const item of data) { + const statusKey + = item.statuscodes === null + ? "null" + : item.statuscodes.includes(200) + ? "success" + : item.statuscodes.includes(500) + ? "fail" + : "other"; + const key = `${item.name}|${item.createdafter}|${statusKey}`; + dataMap.set(key, item.count); } - private getClientByWeight( - criteria: ClientAbilities, - clients: MirrorClient[], - ): MirrorClient | null { - let bestClient: MirrorClient | null = null; - let bestWeight = -1; - - for (const client of clients) { - const weight = this.getClientWeight(client, criteria); - - if (weight > bestWeight) { - bestWeight = weight; - bestClient = client; + return { + activeMirrors: await Promise.all( + this.clients.map(async (c) => { + const { baseUrl } = c.client.clientConfig; + const stats = Object.fromEntries( + TIME_RANGES_FOR_MIRRORS_STATS.map(({ name, time }) => [ + name, + { + total: Number( + dataMap.get(`${baseUrl}|${time}|null`) || 0, + ), + successful: Number( + dataMap.get(`${baseUrl}|${time}|success`) + || 0, + ), + failed: Number( + dataMap.get(`${baseUrl}|${time}|fail`) || 0, + ), + }, + ]), + ); + + return { + name: c.client.constructor.name, + url: baseUrl, + onCooldownUntil: c.client.onCooldownUntil(), + rateLimit: c.client.getCapacities(), + requests: stats, + }; + }), + ), + rateLimitsTotal: this.clients.reduce( + (acc, c) => { + const clientCapacities = c.client.getCapacities(); + + for (const capacity of clientCapacities) { + if (!acc[capacity.ability]) { + acc[capacity.ability] = { total: 0, remaining: 0 }; } - } - - if (bestWeight === -1 || !bestClient) { - return null; - } + acc[capacity.ability].total += capacity.limit; + acc[capacity.ability].remaining += capacity.remaining; + } + + return acc; + }, + {} as Record, + ), + }; + } + + private async useMirror( + ctx: + | DownloadBeatmapSetOptions + | GetBeatmapOptions + | GetBeatmapSetOptions + | SearchBeatmapsetsOptions + | GetBeatmapsOptions + | DownloadOsuBeatmap + | GetBeatmapsetsByBeatmapIdsOptions, + criteria: ClientAbilities, + action: keyof MirrorClient["client"], + ): Promise> { + const usedClients: MirrorClient[] = []; + for (const _ of this.clients) { + const client = this.getClient(criteria, usedClients); + if (!client) + return { result: null, status: 501 }; + + const result = await (client.client[action] as (ctx: any) => Promise>)(ctx); + if (result.result || result.status === 404) + return result; + + usedClients.push(client); + } + return { result: null, status: 502 }; + } + + private getClient( + criteria: ClientAbilities, + ignore?: MirrorClient[], + ): MirrorClient | null { + const clients = this.clients + .filter(client => + client.client.clientConfig.abilities.includes(criteria), + ) + .filter(client => !ignore || !ignore.includes(client)); + + const client = this.getClientByWeight(criteria, clients); + + return client; + } + + private getClientByWeight( + criteria: ClientAbilities, + clients: MirrorClient[], + ): MirrorClient | null { + let bestClient: MirrorClient | null = null; + let bestWeight = -1; + + for (const client of clients) { + const weight = this.getClientWeight(client, criteria); + + if (weight > bestWeight) { + bestWeight = weight; + bestClient = client; + } + } - return bestClient; + if (bestWeight === -1 || !bestClient) { + return null; } - private getClientWeight(client: MirrorClient, ability: ClientAbilities) { - const { limit, remaining } = client.client.getCapacity(ability); + return bestClient; + } - const percentageWeight = remaining / limit; - const capacityBonus = Math.log10(remaining + 1); - const rateLimitWeight = percentageWeight * capacityBonus; + private getClientWeight(client: MirrorClient, ability: ClientAbilities) { + const { limit, remaining } = client.client.getCapacity(ability); - const isDownload = [ - ClientAbilities.DownloadBeatmapSetById, - ClientAbilities.DownloadBeatmapSetByIdNoVideo, - ClientAbilities.DownloadOsuBeatmap, - ].includes(ability); + const percentageWeight = remaining / limit; + const capacityBonus = Math.log10(remaining + 1); + const rateLimitWeight = percentageWeight * capacityBonus; - const latencyWeight = isDownload - ? client.weights.download - : client.weights.API; + const isDownload = [ + ClientAbilities.DownloadBeatmapSetById, + ClientAbilities.DownloadBeatmapSetByIdNoVideo, + ClientAbilities.DownloadOsuBeatmap, + ].includes(ability); - return ( - Math.max(0.00000001, rateLimitWeight) * - Math.max(0.00000001, latencyWeight) * - Math.max(0.00000001, 1 - client.weights.failrate) - ); - } + const latencyWeight = isDownload + ? client.weights.download + : client.weights.API; - private log(message: string, level: 'info' | 'warn' | 'error' = 'info') { - logger[level](`MirrorsManager: ${message}`); - } + return ( + Math.max(0.00000001, rateLimitWeight) + * Math.max(0.00000001, latencyWeight) + * Math.max(0.00000001, 1 - client.weights.failrate) + ); + } + + private log(message: string, level: "info" | "warn" | "error" = "info") { + logger[level](`MirrorsManager: ${message}`); + } } diff --git a/server/src/core/managers/storage/storage-cache.service.ts b/server/src/core/managers/storage/storage-cache.service.ts index af2c879..37bd774 100644 --- a/server/src/core/managers/storage/storage-cache.service.ts +++ b/server/src/core/managers/storage/storage-cache.service.ts @@ -1,265 +1,264 @@ -import { - DownloadBeatmapSetOptions, - DownloadOsuBeatmap, - GetBeatmapOptions, - GetBeatmapSetOptions, - SearchBeatmapsetsOptions, -} from '../../abstracts/client/base-client.types'; -import { Beatmap, Beatmapset } from '../../../types/general/beatmap'; -import Redis from 'ioredis'; -import { RedisKeys } from '../../../types/redis'; -import { RankStatus } from '../../../types/general/rankStatus'; -import config from '../../../config'; -import { BeatmapOsuFile, BeatmapsetFile } from '../../../database/schema'; -import { RedisInstance } from '../../../plugins/redisInstance'; +import type Redis from "ioredis"; + +import config from "../../../config"; +import type { BeatmapOsuFile, BeatmapsetFile } from "../../../database/schema"; +import { RedisInstance } from "../../../plugins/redisInstance"; +import type { Beatmap, Beatmapset } from "../../../types/general/beatmap"; +import { RankStatus } from "../../../types/general/rankStatus"; +import { RedisKeys } from "../../../types/redis"; +import type { + DownloadBeatmapSetOptions, + DownloadOsuBeatmap, + GetBeatmapOptions, + GetBeatmapSetOptions, + SearchBeatmapsetsOptions, +} from "../../abstracts/client/base-client.types"; const ONE_DAY = 1000 * 60 * 60 * 24; const ONE_SECOND = 1000; export class StorageCacheService { - private readonly redis: Redis = RedisInstance; + private readonly redis: Redis = RedisInstance; - async getBeatmap( - ctx: GetBeatmapOptions, - ): Promise { - let beatmapId = ctx.beatmapId; + async getBeatmap( + ctx: GetBeatmapOptions, + ): Promise { + let { beatmapId } = ctx; - if (ctx.beatmapHash) { - const cachedId = await this.redis.get( + if (ctx.beatmapHash) { + const cachedId = await this.redis.get( `${RedisKeys.BEATMAP_ID_BY_HASH}${ctx.beatmapHash}`, - ); + ); - if (!cachedId) return undefined; - if (cachedId === 'null') return null; + if (!cachedId) + return undefined; + if (cachedId === "null") + return null; - beatmapId = Number(cachedId); - } - - const cache = await this.redis.get( - `${RedisKeys.BEATMAP_BY_ID}${beatmapId}`, - ); - - return cache ? JSON.parse(cache) : undefined; - } - - async insertEmptyBeatmap(ctx: GetBeatmapOptions) { - const key = ctx?.beatmapId - ? `${RedisKeys.BEATMAP_BY_ID}${ctx?.beatmapId}` - : `${RedisKeys.BEATMAP_ID_BY_HASH}${ctx?.beatmapHash}`; - await this.redis.set( - key, - 'null', - 'PX', - this.getRedisTTLBasedOnStatus(), - ); - return; + beatmapId = Number(cachedId); } - async insertBeatmap(beatmap: Beatmap) { - await this.redis.set( + const cache = await this.redis.get( + `${RedisKeys.BEATMAP_BY_ID}${beatmapId}`, + ); + + return cache ? JSON.parse(cache) : undefined; + } + + async insertEmptyBeatmap(ctx: GetBeatmapOptions) { + const key = ctx?.beatmapId + ? `${RedisKeys.BEATMAP_BY_ID}${ctx?.beatmapId}` + : `${RedisKeys.BEATMAP_ID_BY_HASH}${ctx?.beatmapHash}`; + await this.redis.set( + key, + "null", + "PX", + this.getRedisTTLBasedOnStatus(), + ); + } + + async insertBeatmap(beatmap: Beatmap) { + await this.redis.set( `${RedisKeys.BEATMAP_BY_ID}${beatmap.id}`, JSON.stringify(beatmap), - 'PX', + "PX", this.getRedisTTLBasedOnStatus(beatmap.status), - ); + ); - await this.redis.set( + await this.redis.set( `${RedisKeys.BEATMAP_ID_BY_HASH}${beatmap.checksum}`, beatmap.id, - 'PX', + "PX", this.getRedisTTLBasedOnStatus(beatmap.status), - ); - } - - async insertEmptyBeatmapset(ctx: GetBeatmapSetOptions) { - const key = `${RedisKeys.BEATMAPSET_BY_ID}${ctx?.beatmapSetId}`; - await this.redis.set( - key, - 'null', - 'PX', - this.getRedisTTLBasedOnStatus(), - ); - return; - } - - async insertBeatmapset(beatmapset: Beatmapset) { - await this.redis.set( + ); + } + + async insertEmptyBeatmapset(ctx: GetBeatmapSetOptions) { + const key = `${RedisKeys.BEATMAPSET_BY_ID}${ctx?.beatmapSetId}`; + await this.redis.set( + key, + "null", + "PX", + this.getRedisTTLBasedOnStatus(), + ); + } + + async insertBeatmapset(beatmapset: Beatmapset) { + await this.redis.set( `${RedisKeys.BEATMAPSET_BY_ID}${beatmapset.id}`, JSON.stringify(beatmapset), - 'PX', + "PX", this.getRedisTTLBasedOnStatus(beatmapset.status), - ); + ); - beatmapset.beatmaps?.forEach((b) => this.insertBeatmap(b)); - } + beatmapset.beatmaps?.forEach(b => this.insertBeatmap(b)); + } - async getBeatmapSet( - ctx: GetBeatmapSetOptions, - ): Promise { - let beatmapsetId = ctx.beatmapSetId; + async getBeatmapSet( + ctx: GetBeatmapSetOptions, + ): Promise { + const beatmapsetId = ctx.beatmapSetId; - const cache = await this.redis.get( + const cache = await this.redis.get( `${RedisKeys.BEATMAPSET_BY_ID}${beatmapsetId}`, - ); + ); - return cache ? JSON.parse(cache) : undefined; - } + return cache ? JSON.parse(cache) : undefined; + } - async insertSearchResult( - ctx: SearchBeatmapsetsOptions, - result: Beatmapset[], - ) { - const requestRawKey = Object.values(ctx) - .reduce((p, v, i) => `${p}indx-${i}:${(v ?? '').toString()}:`, '') - .toString(); - const requestKey = Bun.hash(requestRawKey).toString(); + async insertSearchResult( + ctx: SearchBeatmapsetsOptions, + result: Beatmapset[], + ) { + const requestRawKey = Object.values(ctx) + .reduce((p, v, i) => `${p}indx-${i}:${(v ?? "").toString()}:`, "") + .toString(); + const requestKey = Bun.hash(requestRawKey).toString(); - await this.redis.set( + await this.redis.set( `${RedisKeys.BEATMAPS_SEARCH_RESULT}${requestKey}`, JSON.stringify(result), - 'PX', + "PX", ONE_SECOND * 15, - ); - } - - async getSearchResult( - ctx: SearchBeatmapsetsOptions, - ): Promise { - const requestRawKey = Object.values(ctx) - .reduce((p, v, i) => `${p}indx-${i}:${(v ?? '').toString()}:`, '') - .toString(); - const requestKey = Bun.hash(requestRawKey).toString(); - - const cache = await this.redis.get( + ); + } + + async getSearchResult( + ctx: SearchBeatmapsetsOptions, + ): Promise { + const requestRawKey = Object.values(ctx) + .reduce((p, v, i) => `${p}indx-${i}:${(v ?? "").toString()}:`, "") + .toString(); + const requestKey = Bun.hash(requestRawKey).toString(); + + const cache = await this.redis.get( `${RedisKeys.BEATMAPS_SEARCH_RESULT}${requestKey}`, - ); - - return cache ? JSON.parse(cache) : undefined; - } - - async insertEmptyBeatmapsetFile(ctx: DownloadBeatmapSetOptions) { - const key = `${RedisKeys.BEATMAPSET_FILE_BY_ID}${ctx.beatmapSetId}`; - await this.redis.set( - key, - 'null', - 'PX', - this.getRedisTTLBasedOnStatus(), - ); - return; - } - - async insertBeatmapsetFile( - ctx: DownloadBeatmapSetOptions, - beatmapsetFile: BeatmapsetFile, - ) { - await this.redis.set( + ); + + return cache ? JSON.parse(cache) : undefined; + } + + async insertEmptyBeatmapsetFile(ctx: DownloadBeatmapSetOptions) { + const key = `${RedisKeys.BEATMAPSET_FILE_BY_ID}${ctx.beatmapSetId}`; + await this.redis.set( + key, + "null", + "PX", + this.getRedisTTLBasedOnStatus(), + ); + } + + async insertBeatmapsetFile( + ctx: DownloadBeatmapSetOptions, + beatmapsetFile: BeatmapsetFile, + ) { + await this.redis.set( `${RedisKeys.BEATMAPSET_FILE_BY_ID}${ctx.beatmapSetId}`, JSON.stringify(beatmapsetFile), - 'PX', + "PX", (ONE_DAY / 24) * config.OSZ_FILES_LIFE_SPAN, - ); - } + ); + } - async getBeatmapSetFile( - ctx: DownloadBeatmapSetOptions, - ): Promise { - let beatmapsetId = ctx.beatmapSetId; + async getBeatmapSetFile( + ctx: DownloadBeatmapSetOptions, + ): Promise { + const beatmapsetId = ctx.beatmapSetId; - const cache = await this.redis.get( + const cache = await this.redis.get( `${RedisKeys.BEATMAPSET_FILE_BY_ID}${beatmapsetId}`, - ); - - return cache ? JSON.parse(cache) : undefined; - } - - async insertEmptyBeatmapOsuFile(ctx: DownloadOsuBeatmap) { - const key = `${RedisKeys.BEATMAP_OSU_FILE}${ctx.beatmapId}`; - await this.redis.set( - key, - 'null', - 'PX', - this.getRedisTTLBasedOnStatus(), - ); - return; - } - - async insertBeatmapOsuFile( - ctx: DownloadOsuBeatmap, - beatmapOsuFile: BeatmapOsuFile, - ) { - await this.redis.set( + ); + + return cache ? JSON.parse(cache) : undefined; + } + + async insertEmptyBeatmapOsuFile(ctx: DownloadOsuBeatmap) { + const key = `${RedisKeys.BEATMAP_OSU_FILE}${ctx.beatmapId}`; + await this.redis.set( + key, + "null", + "PX", + this.getRedisTTLBasedOnStatus(), + ); + } + + async insertBeatmapOsuFile( + ctx: DownloadOsuBeatmap, + beatmapOsuFile: BeatmapOsuFile, + ) { + await this.redis.set( `${RedisKeys.BEATMAP_OSU_FILE}${ctx.beatmapId}`, JSON.stringify(beatmapOsuFile), - 'PX', + "PX", (ONE_DAY / 24) * config.OSZ_FILES_LIFE_SPAN, // Let .osu files live for the same time as .osz files - ); - } + ); + } - async getBeatmapOsuFile( - ctx: DownloadOsuBeatmap, - ): Promise { - const cache = await this.redis.get( + async getBeatmapOsuFile( + ctx: DownloadOsuBeatmap, + ): Promise { + const cache = await this.redis.get( `${RedisKeys.BEATMAP_OSU_FILE}${ctx.beatmapId}`, - ); - - return cache ? JSON.parse(cache) : undefined; - } - - async getRedisStats() { - const beatmapsByIdKeys = await this.redis.keys( - RedisKeys.BEATMAP_BY_ID + '*', - ); - const beatmapsIdByHashKeys = await this.redis.keys( - RedisKeys.BEATMAP_ID_BY_HASH + '*', - ); - - const beatmapsetsByIdKeys = await this.redis.keys( - RedisKeys.BEATMAPSET_BY_ID + '*', - ); - - const beatmapsetFilesByIdKeys = await this.redis.keys( - RedisKeys.BEATMAPSET_FILE_BY_ID + '*', - ); - - const beatmapOsuFilesByIdKeys = await this.redis.keys( - RedisKeys.BEATMAP_OSU_FILE + '*', - ); - - return { - beatmaps: { - byId: beatmapsByIdKeys.length, - ids: { - byHash: beatmapsIdByHashKeys.length, - }, - }, - beatmapsets: { - byId: beatmapsetsByIdKeys.length, - }, - beatmapsetFiles: { - byId: beatmapsetFilesByIdKeys.length, - }, - beatmapOsuFiles: { - byId: beatmapOsuFilesByIdKeys.length, - }, - }; - } - - private getRedisTTLBasedOnStatus(status?: RankStatus): number { - const ONE_MINUTE = 1000 * 60; - - switch (status) { - case RankStatus.GRAVEYARD: - return ONE_MINUTE * 30; - case RankStatus.PENDING: - case RankStatus.QUALIFIED: - case RankStatus.WIP: - return ONE_MINUTE * 5; - case RankStatus.APPROVED: - case RankStatus.LOVED: - case RankStatus.RANKED: - return ONE_MINUTE * 15; - default: - return ONE_MINUTE * 5; - } + ); + + return cache ? JSON.parse(cache) : undefined; + } + + async getRedisStats() { + const beatmapsByIdKeys = await this.redis.keys( + `${RedisKeys.BEATMAP_BY_ID}*`, + ); + const beatmapsIdByHashKeys = await this.redis.keys( + `${RedisKeys.BEATMAP_ID_BY_HASH}*`, + ); + + const beatmapsetsByIdKeys = await this.redis.keys( + `${RedisKeys.BEATMAPSET_BY_ID}*`, + ); + + const beatmapsetFilesByIdKeys = await this.redis.keys( + `${RedisKeys.BEATMAPSET_FILE_BY_ID}*`, + ); + + const beatmapOsuFilesByIdKeys = await this.redis.keys( + `${RedisKeys.BEATMAP_OSU_FILE}*`, + ); + + return { + beatmaps: { + byId: beatmapsByIdKeys.length, + ids: { + byHash: beatmapsIdByHashKeys.length, + }, + }, + beatmapsets: { + byId: beatmapsetsByIdKeys.length, + }, + beatmapsetFiles: { + byId: beatmapsetFilesByIdKeys.length, + }, + beatmapOsuFiles: { + byId: beatmapOsuFilesByIdKeys.length, + }, + }; + } + + private getRedisTTLBasedOnStatus(status?: RankStatus): number { + const ONE_MINUTE = 1000 * 60; + + switch (status) { + case RankStatus.GRAVEYARD: + return ONE_MINUTE * 30; + case RankStatus.PENDING: + case RankStatus.QUALIFIED: + case RankStatus.WIP: + return ONE_MINUTE * 5; + case RankStatus.APPROVED: + case RankStatus.LOVED: + case RankStatus.RANKED: + return ONE_MINUTE * 15; + default: + return ONE_MINUTE * 5; } + } } diff --git a/server/src/core/managers/storage/storage-files.service.ts b/server/src/core/managers/storage/storage-files.service.ts index 7470e08..0e6a3f4 100644 --- a/server/src/core/managers/storage/storage-files.service.ts +++ b/server/src/core/managers/storage/storage-files.service.ts @@ -1,253 +1,257 @@ +import { unlink } from "node:fs/promises"; + +import AdmZip from "adm-zip"; + +import config from "../../../config"; import { - createBeatmapsetFile, - deleteBeatmapsetsFiles, - getBeatmapSetFile, - getUnvalidBeatmapSetsFiles, -} from '../../../database/models/beatmapsetFile'; -import { getUTCDate } from '../../../utils/date'; -import { - DownloadBeatmapSetOptions, - DownloadOsuBeatmap, -} from '../../abstracts/client/base-client.types'; -import { StorageCacheService } from './storage-cache.service'; -import { unlink } from 'node:fs/promises'; -import AdmZip from 'adm-zip'; -import logger from '../../../utils/logger'; -import config from '../../../config'; + createBeatmapOsuFile, + deleteBeatmapsOsuFiles, + getBeatmapOsuFile, + getUnvalidBeatmapOsuFiles, +} from "../../../database/models/beatmapOsuFile"; import { - createBeatmapOsuFile, - deleteBeatmapsOsuFiles, - getBeatmapOsuFile, - getUnvalidBeatmapOsuFiles, -} from '../../../database/models/beatmapOsuFile'; + createBeatmapsetFile, + deleteBeatmapsetsFiles, + getBeatmapSetFile, + getUnvalidBeatmapSetsFiles, +} from "../../../database/models/beatmapsetFile"; +import { getUTCDate } from "../../../utils/date"; +import logger from "../../../utils/logger"; import { - bytesToHumanReadableMegabytes, - getDirectoryStats, -} from '../../../utils/stats'; + bytesToHumanReadableMegabytes, + getDirectoryStats, +} from "../../../utils/stats"; +import type { + DownloadBeatmapSetOptions, + DownloadOsuBeatmap, +} from "../../abstracts/client/base-client.types"; +import type { StorageCacheService } from "./storage-cache.service"; export class StorageFilesService { - private readonly dataPath = 'data'; - private readonly videoFormats = ['avi', 'flv', 'mp4', 'm4v']; - - private readonly cacheService: StorageCacheService; + private readonly dataPath = "data"; + private readonly videoFormats = ["avi", "flv", "mp4", "m4v"]; - constructor(cacheService: StorageCacheService) { - this.cacheService = cacheService; + private readonly cacheService: StorageCacheService; - setInterval( - () => { - this.clearOldFiles(); - }, - 1000 * 60 * 30, - ); // 30 minutes + constructor(cacheService: StorageCacheService) { + this.cacheService = cacheService; + setInterval( + () => { this.clearOldFiles(); - - this.log('Initialized'); + }, + 1000 * 60 * 30, + ); // 30 minutes + + this.clearOldFiles(); + + this.log("Initialized"); + } + + async insertBeatmapsetFile( + file: ArrayBuffer | null, + ctx: DownloadBeatmapSetOptions, + ) { + if (!file) { + await this.cacheService.insertEmptyBeatmapsetFile(ctx); + return; } - async insertBeatmapsetFile( - file: ArrayBuffer | null, - ctx: DownloadBeatmapSetOptions, - ) { - if (!file) { - await this.cacheService.insertEmptyBeatmapsetFile(ctx); - return; - } - - const path = await this.saveFile( - file, - `${ctx.beatmapSetId}${ctx.noVideo ? 'n' : ''}.osz`, - ); - - if (ctx.noVideo !== true) { - this.removeIfExists(`${this.dataPath}/${ctx.beatmapSetId}n.osz`); - } - - const beatmapsetFile = await createBeatmapsetFile({ - id: ctx.beatmapSetId, - noVideo: ctx.noVideo || false, - path, - validUntil: new Date( - getUTCDate().getTime() + - 1000 * 60 * 60 * config.OSZ_FILES_LIFE_SPAN, - ).toISOString(), - }); - - await this.cacheService.insertBeatmapsetFile(ctx, beatmapsetFile); - } - - async insertBeatmapOsuFile( - file: ArrayBuffer | null, - ctx: DownloadOsuBeatmap, - ) { - if (!file) { - await this.cacheService.insertEmptyBeatmapOsuFile(ctx); - return; - } - - const path = await this.saveFile(file, `${ctx.beatmapId}.osu`); - - const beatmapOsuFile = await createBeatmapOsuFile({ - id: ctx.beatmapId, - path, - validUntil: new Date( - getUTCDate().getTime() + - 1000 * 60 * 60 * config.OSZ_FILES_LIFE_SPAN, // Same as .osz files - ).toISOString(), - }); + const path = await this.saveFile( + file, + `${ctx.beatmapSetId}${ctx.noVideo ? "n" : ""}.osz`, + ); - await this.cacheService.insertBeatmapOsuFile(ctx, beatmapOsuFile); + if (ctx.noVideo !== true) { + this.removeIfExists(`${this.dataPath}/${ctx.beatmapSetId}n.osz`); } - async getOsuBeatmapFile( - ctx: DownloadOsuBeatmap, - ): Promise { - let data = await this.cacheService.getBeatmapOsuFile(ctx); - - if (data === undefined) { - data = await getBeatmapOsuFile(ctx); - } else if (data === null) { - return null; - } - - if (!data) { - return undefined; - } + const beatmapsetFile = await createBeatmapsetFile({ + id: ctx.beatmapSetId, + noVideo: ctx.noVideo || false, + path, + validUntil: new Date( + getUTCDate().getTime() + + 1000 * 60 * 60 * config.OSZ_FILES_LIFE_SPAN, + ).toISOString(), + }); + + await this.cacheService.insertBeatmapsetFile(ctx, beatmapsetFile); + } + + async insertBeatmapOsuFile( + file: ArrayBuffer | null, + ctx: DownloadOsuBeatmap, + ) { + if (!file) { + await this.cacheService.insertEmptyBeatmapOsuFile(ctx); + return; + } - await this.cacheService.insertBeatmapOsuFile(ctx, data); + const path = await this.saveFile(file, `${ctx.beatmapId}.osu`); - const { path } = data; + const beatmapOsuFile = await createBeatmapOsuFile({ + id: ctx.beatmapId, + path, + validUntil: new Date( + getUTCDate().getTime() + + 1000 * 60 * 60 * config.OSZ_FILES_LIFE_SPAN, // Same as .osz files + ).toISOString(), + }); - const isFileExists = await this.isFileExists(path); + await this.cacheService.insertBeatmapOsuFile(ctx, beatmapOsuFile); + } - if (!isFileExists) { - return undefined; - } + async getOsuBeatmapFile( + ctx: DownloadOsuBeatmap, + ): Promise { + let data = await this.cacheService.getBeatmapOsuFile(ctx); - return this.readFile(path); + if (data === undefined) { + data = await getBeatmapOsuFile(ctx); + } + else if (data === null) { + return null; } - async getBeatmapsetFile( - ctx: DownloadBeatmapSetOptions, - ): Promise { - let data = await this.cacheService.getBeatmapSetFile(ctx); + if (!data) { + return undefined; + } - if (data === undefined) { - data = await getBeatmapSetFile(ctx); - } else if (data === null) { - return null; - } + await this.cacheService.insertBeatmapOsuFile(ctx, data); - // Return if we we don't have the file or the file is marked as no video, - // which we can't serve to request with noVideo set to false (or undefined) - if (!data || (ctx.noVideo !== true && data.noVideo)) { - return undefined; - } + const { path } = data; - await this.cacheService.insertBeatmapsetFile(ctx, data); + const isFileExists = await this.isFileExists(path); - const { path } = data; + if (!isFileExists) { + return undefined; + } - const isFileExists = await this.isFileExists(path); + return this.readFile(path); + } - if (!isFileExists) { - return undefined; - } + async getBeatmapsetFile( + ctx: DownloadBeatmapSetOptions, + ): Promise { + let data = await this.cacheService.getBeatmapSetFile(ctx); - return this.getZippedFile(path, { noVideo: ctx.noVideo || false }); + if (data === undefined) { + data = await getBeatmapSetFile(ctx); } - - public async getStorageFilesStats() { - const directoryStats = await getDirectoryStats(this.dataPath); - - return { - totalFiles: directoryStats.fileCount, - totalBytes: bytesToHumanReadableMegabytes(directoryStats.totalSize), - }; + else if (data === null) { + return null; } - private async getZippedFile(path: string, ctx: { noVideo: boolean }) { - if ( - (path.endsWith('n.osz') && ctx.noVideo) || - (!path.endsWith('n.osz') && !ctx.noVideo) - ) { - return await this.readFile(path); - } + // Return if we we don't have the file or the file is marked as no video, + // which we can't serve to request with noVideo set to false (or undefined) + if (!data || (ctx.noVideo !== true && data.noVideo)) { + return undefined; + } - const shadowCopyPath = `${path}.${crypto.randomUUID()}.temp`; + await this.cacheService.insertBeatmapsetFile(ctx, data); - const osz = new AdmZip(path); - const files = osz.getEntries(); + const { path } = data; - for (const file of files) { - const isVideo = this.videoFormats.some((format) => - file.entryName.endsWith(format), - ); + const isFileExists = await this.isFileExists(path); - if (isVideo) { - osz.deleteFile(file); - } - } + if (!isFileExists) { + return undefined; + } - osz.writeZip(shadowCopyPath); + return this.getZippedFile(path, { noVideo: ctx.noVideo || false }); + } - const file = await this.readFile(shadowCopyPath); + public async getStorageFilesStats() { + const directoryStats = await getDirectoryStats(this.dataPath); - this.removeIfExists(shadowCopyPath); + return { + totalFiles: directoryStats.fileCount, + totalBytes: bytesToHumanReadableMegabytes(directoryStats.totalSize), + }; + } - return file; + private async getZippedFile(path: string, ctx: { noVideo: boolean }) { + if ( + (path.endsWith("n.osz") && ctx.noVideo) + || (!path.endsWith("n.osz") && !ctx.noVideo) + ) { + return await this.readFile(path); } - private async readFile(path: string): Promise { - return await Bun.file(path).arrayBuffer(); - } + const shadowCopyPath = `${path}.${crypto.randomUUID()}.temp`; - private async isFileExists(path: string) { - return await Bun.file(path).exists(); - } + const osz = new AdmZip(path); + const files = osz.getEntries(); - private async removeIfExists(path: string) { - const isFileExists = await this.isFileExists(path); + for (const file of files) { + const isVideo = this.videoFormats.some(format => + file.entryName.endsWith(format), + ); - if (isFileExists) { - await unlink(path); - } + if (isVideo) { + osz.deleteFile(file); + } } - private async saveFile( - file: ArrayBuffer, - fileName: string, - ): Promise { - const path = `${this.dataPath}/${fileName}`; - await Bun.write(path, file); - return path; - } + osz.writeZip(shadowCopyPath); - private async clearOldFiles() { - const beatmapsetsForRemoval = await getUnvalidBeatmapSetsFiles(); - const osuBeatmapsForRemoval = await getUnvalidBeatmapOsuFiles(); + const file = await this.readFile(shadowCopyPath); - const forRemoval = [...beatmapsetsForRemoval, ...osuBeatmapsForRemoval]; + this.removeIfExists(shadowCopyPath); - if (!forRemoval) { - this.log('Nothing to remove. Skip cleaning.'); - return; - } + return file; + } - for (const beatmapset of forRemoval) { - this.log(`Removing "${beatmapset.path}" ...`, 'warn'); + private async readFile(path: string): Promise { + return await Bun.file(path).arrayBuffer(); + } - await this.removeIfExists(beatmapset.path); - } + private async isFileExists(path: string) { + return await Bun.file(path).exists(); + } - await deleteBeatmapsetsFiles(beatmapsetsForRemoval); - await deleteBeatmapsOsuFiles(osuBeatmapsForRemoval); + private async removeIfExists(path: string) { + const isFileExists = await this.isFileExists(path); - this.log('Cleaning is finished!'); + if (isFileExists) { + await unlink(path); } + } + + private async saveFile( + file: ArrayBuffer, + fileName: string, + ): Promise { + const path = `${this.dataPath}/${fileName}`; + await Bun.write(path, file); + return path; + } + + private async clearOldFiles() { + const beatmapsetsForRemoval = await getUnvalidBeatmapSetsFiles(); + const osuBeatmapsForRemoval = await getUnvalidBeatmapOsuFiles(); + + const forRemoval = [...beatmapsetsForRemoval, ...osuBeatmapsForRemoval]; + + if (!forRemoval) { + this.log("Nothing to remove. Skip cleaning."); + return; + } + + for (const beatmapset of forRemoval) { + this.log(`Removing "${beatmapset.path}" ...`, "warn"); - private log(message: string, level: 'info' | 'warn' | 'error' = 'info') { - logger[level](`StorageFilesSerivce: ${message}`); + await this.removeIfExists(beatmapset.path); } + + await deleteBeatmapsetsFiles(beatmapsetsForRemoval); + await deleteBeatmapsOsuFiles(osuBeatmapsForRemoval); + + this.log("Cleaning is finished!"); + } + + private log(message: string, level: "info" | "warn" | "error" = "info") { + logger[level](`StorageFilesSerivce: ${message}`); + } } diff --git a/server/src/core/managers/storage/storage.manager.ts b/server/src/core/managers/storage/storage.manager.ts index 3804fe2..6db0f82 100644 --- a/server/src/core/managers/storage/storage.manager.ts +++ b/server/src/core/managers/storage/storage.manager.ts @@ -1,218 +1,221 @@ +import config from "../../../config"; import { - DownloadBeatmapSetOptions, - DownloadOsuBeatmap, - GetBeatmapOptions, - GetBeatmapSetOptions, - GetBeatmapsetsByBeatmapIdsOptions, - SearchBeatmapsetsOptions, -} from '../../abstracts/client/base-client.types'; -import { Beatmap, Beatmapset } from '../../../types/general/beatmap'; + createBeatmap, + getBeatmapByHash, + getBeatmapById, + getBeatmapCount, +} from "../../../database/models/beatmap"; +import { getBeatmapOsuFileCount } from "../../../database/models/beatmapOsuFile"; import { - createBeatmap, - getBeatmapByHash, - getBeatmapById, - getBeatmapCount, -} from '../../../database/models/beatmap'; -import { - createBeatmapset, - deleteBeatmapsets, - getBeatmapSetById, - getBeatmapSetCount, - getBeatmapSetsByBeatmapIds, - getUnvalidBeatmapSets, -} from '../../../database/models/beatmapset'; -import { StorageCacheService } from './storage-cache.service'; -import { StorageFilesService } from './storage-files.service'; -import { getBeatmapSetsFilesCount } from '../../../database/models/beatmapsetFile'; -import { getBeatmapOsuFileCount } from '../../../database/models/beatmapOsuFile'; -import logger from '../../../utils/logger'; -import config from '../../../config'; + createBeatmapset, + deleteBeatmapsets, + getBeatmapSetById, + getBeatmapSetCount, + getBeatmapSetsByBeatmapIds, + getUnvalidBeatmapSets, +} from "../../../database/models/beatmapset"; +import { getBeatmapSetsFilesCount } from "../../../database/models/beatmapsetFile"; +import type { Beatmap, Beatmapset } from "../../../types/general/beatmap"; +import logger from "../../../utils/logger"; +import type { + DownloadBeatmapSetOptions, + DownloadOsuBeatmap, + GetBeatmapOptions, + GetBeatmapSetOptions, + GetBeatmapsetsByBeatmapIdsOptions, + SearchBeatmapsetsOptions, +} from "../../abstracts/client/base-client.types"; +import { StorageCacheService } from "./storage-cache.service"; +import { StorageFilesService } from "./storage-files.service"; export class StorageManager { - private readonly cacheService: StorageCacheService; - private readonly filesService: StorageFilesService; - - constructor() { - this.cacheService = new StorageCacheService(); - this.filesService = new StorageFilesService(this.cacheService); + private readonly cacheService: StorageCacheService; + private readonly filesService: StorageFilesService; - setInterval( - () => { - this.clearOldBeatmapsets(); - }, - 1000 * 60 * 30, - ); // 30 minutes + constructor() { + this.cacheService = new StorageCacheService(); + this.filesService = new StorageFilesService(this.cacheService); + setInterval( + () => { this.clearOldBeatmapsets(); - } + }, + 1000 * 60 * 30, + ); // 30 minutes - async getBeatmap( - ctx: GetBeatmapOptions, - ): Promise { - let entity = await this.cacheService.getBeatmap(ctx); + this.clearOldBeatmapsets(); + } - if (entity !== undefined) { - return entity; - } + async getBeatmap( + ctx: GetBeatmapOptions, + ): Promise { + let entity = await this.cacheService.getBeatmap(ctx); - if (ctx.beatmapId) { - entity = await getBeatmapById(ctx.beatmapId); - } else if (ctx.beatmapHash) { - entity = await getBeatmapByHash(ctx.beatmapHash); - } - - if (entity) { - this.cacheService.insertBeatmap(entity); - } - - return entity ?? undefined; + if (entity !== undefined) { + return entity; } - async getBeatmapSet( - ctx: GetBeatmapSetOptions, - ): Promise { - let entity = await this.cacheService.getBeatmapSet(ctx); - - if (entity !== undefined) { - return entity; - } - - if (ctx.beatmapSetId) { - entity = await getBeatmapSetById(ctx.beatmapSetId); - } - - if (entity) { - this.cacheService.insertBeatmapset(entity); - } + if (ctx.beatmapId) { + entity = await getBeatmapById(ctx.beatmapId); + } + else if (ctx.beatmapHash) { + entity = await getBeatmapByHash(ctx.beatmapHash); + } - return entity ?? undefined; + if (entity) { + this.cacheService.insertBeatmap(entity); } - async getBeatmapSetsByBeatmapIds( - ctx: GetBeatmapsetsByBeatmapIdsOptions, - ): Promise { - const entities = await getBeatmapSetsByBeatmapIds(ctx.beatmapIds, true); + return entity ?? undefined; + } - if (entities === null) { - return null; - } + async getBeatmapSet( + ctx: GetBeatmapSetOptions, + ): Promise { + let entity = await this.cacheService.getBeatmapSet(ctx); - return entities ?? undefined; + if (entity !== undefined) { + return entity; } - async getBeatmapsetFile( - ctx: DownloadBeatmapSetOptions, - ): Promise { - let entity = await this.filesService.getBeatmapsetFile(ctx); + if (ctx.beatmapSetId) { + entity = await getBeatmapSetById(ctx.beatmapSetId); + } - return entity; + if (entity) { + this.cacheService.insertBeatmapset(entity); } - async getSearchResult( - ctx: SearchBeatmapsetsOptions, - ): Promise { - let entity = await this.cacheService.getSearchResult(ctx); + return entity ?? undefined; + } - return entity; + async getBeatmapSetsByBeatmapIds( + ctx: GetBeatmapsetsByBeatmapIdsOptions, + ): Promise { + const entities = await getBeatmapSetsByBeatmapIds(ctx.beatmapIds, true); + + if (entities === null) { + return null; } - async insertSearchResult( - ctx: SearchBeatmapsetsOptions, - result: Beatmapset[], - ): Promise { - for (const beatmapset of result) { - await this.insertBeatmapset(beatmapset, { - beatmapSetId: beatmapset.id, - }); - } - - await this.cacheService.insertSearchResult(ctx, result); + return entities ?? undefined; + } + + async getBeatmapsetFile( + ctx: DownloadBeatmapSetOptions, + ): Promise { + const entity = await this.filesService.getBeatmapsetFile(ctx); + + return entity; + } + + async getSearchResult( + ctx: SearchBeatmapsetsOptions, + ): Promise { + const entity = await this.cacheService.getSearchResult(ctx); + + return entity; + } + + async insertSearchResult( + ctx: SearchBeatmapsetsOptions, + result: Beatmapset[], + ): Promise { + for (const beatmapset of result) { + await this.insertBeatmapset(beatmapset, { + beatmapSetId: beatmapset.id, + }); } - async getOsuBeatmapFile( - ctx: DownloadOsuBeatmap, - ): Promise { - let entity = await this.filesService.getOsuBeatmapFile(ctx); + await this.cacheService.insertSearchResult(ctx, result); + } - return entity; - } + async getOsuBeatmapFile( + ctx: DownloadOsuBeatmap, + ): Promise { + const entity = await this.filesService.getOsuBeatmapFile(ctx); - async insertBeatmap( - beatmap: Beatmap | null, - ctx: GetBeatmapOptions, - ): Promise { - if (beatmap) { - await createBeatmap(beatmap); - await this.cacheService.insertBeatmap(beatmap); - } else { - await this.cacheService.insertEmptyBeatmap(ctx); - } - } + return entity; + } - async insertBeatmapset( - beatmapset: Beatmapset | null, - ctx: GetBeatmapSetOptions, - ): Promise { - if (beatmapset) { - await createBeatmapset(beatmapset); - await this.cacheService.insertBeatmapset(beatmapset); - } else { - await this.cacheService.insertEmptyBeatmapset(ctx); - } + async insertBeatmap( + beatmap: Beatmap | null, + ctx: GetBeatmapOptions, + ): Promise { + if (beatmap) { + await createBeatmap(beatmap); + await this.cacheService.insertBeatmap(beatmap); } - - async insertBeatmapsetFile( - file: ArrayBuffer | null, - ctx: DownloadBeatmapSetOptions, - ): Promise { - await this.filesService.insertBeatmapsetFile(file, ctx); + else { + await this.cacheService.insertEmptyBeatmap(ctx); } - - async insertBeatmapOsuFile( - file: ArrayBuffer | null, - ctx: DownloadOsuBeatmap, - ): Promise { - await this.filesService.insertBeatmapOsuFile(file, ctx); + } + + async insertBeatmapset( + beatmapset: Beatmapset | null, + ctx: GetBeatmapSetOptions, + ): Promise { + if (beatmapset) { + await createBeatmapset(beatmapset); + await this.cacheService.insertBeatmapset(beatmapset); } - - public async getStorageStatistics() { - return { - database: { - beatmaps: await getBeatmapCount(), - beatmapSets: await getBeatmapSetCount(), - beatmapSetFile: await getBeatmapSetsFilesCount(), - beatmapOsuFile: await getBeatmapOsuFileCount(), - }, - files: await this.filesService.getStorageFilesStats(), - cache: await this.cacheService.getRedisStats(), - }; + else { + await this.cacheService.insertEmptyBeatmapset(ctx); + } + } + + async insertBeatmapsetFile( + file: ArrayBuffer | null, + ctx: DownloadBeatmapSetOptions, + ): Promise { + await this.filesService.insertBeatmapsetFile(file, ctx); + } + + async insertBeatmapOsuFile( + file: ArrayBuffer | null, + ctx: DownloadOsuBeatmap, + ): Promise { + await this.filesService.insertBeatmapOsuFile(file, ctx); + } + + public async getStorageStatistics() { + return { + database: { + beatmaps: await getBeatmapCount(), + beatmapSets: await getBeatmapSetCount(), + beatmapSetFile: await getBeatmapSetsFilesCount(), + beatmapOsuFile: await getBeatmapOsuFileCount(), + }, + files: await this.filesService.getStorageFilesStats(), + cache: await this.cacheService.getRedisStats(), + }; + } + + private async clearOldBeatmapsets() { + if (!config.EnableCronToClearOutdatedBeatmaps) { + return; } - private async clearOldBeatmapsets() { - if (!config.EnableCronToClearOutdatedBeatmaps) { - return; - } - - const beatmapsetsForRemoval = await getUnvalidBeatmapSets(); + const beatmapsetsForRemoval = await getUnvalidBeatmapSets(); - const forRemoval = [...beatmapsetsForRemoval]; + const forRemoval = [...beatmapsetsForRemoval]; - if (!forRemoval) { - this.log('Nothing to remove. Skip cleaning unvalid beatmaps.'); - return; - } + if (!forRemoval) { + this.log("Nothing to remove. Skip cleaning unvalid beatmaps."); + return; + } - this.log( + this.log( `Going to remove ${beatmapsetsForRemoval.length} unvalid beatmapsets from database`, - 'warn', - ); + "warn", + ); - await deleteBeatmapsets(beatmapsetsForRemoval); + await deleteBeatmapsets(beatmapsetsForRemoval); - this.log('Cleaning unvalid beatmaps is finished!'); - } + this.log("Cleaning unvalid beatmaps is finished!"); + } - private log(message: string, level: 'info' | 'warn' | 'error' = 'info') { - logger[level](`StorageManager: ${message}`); - } + private log(message: string, level: "info" | "warn" | "error" = "info") { + logger[level](`StorageManager: ${message}`); + } } diff --git a/server/src/core/services/compare.service.ts b/server/src/core/services/compare.service.ts index 6918ae7..c28615c 100644 --- a/server/src/core/services/compare.service.ts +++ b/server/src/core/services/compare.service.ts @@ -1,126 +1,128 @@ -import { BenchmarkResult } from '../../types/benchmark'; - -import logger from '../../utils/logger'; +import type { BenchmarkResult } from "../../types/benchmark"; +import logger from "../../utils/logger"; +import type { + MirrorClient, +} from "../abstracts/client/base-client.types"; import { - ClientAbilities, - MirrorClient, -} from '../abstracts/client/base-client.types'; + ClientAbilities, +} from "../abstracts/client/base-client.types"; export class CompareService { - private readonly beatmapSetId: number = 1357624; - private readonly beatmapId: number = 1003401; - - async benchmarkMirror(mirror: MirrorClient): Promise { - const downloadBenchmark = await this.latencyBenchmark(mirror); + private readonly beatmapSetId: number = 1357624; + private readonly beatmapId: number = 1003401; + + async benchmarkMirror(mirror: MirrorClient): Promise { + const downloadBenchmark = await this.latencyBenchmark(mirror); + + return downloadBenchmark; + } + + abilitiesToBenchmark(mirror: MirrorClient) { + const apiAbilities = new Set([ClientAbilities.GetBeatmapSetById]); // Basic minimum average client should have + const downloadAbilities = new Set([ + ClientAbilities.DownloadBeatmapSetByIdNoVideo, + ClientAbilities.DownloadBeatmapSetById, + ClientAbilities.DownloadOsuBeatmap, + ]); + + return { + download: mirror.client.clientConfig.abilities.some(ability => + downloadAbilities.has(ability), + ), + api: mirror.client.clientConfig.abilities.some(ability => + apiAbilities.has(ability), + ), + }; + } + + private async latencyBenchmark( + mirror: MirrorClient, + ): Promise { + let start = performance.now(); + const { client } = mirror; + + const results = { + latency: undefined, + downloadSpeed: undefined, + } as BenchmarkResult; + + const toBenchmark = this.abilitiesToBenchmark(mirror); + + if (toBenchmark.api) { + const beatmapSet = await client.getBeatmapSet({ + beatmapSetId: this.beatmapSetId, + }); + + if (!beatmapSet.result) { + this.log( + `Failed to fetch beatmap set ${this.beatmapSetId} from ${client.clientConfig.baseUrl}`, + "error", + ); + } - return downloadBenchmark; + results.latency = Math.round(performance.now() - start); } - abilitiesToBenchmark(mirror: MirrorClient) { - const apiAbilities = [ClientAbilities.GetBeatmapSetById]; // Basic minimum average client should have - const downloadAbilities = [ - ClientAbilities.DownloadBeatmapSetByIdNoVideo, - ClientAbilities.DownloadBeatmapSetById, - ClientAbilities.DownloadOsuBeatmap, - ]; - - return { - download: mirror.client.clientConfig.abilities.some((ability) => - downloadAbilities.includes(ability), - ), - api: mirror.client.clientConfig.abilities.some((ability) => - apiAbilities.includes(ability), - ), - }; - } + if (toBenchmark.download) { + start = performance.now(); - private async latencyBenchmark( - mirror: MirrorClient, - ): Promise { - let start = performance.now(); - const client = mirror.client; + const downloadOsuBeatmap = client.clientConfig.abilities.includes( + ClientAbilities.DownloadOsuBeatmap, + ); - let results = { - latency: undefined, - downloadSpeed: undefined, - } as BenchmarkResult; + let downloadResult: { result: ArrayBuffer | null } = { + result: null, + }; - const toBenchmark = this.abilitiesToBenchmark(mirror); + if (downloadOsuBeatmap) { + downloadResult = await client.downloadOsuBeatmap({ + beatmapId: this.beatmapId, + }); - if (toBenchmark.api) { - const beatmapSet = await client.getBeatmapSet({ - beatmapSetId: this.beatmapSetId, - }); - - if (!beatmapSet.result) { - this.log( - `Failed to fetch beatmap set ${this.beatmapSetId} from ${client.clientConfig.baseUrl}`, - 'error', - ); - } + if (!downloadResult) { + this.log( + `Failed to download .osu beatmap from ${client.clientConfig.baseUrl}`, + "error", + ); - results.latency = Math.round(performance.now() - start); + return results; } + } + else { + const downloadWithoutVideo + = client.clientConfig.abilities.includes( + ClientAbilities.DownloadBeatmapSetByIdNoVideo, + ); - if (toBenchmark.download) { - start = performance.now(); - - const downloadOsuBeatmap = client.clientConfig.abilities.includes( - ClientAbilities.DownloadOsuBeatmap, - ); - - let downloadResult: { result: ArrayBuffer | null } = { - result: null, - }; - - if (downloadOsuBeatmap) { - downloadResult = await client.downloadOsuBeatmap({ - beatmapId: this.beatmapId, - }); + downloadResult = await client.downloadBeatmapSet({ + beatmapSetId: this.beatmapSetId, + noVideo: downloadWithoutVideo ? true : false, + }); - if (!downloadResult) { - this.log( - `Failed to download .osu beatmap from ${client.clientConfig.baseUrl}`, - 'error', - ); - - return results; - } - } else { - const downloadWithoutVideo = - client.clientConfig.abilities.includes( - ClientAbilities.DownloadBeatmapSetByIdNoVideo, - ); - - downloadResult = await client.downloadBeatmapSet({ - beatmapSetId: this.beatmapSetId, - noVideo: downloadWithoutVideo ? true : false, - }); - - if (!downloadResult) { - this.log( + if (!downloadResult) { + this.log( `Failed to download beatmap set ${this.beatmapSetId} from ${client.clientConfig.baseUrl}`, - 'error', - ); - - return results; - } - } + "error", + ); - const downloadResultSize = downloadResult.result?.byteLength || 0; - const downloadSpeed = Math.round( - downloadResultSize / - 1024 / - ((performance.now() - start) / 1000), - ); // KB/s - - results.downloadSpeed = downloadSpeed; + return results; } + } - return results; - } + const downloadResultSize = downloadResult.result?.byteLength || 0; + const downloadSpeed = Math.round( + downloadResultSize + / 1024 + / ((performance.now() - start) / 1000), + ); // KB/s - private log(message: string, level: 'info' | 'warn' | 'error' = 'info') { - logger[level](`CompareService: ${message}`); + results.downloadSpeed = downloadSpeed; } + + return results; + } + + private log(message: string, level: "info" | "warn" | "error" = "info") { + logger[level](`CompareService: ${message}`); + } } diff --git a/server/src/core/services/convert.service.ts b/server/src/core/services/convert.service.ts index 823c6e6..d358910 100644 --- a/server/src/core/services/convert.service.ts +++ b/server/src/core/services/convert.service.ts @@ -1,186 +1,186 @@ -import { Beatmap, Beatmapset } from '../../types/general/beatmap'; -import { - OsulabsBeatmap, - OsulabsBeatmapset, -} from '../domains/beatmaps.download/osulabs-client.types'; -import { - MinoBeatmap, - MinoBeatmapset, -} from '../domains/catboy.best/mino-client.types'; -import { DirectBeatmap } from '../domains/osu.direct/direct-client.types'; -import { - BanchoBeatmap, - BanchoBeatmapset, -} from '../domains/osu.ppy.sh/bancho-client.types'; +import type { Beatmap, Beatmapset } from "../../types/general/beatmap"; +import type { + OsulabsBeatmap, + OsulabsBeatmapset, +} from "../domains/beatmaps.download/osulabs-client.types"; +import type { + MinoBeatmap, + MinoBeatmapset, +} from "../domains/catboy.best/mino-client.types"; +import type { DirectBeatmap } from "../domains/osu.direct/direct-client.types"; +import type { + BanchoBeatmap, + BanchoBeatmapset, +} from "../domains/osu.ppy.sh/bancho-client.types"; export class ConvertService { - private mirror: - | 'mino' - | 'bancho' - | 'direct' - | 'gatari' - | 'nerinyan' - | 'osulabs'; - - constructor(mirror: string) { - switch (mirror) { - case 'https://osu.ppy.sh': - this.mirror = 'bancho'; - break; - case 'https://catboy.best': - this.mirror = 'mino'; - break; - case 'https://osu.direct/api': - this.mirror = 'direct'; - break; - case 'https://osu.gatari.pw': - this.mirror = 'gatari'; - break; - case 'https://api.nerinyan.moe': - this.mirror = 'nerinyan'; - break; - case 'https://beatmaps.download': - this.mirror = 'osulabs'; - break; - default: - throw new Error('ConvertService: Invalid mirror provided'); - } + private mirror: + | "mino" + | "bancho" + | "direct" + | "gatari" + | "nerinyan" + | "osulabs"; + + constructor(mirror: string) { + switch (mirror) { + case "https://osu.ppy.sh": + this.mirror = "bancho"; + break; + case "https://catboy.best": + this.mirror = "mino"; + break; + case "https://osu.direct/api": + this.mirror = "direct"; + break; + case "https://osu.gatari.pw": + this.mirror = "gatari"; + break; + case "https://api.nerinyan.moe": + this.mirror = "nerinyan"; + break; + case "https://beatmaps.download": + this.mirror = "osulabs"; + break; + default: + throw new Error("ConvertService: Invalid mirror provided"); } - - public convertBeatmapset(beatmapset: T): Beatmapset { - switch (this.mirror) { - case 'bancho': - return this.convertBacnhoBeatmapset( - beatmapset as BanchoBeatmapset, - ); - case 'mino': - return this.convertMinoBeatmapset(beatmapset as MinoBeatmapset); - case 'osulabs': - return this.convertOsulabsBeatmapset( - beatmapset as OsulabsBeatmapset, - ); - default: - throw new Error('ConvertService: Cannot convert beatmapset'); - } - } - - public convertBeatmap(beatmap: T): Beatmap { - switch (this.mirror) { - case 'bancho': - return this.convertBanchoBeatmap(beatmap as BanchoBeatmap); - case 'mino': - return this.convertMinoBeatmap(beatmap as MinoBeatmap); - case 'osulabs': - return this.convertOsulabsBeatmap(beatmap as OsulabsBeatmap); - case 'direct': - return this.convertDirectBeatmap(beatmap as DirectBeatmap); - default: - throw new Error('ConvertService: Cannot convert beatmap'); - } - } - - private convertBacnhoBeatmapset(beatmapset: BanchoBeatmapset): Beatmapset { - delete beatmapset.current_user_attributes; - delete beatmapset.recent_favourites; - delete beatmapset.discussions; - delete beatmapset.events; - delete beatmapset.related_tags; - - return { - ...beatmapset, - beatmaps: beatmapset.beatmaps?.map((beatmap) => - this.convertBanchoBeatmap(beatmap), - ), - converts: beatmapset.converts?.map((beatmap) => - this.convertBanchoBeatmap(beatmap), - ), - } as Beatmapset; + } + + public convertBeatmapset(beatmapset: T): Beatmapset { + switch (this.mirror) { + case "bancho": + return this.convertBacnhoBeatmapset( + beatmapset as BanchoBeatmapset, + ); + case "mino": + return this.convertMinoBeatmapset(beatmapset as MinoBeatmapset); + case "osulabs": + return this.convertOsulabsBeatmapset( + beatmapset as OsulabsBeatmapset, + ); + default: + throw new Error("ConvertService: Cannot convert beatmapset"); } - - private convertBanchoBeatmap(beatmap: BanchoBeatmap): Beatmap { - delete beatmap.beatmapset; - delete beatmap.owners; - - return { - ...beatmap, - } as Beatmap; - } - - private convertDirectBeatmap(beatmap: DirectBeatmap): Beatmap { - return { - ...beatmap, - failtimes: { - fail: Array(100).fill(0), - exit: Array(100).fill(0), - }, - }; - } - - private convertMinoBeatmap(beatmap: MinoBeatmap): Beatmap { - delete beatmap.set; - delete beatmap.last_checked; - delete beatmap.owners; - delete beatmap.current_user_tag_ids; - delete beatmap.top_tag_ids; - - return { - ...beatmap, - last_updated: new Date(beatmap.last_updated).toISOString(), - } as Beatmap; - } - - private convertOsulabsBeatmap(beatmap: OsulabsBeatmap): Beatmap { - delete beatmap.set; - delete beatmap.last_checked; - delete beatmap.owners; - delete beatmap.current_user_tag_ids; - delete beatmap.top_tag_ids; - - return { - ...beatmap, - last_updated: new Date(beatmap.last_updated).toISOString(), - } as Beatmap; - } - - private convertMinoBeatmapset(beatmapset: MinoBeatmapset): Beatmapset { - delete beatmapset.next_update; - delete beatmapset.last_checked; - delete beatmapset.has_favourited; - delete beatmapset.recent_favourites; - delete beatmapset.related_tags; - delete beatmapset.rating; - - return { - ...beatmapset, - last_updated: new Date(beatmapset.last_updated).toISOString(), - beatmaps: beatmapset.beatmaps?.map((beatmap) => - this.convertMinoBeatmap(beatmap), - ), - converts: beatmapset.converts?.map((beatmap) => - this.convertMinoBeatmap(beatmap), - ), - } as Beatmapset; - } - - private convertOsulabsBeatmapset( - beatmapset: OsulabsBeatmapset, - ): Beatmapset { - delete beatmapset.next_update; - delete beatmapset.last_checked; - delete beatmapset.has_favourited; - delete beatmapset.recent_favourites; - delete beatmapset.related_tags; - delete beatmapset.rating; - - return { - ...beatmapset, - last_updated: new Date(beatmapset.last_updated).toISOString(), - beatmaps: beatmapset.beatmaps?.map((beatmap) => - this.convertOsulabsBeatmap(beatmap), - ), - converts: beatmapset.converts?.map((beatmap) => - this.convertOsulabsBeatmap(beatmap), - ), - } as Beatmapset; + } + + public convertBeatmap(beatmap: T): Beatmap { + switch (this.mirror) { + case "bancho": + return this.convertBanchoBeatmap(beatmap as BanchoBeatmap); + case "mino": + return this.convertMinoBeatmap(beatmap as MinoBeatmap); + case "osulabs": + return this.convertOsulabsBeatmap(beatmap as OsulabsBeatmap); + case "direct": + return this.convertDirectBeatmap(beatmap as DirectBeatmap); + default: + throw new Error("ConvertService: Cannot convert beatmap"); } + } + + private convertBacnhoBeatmapset(beatmapset: BanchoBeatmapset): Beatmapset { + delete beatmapset.current_user_attributes; + delete beatmapset.recent_favourites; + delete beatmapset.discussions; + delete beatmapset.events; + delete beatmapset.related_tags; + + return { + ...beatmapset, + beatmaps: beatmapset.beatmaps?.map(beatmap => + this.convertBanchoBeatmap(beatmap), + ), + converts: beatmapset.converts?.map(beatmap => + this.convertBanchoBeatmap(beatmap), + ), + } as Beatmapset; + } + + private convertBanchoBeatmap(beatmap: BanchoBeatmap): Beatmap { + delete beatmap.beatmapset; + delete beatmap.owners; + + return { + ...beatmap, + } as Beatmap; + } + + private convertDirectBeatmap(beatmap: DirectBeatmap): Beatmap { + return { + ...beatmap, + failtimes: { + fail: Array.from({ length: 100 }).fill(0), + exit: Array.from({ length: 100 }).fill(0), + }, + }; + } + + private convertMinoBeatmap(beatmap: MinoBeatmap): Beatmap { + delete beatmap.set; + delete beatmap.last_checked; + delete beatmap.owners; + delete beatmap.current_user_tag_ids; + delete beatmap.top_tag_ids; + + return { + ...beatmap, + last_updated: new Date(beatmap.last_updated).toISOString(), + } as Beatmap; + } + + private convertOsulabsBeatmap(beatmap: OsulabsBeatmap): Beatmap { + delete beatmap.set; + delete beatmap.last_checked; + delete beatmap.owners; + delete beatmap.current_user_tag_ids; + delete beatmap.top_tag_ids; + + return { + ...beatmap, + last_updated: new Date(beatmap.last_updated).toISOString(), + } as Beatmap; + } + + private convertMinoBeatmapset(beatmapset: MinoBeatmapset): Beatmapset { + delete beatmapset.next_update; + delete beatmapset.last_checked; + delete beatmapset.has_favourited; + delete beatmapset.recent_favourites; + delete beatmapset.related_tags; + delete beatmapset.rating; + + return { + ...beatmapset, + last_updated: new Date(beatmapset.last_updated).toISOString(), + beatmaps: beatmapset.beatmaps?.map(beatmap => + this.convertMinoBeatmap(beatmap), + ), + converts: beatmapset.converts?.map(beatmap => + this.convertMinoBeatmap(beatmap), + ), + } as Beatmapset; + } + + private convertOsulabsBeatmapset( + beatmapset: OsulabsBeatmapset, + ): Beatmapset { + delete beatmapset.next_update; + delete beatmapset.last_checked; + delete beatmapset.has_favourited; + delete beatmapset.recent_favourites; + delete beatmapset.related_tags; + delete beatmapset.rating; + + return { + ...beatmapset, + last_updated: new Date(beatmapset.last_updated).toISOString(), + beatmaps: beatmapset.beatmaps?.map(beatmap => + this.convertOsulabsBeatmap(beatmap), + ), + converts: beatmapset.converts?.map(beatmap => + this.convertOsulabsBeatmap(beatmap), + ), + } as Beatmapset; + } } diff --git a/server/src/core/services/stats.service.ts b/server/src/core/services/stats.service.ts index e6bb38f..130ddd7 100644 --- a/server/src/core/services/stats.service.ts +++ b/server/src/core/services/stats.service.ts @@ -1,41 +1,41 @@ -import { bytesToHumanReadableMegabytes } from '../../utils/stats'; +import { bytesToHumanReadableMegabytes } from "../../utils/stats"; export class StatsService { - public getServerStatistics() { - const uptimeNanoseconds = Bun.nanoseconds(); - const memory = process.memoryUsage(); + public getServerStatistics() { + const uptimeNanoseconds = Bun.nanoseconds(); + const memory = process.memoryUsage(); - return { - uptime: { - nanoseconds: uptimeNanoseconds, - pretty: this.formatNanoseconds(uptimeNanoseconds), - }, - memory: this.humanReadableMemory(memory), - pid: process.pid, - version: Bun.version, - revision: Bun.revision, - }; - } + return { + uptime: { + nanoseconds: uptimeNanoseconds, + pretty: this.formatNanoseconds(uptimeNanoseconds), + }, + memory: this.humanReadableMemory(memory), + pid: process.pid, + version: Bun.version, + revision: Bun.revision, + }; + } - private humanReadableMemory(memory: NodeJS.MemoryUsage) { - return Object.fromEntries( - Object.entries(memory).map(([key, value]) => [ - key, - bytesToHumanReadableMegabytes(value), - ]), - ); - } + private humanReadableMemory(memory: NodeJS.MemoryUsage) { + return Object.fromEntries( + Object.entries(memory).map(([key, value]) => [ + key, + bytesToHumanReadableMegabytes(value), + ]), + ); + } - private formatNanoseconds(nanoseconds: number) { - let seconds = Math.floor(nanoseconds / 1e9); - let minutes = Math.floor(seconds / 60); - let hours = Math.floor(minutes / 60); - let days = Math.floor(hours / 24); + private formatNanoseconds(nanoseconds: number) { + let seconds = Math.floor(nanoseconds / 1e9); + let minutes = Math.floor(seconds / 60); + let hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); - seconds %= 60; - minutes %= 60; - hours %= 24; + seconds %= 60; + minutes %= 60; + hours %= 24; - return `${days}d ${hours}h ${minutes}m ${seconds}s`; - } + return `${days}d ${hours}h ${minutes}m ${seconds}s`; + } } diff --git a/server/src/database/client.ts b/server/src/database/client.ts index fbd71b5..00e4a97 100644 --- a/server/src/database/client.ts +++ b/server/src/database/client.ts @@ -1,39 +1,39 @@ -import { drizzle } from 'drizzle-orm/node-postgres'; -import { Pool } from 'pg'; +import type { NodePgDatabase } from "drizzle-orm/node-postgres"; +import { drizzle } from "drizzle-orm/node-postgres"; +import { Pool } from "pg"; -import * as schema from './schema'; +import config from "../config"; +import * as schema from "./schema"; -import type { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import config from '../config'; export type DB = NodePgDatabase; class DbConnection { - private static instance: DbConnection; - private pool: Pool; + private static instance: DbConnection; + private pool: Pool; - private constructor() { - this.pool = new Pool({ - host: config.POSTGRES_HOST, - port: parseInt(config.POSTGRES_PORT, 10), - user: config.POSTGRES_USER, - password: config.POSTGRES_PASSWORD, - database: config.POSTGRES_DB, - max: 10, - }); - } + private constructor() { + this.pool = new Pool({ + host: config.POSTGRES_HOST, + port: Number.parseInt(config.POSTGRES_PORT, 10), + user: config.POSTGRES_USER, + password: config.POSTGRES_PASSWORD, + database: config.POSTGRES_DB, + max: 10, + }); + } - public static getInstance(): DbConnection { - if (!DbConnection.instance) { - DbConnection.instance = new DbConnection(); - } - return DbConnection.instance; + public static getInstance(): DbConnection { + if (!DbConnection.instance) { + DbConnection.instance = new DbConnection(); } + return DbConnection.instance; + } - public getClient(): DB { - return drizzle(this.pool, { - schema, - }); - } + public getClient(): DB { + return drizzle(this.pool, { + schema, + }); + } } const dbConnection = DbConnection.getInstance(); diff --git a/server/src/database/config.ts b/server/src/database/config.ts index 8df219b..6da374f 100644 --- a/server/src/database/config.ts +++ b/server/src/database/config.ts @@ -1,24 +1,27 @@ -import { Config, defineConfig } from 'drizzle-kit'; -import 'dotenv/config'; -import config from '../config'; +import "dotenv/config"; + +import type { Config } from "drizzle-kit"; +import { defineConfig } from "drizzle-kit"; + +import config from "../config"; export const dbCredentials = { - host: config.POSTGRES_HOST, - port: parseInt(config.POSTGRES_PORT), - user: config.POSTGRES_USER, - password: config.POSTGRES_PASSWORD, - database: config.POSTGRES_DB, + host: config.POSTGRES_HOST, + port: Number.parseInt(config.POSTGRES_PORT, 10), + user: config.POSTGRES_USER, + password: config.POSTGRES_PASSWORD, + database: config.POSTGRES_DB, }; export default defineConfig({ - out: './server/src/database/migrations', - schema: './server/src/database/schema.ts', - dialect: 'postgresql', - dbCredentials: { - url: `postgres://${dbCredentials.user}:${dbCredentials.password}@${dbCredentials.host}:${dbCredentials.port}/${dbCredentials.database}`, - }, - migrations: { - table: 'drizzle_migrations', - schema: 'public', - }, + out: "./server/src/database/migrations", + schema: "./server/src/database/schema.ts", + dialect: "postgresql", + dbCredentials: { + url: `postgres://${dbCredentials.user}:${dbCredentials.password}@${dbCredentials.host}:${dbCredentials.port}/${dbCredentials.database}`, + }, + migrations: { + table: "drizzle_migrations", + schema: "public", + }, }) satisfies Config; diff --git a/server/src/database/migrate.ts b/server/src/database/migrate.ts index 4e75e24..0368d70 100644 --- a/server/src/database/migrate.ts +++ b/server/src/database/migrate.ts @@ -1,19 +1,21 @@ -import { exit } from 'process'; -import { drizzle } from 'drizzle-orm/postgres-js'; -import { migrate } from 'drizzle-orm/postgres-js/migrator'; -import postgres from 'postgres'; -import config from '../config'; +import { exit } from "node:process"; + +import { drizzle } from "drizzle-orm/postgres-js"; +import { migrate } from "drizzle-orm/postgres-js/migrator"; +import postgres from "postgres"; + +import config from "../config"; const connection = postgres({ - host: config.POSTGRES_HOST, - port: parseInt(config.POSTGRES_PORT, 10), - user: config.POSTGRES_USER, - password: config.POSTGRES_PASSWORD, - database: config.POSTGRES_DB, - max: 1, + host: config.POSTGRES_HOST, + port: Number.parseInt(config.POSTGRES_PORT, 10), + user: config.POSTGRES_USER, + password: config.POSTGRES_PASSWORD, + database: config.POSTGRES_DB, + max: 1, }); await migrate(drizzle(connection), { - migrationsFolder: `${__dirname}/migrations`, + migrationsFolder: `${import.meta.dirname}/migrations`, }); exit(0); diff --git a/server/src/database/migrations/meta/0000_snapshot.json b/server/src/database/migrations/meta/0000_snapshot.json index 99503fe..ffde1ff 100644 --- a/server/src/database/migrations/meta/0000_snapshot.json +++ b/server/src/database/migrations/meta/0000_snapshot.json @@ -1,154 +1,154 @@ { - "id": "fd20adb3-8111-4359-b940-dd9333ddd005", - "prevId": "00000000-0000-0000-0000-000000000000", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.benchmarks": { - "name": "benchmarks", - "schema": "", - "columns": { - "benchmark_id": { - "name": "benchmark_id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "mirror_id": { - "name": "mirror_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "download_speed": { - "name": "download_speed", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "api_latency": { - "name": "api_latency", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "now()::timestamp without time zone" - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "now()::timestamp without time zone" - }, - "deleted_at": { - "name": "deleted_at", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "benchmarks_mirror_id_mirrors_mirror_id_fk": { - "name": "benchmarks_mirror_id_mirrors_mirror_id_fk", - "tableFrom": "benchmarks", - "tableTo": "mirrors", - "columnsFrom": ["mirror_id"], - "columnsTo": ["mirror_id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} + "id": "fd20adb3-8111-4359-b940-dd9333ddd005", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.benchmarks": { + "name": "benchmarks", + "schema": "", + "columns": { + "benchmark_id": { + "name": "benchmark_id", + "type": "serial", + "primaryKey": true, + "notNull": true }, - "public.mirrors": { - "name": "mirrors", - "schema": "", - "columns": { - "mirror_id": { - "name": "mirror_id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "url": { - "name": "url", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "weight": { - "name": "weight", - "type": "real", - "primaryKey": false, - "notNull": true, - "default": 1 - }, - "requests_processed": { - "name": "requests_processed", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "requests_failed": { - "name": "requests_failed", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "requests_total": { - "name": "requests_total", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "now()::timestamp without time zone" - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "now()::timestamp without time zone" - }, - "deleted_at": { - "name": "deleted_at", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} + "mirror_id": { + "name": "mirror_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "download_speed": { + "name": "download_speed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "api_latency": { + "name": "api_latency", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "now()::timestamp without time zone" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "now()::timestamp without time zone" + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": false } + }, + "indexes": {}, + "foreignKeys": { + "benchmarks_mirror_id_mirrors_mirror_id_fk": { + "name": "benchmarks_mirror_id_mirrors_mirror_id_fk", + "tableFrom": "benchmarks", + "tableTo": "mirrors", + "columnsFrom": ["mirror_id"], + "columnsTo": ["mirror_id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} + "public.mirrors": { + "name": "mirrors", + "schema": "", + "columns": { + "mirror_id": { + "name": "mirror_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "requests_processed": { + "name": "requests_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "requests_failed": { + "name": "requests_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "requests_total": { + "name": "requests_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "now()::timestamp without time zone" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "now()::timestamp without time zone" + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } } diff --git a/server/src/database/migrations/meta/0001_snapshot.json b/server/src/database/migrations/meta/0001_snapshot.json index 2e5b32b..2045178 100644 --- a/server/src/database/migrations/meta/0001_snapshot.json +++ b/server/src/database/migrations/meta/0001_snapshot.json @@ -228,4 +228,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/server/src/database/migrations/meta/0002_snapshot.json b/server/src/database/migrations/meta/0002_snapshot.json index 361c70b..b690b45 100644 --- a/server/src/database/migrations/meta/0002_snapshot.json +++ b/server/src/database/migrations/meta/0002_snapshot.json @@ -200,4 +200,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/server/src/database/migrations/meta/0003_snapshot.json b/server/src/database/migrations/meta/0003_snapshot.json index 336c322..3372b2b 100644 --- a/server/src/database/migrations/meta/0003_snapshot.json +++ b/server/src/database/migrations/meta/0003_snapshot.json @@ -199,4 +199,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/server/src/database/migrations/meta/0004_snapshot.json b/server/src/database/migrations/meta/0004_snapshot.json index e9e72ca..b13b8eb 100644 --- a/server/src/database/migrations/meta/0004_snapshot.json +++ b/server/src/database/migrations/meta/0004_snapshot.json @@ -736,4 +736,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/server/src/database/migrations/meta/0005_snapshot.json b/server/src/database/migrations/meta/0005_snapshot.json index 82793c6..0d65e80 100644 --- a/server/src/database/migrations/meta/0005_snapshot.json +++ b/server/src/database/migrations/meta/0005_snapshot.json @@ -736,4 +736,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/server/src/database/migrations/meta/0006_snapshot.json b/server/src/database/migrations/meta/0006_snapshot.json index 17f1542..32146e1 100644 --- a/server/src/database/migrations/meta/0006_snapshot.json +++ b/server/src/database/migrations/meta/0006_snapshot.json @@ -730,4 +730,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/server/src/database/migrations/meta/0007_snapshot.json b/server/src/database/migrations/meta/0007_snapshot.json index c837872..49ee347 100644 --- a/server/src/database/migrations/meta/0007_snapshot.json +++ b/server/src/database/migrations/meta/0007_snapshot.json @@ -736,4 +736,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/server/src/database/migrations/meta/0008_snapshot.json b/server/src/database/migrations/meta/0008_snapshot.json index 69e6d02..c3fef93 100644 --- a/server/src/database/migrations/meta/0008_snapshot.json +++ b/server/src/database/migrations/meta/0008_snapshot.json @@ -742,4 +742,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/server/src/database/migrations/meta/0009_snapshot.json b/server/src/database/migrations/meta/0009_snapshot.json index b5e4705..9a504fb 100644 --- a/server/src/database/migrations/meta/0009_snapshot.json +++ b/server/src/database/migrations/meta/0009_snapshot.json @@ -673,4 +673,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/server/src/database/migrations/meta/0010_snapshot.json b/server/src/database/migrations/meta/0010_snapshot.json index 88b490c..ecd2b1c 100644 --- a/server/src/database/migrations/meta/0010_snapshot.json +++ b/server/src/database/migrations/meta/0010_snapshot.json @@ -722,4 +722,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/server/src/database/migrations/meta/_journal.json b/server/src/database/migrations/meta/_journal.json index fe25237..30d9360 100644 --- a/server/src/database/migrations/meta/_journal.json +++ b/server/src/database/migrations/meta/_journal.json @@ -80,4 +80,4 @@ "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/server/src/database/models/beatmap.ts b/server/src/database/models/beatmap.ts index 18af5e1..a6a919f 100644 --- a/server/src/database/models/beatmap.ts +++ b/server/src/database/models/beatmap.ts @@ -1,167 +1,168 @@ -import { and, count, eq, gte, sql } from 'drizzle-orm'; -import { db } from '../client'; -import { beatmaps, NewBeatmap } from '../schema'; -import { Beatmap as BeatmapObject } from '../../types/general/beatmap'; -import { Beatmap as BeatmapDatabase } from '../schema'; -import { RankStatus } from '../../types/general/rankStatus'; -import { getUTCDate } from '../../utils/date'; -import { GameMode } from '../../types/general/gameMode'; +import { and, count, eq, gte, sql } from "drizzle-orm"; + +import type { Beatmap as BeatmapObject } from "../../types/general/beatmap"; +import type { GameMode } from "../../types/general/gameMode"; +import { RankStatus } from "../../types/general/rankStatus"; +import { getUTCDate } from "../../utils/date"; +import { db } from "../client"; +import type { Beatmap as BeatmapDatabase, NewBeatmap } from "../schema"; +import { beatmaps } from "../schema"; const ONE_DAY = 1000 * 60 * 60 * 24; export async function getBeatmapCount() { - const entities = await db - .select({ count: count() }) - .from(beatmaps) - .where( - gte( - sql`cast(${beatmaps.validUntil} as timestamp)`, - sql`cast(${getUTCDate()} as timestamp)`, - ), - ); - - if (entities.length <= 0) { - return 0; - } - - return entities[0].count; + const entities = await db + .select({ count: count() }) + .from(beatmaps) + .where( + gte( + sql`cast(${beatmaps.validUntil} as timestamp)`, + sql`cast(${getUTCDate()} as timestamp)`, + ), + ); + + if (entities.length <= 0) { + return 0; + } + + return entities[0].count; } export async function getBeatmapById( - beatmapId: number, + beatmapId: number, ): Promise { - const entities = await db - .select() - .from(beatmaps) - .where( - and( - eq(beatmaps.id, beatmapId), - eq(beatmaps.convert, false), - gte( - sql`cast(${beatmaps.validUntil} as timestamp)`, - sql`cast(${getUTCDate()} as timestamp)`, - ), - ), - ); - - if (entities.length === 0) { - return null; - } - - return databaseToObject(entities[0]); + const entities = await db + .select() + .from(beatmaps) + .where( + and( + eq(beatmaps.id, beatmapId), + eq(beatmaps.convert, false), + gte( + sql`cast(${beatmaps.validUntil} as timestamp)`, + sql`cast(${getUTCDate()} as timestamp)`, + ), + ), + ); + + if (entities.length === 0) { + return null; + } + + return databaseToObject(entities[0]); } export async function getBeatmapByHash( - beatmapHash: string, + beatmapHash: string, ): Promise { - const entities = await db - .select() - .from(beatmaps) - .where( - and( - eq(beatmaps.checksum, beatmapHash), - eq(beatmaps.convert, false), - gte( - sql`cast(${beatmaps.validUntil} as timestamp)`, - sql`cast(${getUTCDate()} as timestamp)`, - ), - ), - ); - - if (entities.length === 0) { - return null; - } - - return databaseToObject(entities[0]); + const entities = await db + .select() + .from(beatmaps) + .where( + and( + eq(beatmaps.checksum, beatmapHash), + eq(beatmaps.convert, false), + gte( + sql`cast(${beatmaps.validUntil} as timestamp)`, + sql`cast(${getUTCDate()} as timestamp)`, + ), + ), + ); + + if (entities.length === 0) { + return null; + } + + return databaseToObject(entities[0]); } export async function getBeatmapsBySetId( - beatmapSetId: number, - convert: boolean = false, + beatmapSetId: number, + convert = false, ): Promise { - const entities = await db - .select() - .from(beatmaps) - .where( - and( - eq(beatmaps.beatmapset_id, beatmapSetId), - eq(beatmaps.convert, convert), - gte( - sql`cast(${beatmaps.validUntil} as timestamp)`, - sql`cast(${getUTCDate()} as timestamp)`, - ), - ), - ); - - if (entities.length === 0) { - return null; - } - - return entities.map((e) => databaseToObject(e)); + const entities = await db + .select() + .from(beatmaps) + .where( + and( + eq(beatmaps.beatmapset_id, beatmapSetId), + eq(beatmaps.convert, convert), + gte( + sql`cast(${beatmaps.validUntil} as timestamp)`, + sql`cast(${getUTCDate()} as timestamp)`, + ), + ), + ); + + if (entities.length === 0) { + return null; + } + + return entities.map(e => databaseToObject(e)); } export async function createBeatmap( - obj: BeatmapObject, + obj: BeatmapObject, ): Promise { - const data: NewBeatmap = objectToDatabase(obj); - - const entities = await db - .insert(beatmaps) - .values(data) - .onConflictDoUpdate({ - target: [beatmaps.id, beatmaps.mode], - set: data, - }) - .returning(); - return entities[0]; + const data: NewBeatmap = objectToDatabase(obj); + + const entities = await db + .insert(beatmaps) + .values(data) + .onConflictDoUpdate({ + target: [beatmaps.id, beatmaps.mode], + set: data, + }) + .returning(); + return entities[0]; } function objectToDatabase(obj: BeatmapObject): BeatmapDatabase { - const data: BeatmapDatabase = { - ...obj, - status: obj.status, - ranked: obj.ranked, - failtimes: JSON.stringify(obj.failtimes), - bpm: obj.bpm ?? null, - deleted_at: obj.deleted_at ?? null, - checksum: obj.checksum ?? null, - max_combo: obj.max_combo ?? null, - validUntil: new Date( - getUTCDate().getTime() + getTTLBasedOnStatus(obj.status), - ).toISOString(), - }; - - return data; + const data: BeatmapDatabase = { + ...obj, + status: obj.status, + ranked: obj.ranked, + failtimes: JSON.stringify(obj.failtimes), + bpm: obj.bpm ?? null, + deleted_at: obj.deleted_at ?? null, + checksum: obj.checksum ?? null, + max_combo: obj.max_combo ?? null, + validUntil: new Date( + getUTCDate().getTime() + getTTLBasedOnStatus(obj.status), + ).toISOString(), + }; + + return data; } export function databaseToObject(obj: BeatmapDatabase): BeatmapObject { - return { - ...obj, - mode: obj.mode as GameMode, - bpm: obj.bpm ?? 0, - checksum: obj.checksum ?? undefined, - max_combo: obj.max_combo ?? undefined, - status: obj.status as RankStatus, - ranked: obj.ranked, - failtimes: JSON.parse(obj.failtimes ?? '{}'), - // @ts-ignore - validUntil: undefined, - }; + return { + ...obj, + mode: obj.mode as GameMode, + bpm: obj.bpm ?? 0, + checksum: obj.checksum ?? undefined, + max_combo: obj.max_combo ?? undefined, + status: obj.status as RankStatus, + ranked: obj.ranked, + failtimes: JSON.parse(obj.failtimes ?? "{}"), + // @ts-expect-error -- Will be set when enriching + validUntil: undefined, + }; } function getTTLBasedOnStatus(status: RankStatus): number { - switch (status) { - case RankStatus.GRAVEYARD: - return ONE_DAY * 7; // 7 days - case RankStatus.PENDING: - case RankStatus.QUALIFIED: - case RankStatus.WIP: - return 1000 * 60 * 5; // 5 minutes - case RankStatus.APPROVED: - case RankStatus.LOVED: - case RankStatus.RANKED: - return ONE_DAY * 30; // 30 days - default: - return -1; - } + switch (status) { + case RankStatus.GRAVEYARD: + return ONE_DAY * 7; // 7 days + case RankStatus.PENDING: + case RankStatus.QUALIFIED: + case RankStatus.WIP: + return 1000 * 60 * 5; // 5 minutes + case RankStatus.APPROVED: + case RankStatus.LOVED: + case RankStatus.RANKED: + return ONE_DAY * 30; // 30 days + default: + return -1; + } } diff --git a/server/src/database/models/beatmapOsuFile.ts b/server/src/database/models/beatmapOsuFile.ts index dbf9a94..db92eb8 100644 --- a/server/src/database/models/beatmapOsuFile.ts +++ b/server/src/database/models/beatmapOsuFile.ts @@ -1,81 +1,83 @@ -import { and, count, eq, gte, inArray, sql } from 'drizzle-orm'; -import { db } from '../client'; -import { BeatmapOsuFile, beatmapOsuFiles, NewBeatmapOsuFile } from '../schema'; -import { getUTCDate } from '../../utils/date'; -import { DownloadOsuBeatmap } from '../../core/abstracts/client/base-client.types'; +import { and, count, eq, gte, inArray, sql } from "drizzle-orm"; + +import type { DownloadOsuBeatmap } from "../../core/abstracts/client/base-client.types"; +import { getUTCDate } from "../../utils/date"; +import { db } from "../client"; +import type { BeatmapOsuFile, NewBeatmapOsuFile } from "../schema"; +import { beatmapOsuFiles } from "../schema"; export async function getBeatmapOsuFileCount() { - const entities = await db - .select({ count: count() }) - .from(beatmapOsuFiles) - .where( - gte( - sql`cast(${beatmapOsuFiles.validUntil} as timestamp)`, - sql`cast(${getUTCDate()} as timestamp)`, - ), - ); + const entities = await db + .select({ count: count() }) + .from(beatmapOsuFiles) + .where( + gte( + sql`cast(${beatmapOsuFiles.validUntil} as timestamp)`, + sql`cast(${getUTCDate()} as timestamp)`, + ), + ); - if (entities.length <= 0) { - return 0; - } + if (entities.length <= 0) { + return 0; + } - return entities[0].count; + return entities[0].count; } export async function getUnvalidBeatmapOsuFiles(): Promise { - const entities = await db - .select() - .from(beatmapOsuFiles) - .where( - and( - gte( - sql`cast(${getUTCDate()} as timestamp)`, - sql`cast(${beatmapOsuFiles.validUntil} as timestamp)`, - ), - ), - ); + const entities = await db + .select() + .from(beatmapOsuFiles) + .where( + and( + gte( + sql`cast(${getUTCDate()} as timestamp)`, + sql`cast(${beatmapOsuFiles.validUntil} as timestamp)`, + ), + ), + ); - return entities ?? []; + return entities ?? []; } export async function getBeatmapOsuFile( - ctx: DownloadOsuBeatmap, + ctx: DownloadOsuBeatmap, ): Promise { - const entities = await db - .select() - .from(beatmapOsuFiles) - .where( - and( - eq(beatmapOsuFiles.id, ctx.beatmapId), - gte( - sql`cast(${beatmapOsuFiles.validUntil} as timestamp)`, - sql`cast(${getUTCDate()} as timestamp)`, - ), - ), - ); + const entities = await db + .select() + .from(beatmapOsuFiles) + .where( + and( + eq(beatmapOsuFiles.id, ctx.beatmapId), + gte( + sql`cast(${beatmapOsuFiles.validUntil} as timestamp)`, + sql`cast(${getUTCDate()} as timestamp)`, + ), + ), + ); - return entities[0] ?? null; + return entities[0] ?? null; } export async function createBeatmapOsuFile( - data: NewBeatmapOsuFile, + data: NewBeatmapOsuFile, ): Promise { - const entities = await db - .insert(beatmapOsuFiles) - .values(data) - .onConflictDoUpdate({ - target: [beatmapOsuFiles.id], - set: data, - }) - .returning(); - return entities[0]; + const entities = await db + .insert(beatmapOsuFiles) + .values(data) + .onConflictDoUpdate({ + target: [beatmapOsuFiles.id], + set: data, + }) + .returning(); + return entities[0]; } export async function deleteBeatmapsOsuFiles(data: BeatmapOsuFile[]) { - await db.delete(beatmapOsuFiles).where( - inArray( - beatmapOsuFiles.id, - data.map((s) => s.id), - ), - ); + await db.delete(beatmapOsuFiles).where( + inArray( + beatmapOsuFiles.id, + data.map(s => s.id), + ), + ); } diff --git a/server/src/database/models/beatmapset.ts b/server/src/database/models/beatmapset.ts index 6720b85..4a18e1a 100644 --- a/server/src/database/models/beatmapset.ts +++ b/server/src/database/models/beatmapset.ts @@ -1,285 +1,286 @@ -import { and, count, eq, gte, inArray, sql } from 'drizzle-orm'; -import { db } from '../client'; -import { beatmaps, Beatmapset, beatmapsets, NewBeatmapset } from '../schema'; -import { - Beatmapset as BeatmapsetObject, - Beatmap as BeatmapObject, -} from '../../types/general/beatmap'; - -import { databaseToObject as beatmapDatabaseToObject } from './beatmap'; -import { Beatmapset as BeatmapsetDatabase } from '../schema'; -import { RankStatus } from '../../types/general/rankStatus'; -import { getUTCDate } from '../../utils/date'; -import { createBeatmap } from './beatmap'; -import { splitByCondition } from '../../utils/array'; +import { and, count, eq, gte, inArray, sql } from "drizzle-orm"; + +import type { + Beatmap as BeatmapObject, + Beatmapset as BeatmapsetObject, +} from "../../types/general/beatmap"; +import { RankStatus } from "../../types/general/rankStatus"; +import { splitByCondition } from "../../utils/array"; +import { getUTCDate } from "../../utils/date"; +import { db } from "../client"; +import type { Beatmapset, Beatmapset as BeatmapsetDatabase, NewBeatmapset } from "../schema"; +import { beatmaps, beatmapsets } from "../schema"; +import { createBeatmap, databaseToObject as beatmapDatabaseToObject } from "./beatmap"; const ONE_DAY = 1000 * 60 * 60 * 24; export async function getBeatmapSetCount() { - const entities = await db - .select({ count: count() }) - .from(beatmapsets) - .where( - gte( - sql`cast(${beatmapsets.validUntil} as timestamp)`, - sql`cast(${getUTCDate()} as timestamp)`, - ), - ); - - if (entities.length <= 0) { - return 0; - } + const entities = await db + .select({ count: count() }) + .from(beatmapsets) + .where( + gte( + sql`cast(${beatmapsets.validUntil} as timestamp)`, + sql`cast(${getUTCDate()} as timestamp)`, + ), + ); - return entities[0].count; + if (entities.length <= 0) { + return 0; + } + + return entities[0].count; } export async function getUnvalidBeatmapSets(): Promise { - const entities = await db - .select() - .from(beatmapsets) - .where( - and( - gte( - sql`cast(${getUTCDate()} as timestamp)`, - sql`cast(${beatmapsets.validUntil} as timestamp)`, - ), - ), - ); - - return entities; + const entities = await db + .select() + .from(beatmapsets) + .where( + and( + gte( + sql`cast(${getUTCDate()} as timestamp)`, + sql`cast(${beatmapsets.validUntil} as timestamp)`, + ), + ), + ); + + return entities; } export async function getBeatmapSetById( - beatmapsetId: number, + beatmapsetId: number, ): Promise { - const entities = await db - .select() - .from(beatmapsets) - .where( - and( - eq(beatmapsets.id, beatmapsetId), - gte( - sql`cast(${beatmapsets.validUntil} as timestamp)`, - sql`cast(${getUTCDate()} as timestamp)`, - ), - ), - ) - .innerJoin(beatmaps, eq(beatmapsets.id, beatmaps.beatmapset_id)); - - if (entities.length === 0) { - return null; - } - - const result = { - beatmapsets: entities[0].beatmapsets, - beatmaps: entities.map((e) => e.beatmaps), - }; - - if ( - result.beatmaps.some( - (b) => Date.parse(b.validUntil) < getUTCDate().getTime(), - ) - ) { - return null; - } - - return await enrichWithBeatmaps( - databaseToObject(result.beatmapsets), - result.beatmaps.map((b) => beatmapDatabaseToObject(b)), - ); + const entities = await db + .select() + .from(beatmapsets) + .where( + and( + eq(beatmapsets.id, beatmapsetId), + gte( + sql`cast(${beatmapsets.validUntil} as timestamp)`, + sql`cast(${getUTCDate()} as timestamp)`, + ), + ), + ) + .innerJoin(beatmaps, eq(beatmapsets.id, beatmaps.beatmapset_id)); + + if (entities.length === 0) { + return null; + } + + const result = { + beatmapsets: entities[0].beatmapsets, + beatmaps: entities.map(e => e.beatmaps), + }; + + if ( + result.beatmaps.some( + b => Date.parse(b.validUntil) < getUTCDate().getTime(), + ) + ) { + return null; + } + + return await enrichWithBeatmaps( + databaseToObject(result.beatmapsets), + result.beatmaps.map(b => beatmapDatabaseToObject(b)), + ); } export async function getBeatmapSetsByBeatmapIds( - beatmapIds: number[], - ignoreValidUntil: true, + beatmapIds: number[], + ignoreValidUntil: true, ): Promise { - const whereConditions = [ - inArray(beatmaps.id, beatmapIds), - ignoreValidUntil - ? undefined - : gte( - sql`cast(${beatmapsets.validUntil} as timestamp)`, - sql`cast(${getUTCDate()} as timestamp)`, - ), - ].filter((condition) => condition !== undefined); - - const entities = await db - .select() - .from(beatmapsets) - .innerJoin(beatmaps, eq(beatmapsets.id, beatmaps.beatmapset_id)) - .where(and(...whereConditions)); - - if (entities.length === 0) { - return null; - } + const whereConditions = [ + inArray(beatmaps.id, beatmapIds), + ignoreValidUntil + ? undefined + : gte( + sql`cast(${beatmapsets.validUntil} as timestamp)`, + sql`cast(${getUTCDate()} as timestamp)`, + ), + ].filter(condition => condition !== undefined); - const data = entities.reduce((acc, { beatmapsets, beatmaps }) => { - const setId = beatmapsets.id; + const entities = await db + .select() + .from(beatmapsets) + .innerJoin(beatmaps, eq(beatmapsets.id, beatmaps.beatmapset_id)) + .where(and(...whereConditions)); - const existing = acc.find((e) => e.id === setId); + if (entities.length === 0) { + return null; + } - if (existing) { - existing.beatmaps?.push(beatmapDatabaseToObject(beatmaps)); - } else { - acc.push({ - ...databaseToObject(beatmapsets), - beatmaps: [beatmapDatabaseToObject(beatmaps)], - }); - } + const data = entities.reduce((acc, { beatmapsets, beatmaps }) => { + const setId = beatmapsets.id; - return acc; - }, [] as BeatmapsetObject[]); + const existing = acc.find(e => e.id === setId); - return data.map((set) => enrichWithBeatmaps(set, set.beatmaps ?? [])); + if (existing) { + existing.beatmaps?.push(beatmapDatabaseToObject(beatmaps)); + } + else { + acc.push({ + ...databaseToObject(beatmapsets), + beatmaps: [beatmapDatabaseToObject(beatmaps)], + }); + } + + return acc; + }, [] as BeatmapsetObject[]); + + return data.map(set => enrichWithBeatmaps(set, set.beatmaps ?? [])); } export async function createBeatmapset( - obj: BeatmapsetObject, + obj: BeatmapsetObject, ): Promise { - const data: NewBeatmapset = objectToDatabase(obj); - - const createBeatmapsPromises = Array.from( - [obj.beatmaps, obj.converts].flat(), - ).map(async (b) => (b ? await createBeatmap(b) : null)); - - await Promise.all(createBeatmapsPromises); - - data.validUntil = getValidUntilBasedOnBeatmapsTTL( - [obj.beatmaps ?? [], obj.converts ?? []].flat(), - ); - - const entities = await db - .insert(beatmapsets) - .values(data) - .onConflictDoUpdate({ - target: [beatmapsets.id], - set: data, - }) - .returning(); - return entities[0]; + const data: NewBeatmapset = objectToDatabase(obj); + + const createBeatmapsPromises = Array.from( + [obj.beatmaps, obj.converts].flat(), + ).map(async b => (b ? await createBeatmap(b) : null)); + + await Promise.all(createBeatmapsPromises); + + data.validUntil = getValidUntilBasedOnBeatmapsTTL( + [obj.beatmaps ?? [], obj.converts ?? []].flat(), + ); + + const entities = await db + .insert(beatmapsets) + .values(data) + .onConflictDoUpdate({ + target: [beatmapsets.id], + set: data, + }) + .returning(); + return entities[0]; } export async function deleteBeatmapsets(data: Beatmapset[]) { - await db.delete(beatmapsets).where( - inArray( - beatmapsets.id, - data.map((s) => s.id), - ), - ); - - await db.delete(beatmaps).where( - inArray( - beatmaps.beatmapset_id, - data.map((s) => s.id), - ), - ); + await db.delete(beatmapsets).where( + inArray( + beatmapsets.id, + data.map(s => s.id), + ), + ); + + await db.delete(beatmaps).where( + inArray( + beatmaps.beatmapset_id, + data.map(s => s.id), + ), + ); } function enrichWithBeatmaps( - beatmapset: BeatmapsetObject, - beatmaps: BeatmapObject[], + beatmapset: BeatmapsetObject, + beatmaps: BeatmapObject[], ): BeatmapsetObject { - const [convertedBeatmaps, defaultBeatmaps] = splitByCondition( - beatmaps, - (b) => b.convert, - ); - - return { - ...beatmapset, - beatmaps: defaultBeatmaps ?? undefined, - converts: convertedBeatmaps ?? undefined, - }; + const [convertedBeatmaps, defaultBeatmaps] = splitByCondition( + beatmaps, + b => b.convert, + ); + + return { + ...beatmapset, + beatmaps: defaultBeatmaps ?? undefined, + converts: convertedBeatmaps ?? undefined, + }; } function objectToDatabase(obj: BeatmapsetObject): BeatmapsetDatabase { - const data: BeatmapsetDatabase = { - ...obj, - status: obj.status, - ranked: obj.ranked, - bpm: obj.bpm ?? null, - deleted_at: obj.deleted_at ?? null, - covers: JSON.stringify(obj.covers), - hype: JSON.stringify(obj.hype), - track_id: obj.track_id ?? null, - legacy_thread_url: obj.legacy_thread_url ?? null, - nominations_summary: JSON.stringify(obj.nominations_summary), - ranked_date: obj.ranked_date ?? null, - submitted_date: obj.submitted_date ?? null, - availability: JSON.stringify(obj.availability), - has_favourited: obj.has_favourited ?? null, - current_nominations: JSON.stringify(obj.current_nominations), - description: JSON.stringify(obj.description), - genre: JSON.stringify(obj.genre), - langauge: JSON.stringify(obj.langauge), - pack_tags: obj.pack_tags ?? null, - ratings: obj.ratings ?? null, - related_users: JSON.stringify(obj.related_users), - user: JSON.stringify(obj.user), - validUntil: new Date( - getUTCDate().getTime() + getTTLBasedOnStatus(obj.status), - ).toISOString(), - }; - - return data; + const data: BeatmapsetDatabase = { + ...obj, + status: obj.status, + ranked: obj.ranked, + bpm: obj.bpm ?? null, + deleted_at: obj.deleted_at ?? null, + covers: JSON.stringify(obj.covers), + hype: JSON.stringify(obj.hype), + track_id: obj.track_id ?? null, + legacy_thread_url: obj.legacy_thread_url ?? null, + nominations_summary: JSON.stringify(obj.nominations_summary), + ranked_date: obj.ranked_date ?? null, + submitted_date: obj.submitted_date ?? null, + availability: JSON.stringify(obj.availability), + has_favourited: obj.has_favourited ?? null, + current_nominations: JSON.stringify(obj.current_nominations), + description: JSON.stringify(obj.description), + genre: JSON.stringify(obj.genre), + langauge: JSON.stringify(obj.langauge), + pack_tags: obj.pack_tags ?? null, + ratings: obj.ratings ?? null, + related_users: JSON.stringify(obj.related_users), + user: JSON.stringify(obj.user), + validUntil: new Date( + getUTCDate().getTime() + getTTLBasedOnStatus(obj.status), + ).toISOString(), + }; + + return data; } function databaseToObject(obj: BeatmapsetDatabase): BeatmapsetObject { - const data: BeatmapsetObject = { - ...obj, - status: obj.status as RankStatus, - ranked: obj.ranked, - covers: JSON.parse(obj.covers), - hype: JSON.parse(obj.hype ?? '{}'), - nominations_summary: JSON.parse(obj.nominations_summary ?? '{}'), - submitted_date: obj.submitted_date ?? undefined, - availability: JSON.parse(obj.availability ?? '{}'), - has_favourited: obj.has_favourited ?? undefined, - current_nominations: JSON.parse(obj.current_nominations ?? '{}'), - description: JSON.parse(obj.description ?? '{}'), - genre: JSON.parse(obj.genre ?? '{}'), - langauge: JSON.parse(obj.langauge ?? '{}'), - pack_tags: obj.pack_tags ?? undefined, - ratings: obj.ratings ?? undefined, - related_users: JSON.parse(obj.related_users ?? '{}'), - user: JSON.parse(obj.user ?? '{}'), - bpm: obj.bpm ?? 0, - legacy_thread_url: obj.legacy_thread_url ?? undefined, - related_tags: undefined, - // @ts-ignore - validUntil: undefined, - }; - - return data; + const data: BeatmapsetObject = { + ...obj, + status: obj.status as RankStatus, + ranked: obj.ranked, + covers: JSON.parse(obj.covers), + hype: JSON.parse(obj.hype ?? "{}"), + nominations_summary: JSON.parse(obj.nominations_summary ?? "{}"), + submitted_date: obj.submitted_date ?? undefined, + availability: JSON.parse(obj.availability ?? "{}"), + has_favourited: obj.has_favourited ?? undefined, + current_nominations: JSON.parse(obj.current_nominations ?? "{}"), + description: JSON.parse(obj.description ?? "{}"), + genre: JSON.parse(obj.genre ?? "{}"), + langauge: JSON.parse(obj.langauge ?? "{}"), + pack_tags: obj.pack_tags ?? undefined, + ratings: obj.ratings ?? undefined, + related_users: JSON.parse(obj.related_users ?? "{}"), + user: JSON.parse(obj.user ?? "{}"), + bpm: obj.bpm ?? 0, + legacy_thread_url: obj.legacy_thread_url ?? undefined, + related_tags: undefined, + // @ts-expect-error -- Will be set when enriching + validUntil: undefined, + }; + + return data; } function getTTLBasedOnStatus(status: RankStatus): number { - switch (status) { - case RankStatus.GRAVEYARD: - return ONE_DAY * 7; // 7 days - case RankStatus.PENDING: - case RankStatus.WIP: - case RankStatus.QUALIFIED: - return 1000 * 60 * 5; // 5 minutes - case RankStatus.APPROVED: - case RankStatus.LOVED: - case RankStatus.RANKED: - return ONE_DAY * 30; // 30 days - default: - return -1; - } + switch (status) { + case RankStatus.GRAVEYARD: + return ONE_DAY * 7; // 7 days + case RankStatus.PENDING: + case RankStatus.WIP: + case RankStatus.QUALIFIED: + return 1000 * 60 * 5; // 5 minutes + case RankStatus.APPROVED: + case RankStatus.LOVED: + case RankStatus.RANKED: + return ONE_DAY * 30; // 30 days + default: + return -1; + } } function getValidUntilBasedOnBeatmapsTTL(beatmaps: BeatmapObject[]): string { - return new Date( - getUTCDate().getTime() + - getLowestTTLBasedOnStatuses(beatmaps.map((b) => b.status)), - ).toISOString(); + return new Date( + getUTCDate().getTime() + + getLowestTTLBasedOnStatuses(beatmaps.map(b => b.status)), + ).toISOString(); } function getLowestTTLBasedOnStatuses(statuses: RankStatus[]): number { - if (statuses.length === 0) return 0; + if (statuses.length === 0) + return 0; - return statuses - .map((status) => getTTLBasedOnStatus(status)) - .sort((a, b) => a - b)[0]; + return statuses + .map(status => getTTLBasedOnStatus(status)) + .sort((a, b) => a - b)[0]; } diff --git a/server/src/database/models/beatmapsetFile.ts b/server/src/database/models/beatmapsetFile.ts index 50635c9..2875ea1 100644 --- a/server/src/database/models/beatmapsetFile.ts +++ b/server/src/database/models/beatmapsetFile.ts @@ -1,84 +1,86 @@ -import { and, count, eq, gte, inArray, sql } from 'drizzle-orm'; -import { db } from '../client'; -import { BeatmapsetFile, beatmapsetsFiles, NewBeatmapsetFile } from '../schema'; -import { getUTCDate } from '../../utils/date'; -import { DownloadBeatmapSetOptions } from '../../core/abstracts/client/base-client.types'; +import { and, count, eq, gte, inArray, sql } from "drizzle-orm"; + +import type { DownloadBeatmapSetOptions } from "../../core/abstracts/client/base-client.types"; +import { getUTCDate } from "../../utils/date"; +import { db } from "../client"; +import type { BeatmapsetFile, NewBeatmapsetFile } from "../schema"; +import { beatmapsetsFiles } from "../schema"; export async function getBeatmapSetsFilesCount() { - const entities = await db - .select({ count: count() }) - .from(beatmapsetsFiles) - .where( - gte( - sql`cast(${beatmapsetsFiles.validUntil} as timestamp)`, - sql`cast(${getUTCDate()} as timestamp)`, - ), - ); + const entities = await db + .select({ count: count() }) + .from(beatmapsetsFiles) + .where( + gte( + sql`cast(${beatmapsetsFiles.validUntil} as timestamp)`, + sql`cast(${getUTCDate()} as timestamp)`, + ), + ); - if (entities.length <= 0) { - return 0; - } + if (entities.length <= 0) { + return 0; + } - return entities[0].count; + return entities[0].count; } export async function getUnvalidBeatmapSetsFiles(): Promise { - const entities = await db - .select() - .from(beatmapsetsFiles) - .where( - and( - gte( - sql`cast(${getUTCDate()} as timestamp)`, - sql`cast(${beatmapsetsFiles.validUntil} as timestamp)`, - ), - ), - ); + const entities = await db + .select() + .from(beatmapsetsFiles) + .where( + and( + gte( + sql`cast(${getUTCDate()} as timestamp)`, + sql`cast(${beatmapsetsFiles.validUntil} as timestamp)`, + ), + ), + ); - return entities ?? []; + return entities ?? []; } export async function getBeatmapSetFile( - ctx: DownloadBeatmapSetOptions, + ctx: DownloadBeatmapSetOptions, ): Promise { - const entities = await db - .select() - .from(beatmapsetsFiles) - .where( - and( - eq(beatmapsetsFiles.id, ctx.beatmapSetId), - ctx.noVideo !== true - ? eq(beatmapsetsFiles.noVideo, false) - : undefined, - gte( - sql`cast(${beatmapsetsFiles.validUntil} as timestamp)`, - sql`cast(${getUTCDate()} as timestamp)`, - ), - ), - ); + const entities = await db + .select() + .from(beatmapsetsFiles) + .where( + and( + eq(beatmapsetsFiles.id, ctx.beatmapSetId), + ctx.noVideo !== true + ? eq(beatmapsetsFiles.noVideo, false) + : undefined, + gte( + sql`cast(${beatmapsetsFiles.validUntil} as timestamp)`, + sql`cast(${getUTCDate()} as timestamp)`, + ), + ), + ); - return entities[0] ?? null; + return entities[0] ?? null; } export async function createBeatmapsetFile( - data: NewBeatmapsetFile, + data: NewBeatmapsetFile, ): Promise { - const entities = await db - .insert(beatmapsetsFiles) - .values(data) - .onConflictDoUpdate({ - target: [beatmapsetsFiles.id], - set: data, - }) - .returning(); - return entities[0]; + const entities = await db + .insert(beatmapsetsFiles) + .values(data) + .onConflictDoUpdate({ + target: [beatmapsetsFiles.id], + set: data, + }) + .returning(); + return entities[0]; } export async function deleteBeatmapsetsFiles(data: BeatmapsetFile[]) { - await db.delete(beatmapsetsFiles).where( - inArray( - beatmapsetsFiles.id, - data.map((s) => s.id), - ), - ); + await db.delete(beatmapsetsFiles).where( + inArray( + beatmapsetsFiles.id, + data.map(s => s.id), + ), + ); } diff --git a/server/src/database/models/mirrors.ts b/server/src/database/models/mirrors.ts index 9c90cf2..f1ac451 100644 --- a/server/src/database/models/mirrors.ts +++ b/server/src/database/models/mirrors.ts @@ -1,27 +1,29 @@ -import { eq } from 'drizzle-orm'; -import { db } from '../client'; -import { Mirror, mirrors, NewMirror } from '../schema'; +import { eq } from "drizzle-orm"; + +import { db } from "../client"; +import type { Mirror, NewMirror } from "../schema"; +import { mirrors } from "../schema"; export async function getMirrorByUrl(url: string): Promise { - const entities = await db - .select() - .from(mirrors) - .where(eq(mirrors.url, url)); - return entities[0] ?? null; + const entities = await db + .select() + .from(mirrors) + .where(eq(mirrors.url, url)); + return entities[0] ?? null; } export async function getMirrors(): Promise { - return db.select().from(mirrors); + return db.select().from(mirrors); } export async function updateMirror( - mirrorId: number, - data: Partial, + mirrorId: number, + data: Partial, ): Promise { - await db.update(mirrors).set(data).where(eq(mirrors.mirrorId, mirrorId)); + await db.update(mirrors).set(data).where(eq(mirrors.mirrorId, mirrorId)); } export async function createMirror(data: NewMirror): Promise { - const entities = await db.insert(mirrors).values(data).returning(); - return entities[0]; + const entities = await db.insert(mirrors).values(data).returning(); + return entities[0]; } diff --git a/server/src/database/models/requests.ts b/server/src/database/models/requests.ts index 5ff7ce8..e98ddd9 100644 --- a/server/src/database/models/requests.ts +++ b/server/src/database/models/requests.ts @@ -1,59 +1,59 @@ -import { and, count, eq, gte, inArray, sql } from 'drizzle-orm'; -import { db } from '../client'; -import { NewRequest, Request, requests } from '../schema'; -import { HttpStatusCode } from 'axios'; -import { unionAll } from 'drizzle-orm/pg-core'; -import { CasingCache } from 'drizzle-orm/casing'; +import type { HttpStatusCode } from "axios"; +import { and, count, eq, gte, inArray, sql } from "drizzle-orm"; + +import { db } from "../client"; +import type { NewRequest, Request } from "../schema"; +import { requests } from "../schema"; export async function getRequestsCount( - baseUrl: string, - createdAfter?: number, - statusCodes?: HttpStatusCode[], + baseUrl: string, + createdAfter?: number, + statusCodes?: HttpStatusCode[], ) { - const entities = await db - .select({ count: count() }) - .from(requests) - .where( - and( - eq(requests.baseUrl, baseUrl), - createdAfter - ? gte( - sql`cast(${requests.createdAt} as timestamp)`, - sql`cast(${new Date(createdAfter)} as timestamp)`, - ) - : undefined, - statusCodes ? inArray(requests.status, statusCodes) : undefined, - ), - ); + const entities = await db + .select({ count: count() }) + .from(requests) + .where( + and( + eq(requests.baseUrl, baseUrl), + createdAfter + ? gte( + sql`cast(${requests.createdAt} as timestamp)`, + sql`cast(${new Date(createdAfter)} as timestamp)`, + ) + : undefined, + statusCodes ? inArray(requests.status, statusCodes) : undefined, + ), + ); - if (entities.length <= 0) { - return 0; - } + if (entities.length <= 0) { + return 0; + } - return entities[0].count; + return entities[0].count; } export async function getMirrorsRequestsCountForStats( - dataRequests: { - baseUrl: string; - createdAfter: string | null; - statusCodes?: HttpStatusCode[]; - }[], + dataRequests: Array<{ + baseUrl: string; + createdAfter: string | null; + statusCodes?: HttpStatusCode[]; + }>, ) { - const values = dataRequests - .map( - (_, i) => + const values = dataRequests + .map( + (_, i) => `('${dataRequests[i].baseUrl}', ${dataRequests[i].createdAfter ? `'${dataRequests[i].createdAfter}'` : null}, ${dataRequests[i].statusCodes && (dataRequests[i].statusCodes?.length ?? 0) > 0 ? `ARRAY[${dataRequests[i].statusCodes}]` : null})`, - ) - .join(', '); + ) + .join(", "); - const entities = await db - .execute<{ - name: string; - createdafter: string | null; - statuscodes: HttpStatusCode[] | null; - count: number; - }>( + const entities = await db + .execute<{ + name: string; + createdafter: string | null; + statuscodes: HttpStatusCode[] | null; + count: number; + }>( ` WITH request_params (baseUrl, createdAfter, statusCodes) AS ( VALUES ${values} @@ -77,34 +77,34 @@ export async function getMirrorsRequestsCountForStats( GROUP BY rp.baseUrl, rp.createdAfter, rp.statusCodes ORDER BY rp.baseUrl, rp.createdAfter `, - ) - .then((result) => result.rows); + ) + .then(result => result.rows); - return entities; + return entities; } export async function getRequestsByBaseUrl( - baseUrl: string, - createdAfter: number, + baseUrl: string, + createdAfter: number, ): Promise { - const entities = await db - .select() - .from(requests) - .where( - and( - eq(requests.baseUrl, baseUrl), - createdAfter - ? gte( - sql`cast(${requests.createdAt} as timestamp)`, - sql`cast(${new Date(createdAfter)} as timestamp)`, - ) - : undefined, - ), - ); - return entities ?? []; + const entities = await db + .select() + .from(requests) + .where( + and( + eq(requests.baseUrl, baseUrl), + createdAfter + ? gte( + sql`cast(${requests.createdAt} as timestamp)`, + sql`cast(${new Date(createdAfter)} as timestamp)`, + ) + : undefined, + ), + ); + return entities ?? []; } export async function createRequest(data: NewRequest): Promise { - const entities = await db.insert(requests).values(data).returning(); - return entities[0]; + const entities = await db.insert(requests).values(data).returning(); + return entities[0]; } diff --git a/server/src/database/schema.ts b/server/src/database/schema.ts index 9062ff3..f834ca5 100644 --- a/server/src/database/schema.ts +++ b/server/src/database/schema.ts @@ -1,160 +1,160 @@ -import { sql } from 'drizzle-orm'; +import { sql } from "drizzle-orm"; import { - pgTable, - text, - serial, - integer, - real, - boolean, - primaryKey, -} from 'drizzle-orm/pg-core'; + boolean, + integer, + pgTable, + primaryKey, + real, + serial, + text, +} from "drizzle-orm/pg-core"; const timestamps = { - updatedAt: text('updated_at') - .default(sql`now()::timestamp without time zone`) - .notNull(), - createdAt: text('created_at') - .default(sql`now()::timestamp without time zone`) - .notNull(), - deletedAt: text('deleted_at'), + updatedAt: text("updated_at") + .default(sql`now()::timestamp without time zone`) + .notNull(), + createdAt: text("created_at") + .default(sql`now()::timestamp without time zone`) + .notNull(), + deletedAt: text("deleted_at"), }; const validTimestamps = { - validUntil: text('valid_until').notNull(), + validUntil: text("valid_until").notNull(), }; -export const mirrors = pgTable('mirrors', { - mirrorId: serial('mirror_id').primaryKey(), - url: text('url').notNull(), - ...timestamps, +export const mirrors = pgTable("mirrors", { + mirrorId: serial("mirror_id").primaryKey(), + url: text("url").notNull(), + ...timestamps, }); -export const requests = pgTable('requests', { - requestId: serial('request_id').primaryKey(), - baseUrl: text('base_url').notNull(), - url: text('endpoint').notNull(), - method: text('method').notNull(), - contentType: text('content_type'), - downloadSpeed: integer('download_speed'), - status: integer('status').notNull(), - latency: integer('latency'), - data: text('data'), - ...timestamps, +export const requests = pgTable("requests", { + requestId: serial("request_id").primaryKey(), + baseUrl: text("base_url").notNull(), + url: text("endpoint").notNull(), + method: text("method").notNull(), + contentType: text("content_type"), + downloadSpeed: integer("download_speed"), + status: integer("status").notNull(), + latency: integer("latency"), + data: text("data"), + ...timestamps, }); -export const beatmapsets = pgTable('beatmapsets', { - id: integer('id').primaryKey(), - artist: text('artist').notNull(), - artist_unicode: text('artist_unicode').notNull(), - creator: text('creator').notNull(), - source: text('source').notNull(), - tags: text('tags').notNull(), - title: text('title').notNull(), - title_unicode: text('title_unicode').notNull(), - /** JSON */ - covers: text('covers').notNull(), - favourite_count: integer('favourite_count').notNull(), - hype: text('hype'), - nsfw: boolean('nsfw').notNull(), - offset: integer('offset').notNull(), - play_count: integer('play_count').notNull(), - preview_url: text('preview_url').notNull(), - spotlight: boolean('spotlight').notNull(), - status: text('status').notNull(), - track_id: integer('track_id'), - user_id: integer('user_id').notNull(), - video: boolean('video').notNull(), - bpm: real('bpm'), - can_be_hyped: boolean('can_be_hyped').notNull(), - deleted_at: text('deleted_at'), - discussion_enabled: boolean('discussion_enabled').notNull(), - discussion_locked: boolean('discussion_locked').notNull(), - is_scoreable: boolean('is_scoreable').notNull(), - last_updated: text('last_updated').notNull(), - legacy_thread_url: text('legacy_thread_url'), - /** JSON */ - nominations_summary: text('nominations_summary'), - ranked: integer('ranked').notNull(), - ranked_date: text('ranked_date'), - storyboard: boolean('storyboard').notNull(), - submitted_date: text('submitted_date'), - /** JSON */ - availability: text('availability'), - has_favourited: boolean('has_favourited'), - /** JSON */ - current_nominations: text('current_nominations'), - /** JSON */ - description: text('description'), - /** JSON */ - genre: text('genre'), - /** JSON */ - langauge: text('langauge'), - pack_tags: text('pack_tags') - .array() - .default(sql`'{}'::text[]`), - ratings: integer('ratings') - .array() - .default(sql`'{}'::int[]`), - /** JSON */ - related_users: text('related_users'), - /** JSON */ - user: text('user'), - ...validTimestamps, +export const beatmapsets = pgTable("beatmapsets", { + id: integer("id").primaryKey(), + artist: text("artist").notNull(), + artist_unicode: text("artist_unicode").notNull(), + creator: text("creator").notNull(), + source: text("source").notNull(), + tags: text("tags").notNull(), + title: text("title").notNull(), + title_unicode: text("title_unicode").notNull(), + /** JSON */ + covers: text("covers").notNull(), + favourite_count: integer("favourite_count").notNull(), + hype: text("hype"), + nsfw: boolean("nsfw").notNull(), + offset: integer("offset").notNull(), + play_count: integer("play_count").notNull(), + preview_url: text("preview_url").notNull(), + spotlight: boolean("spotlight").notNull(), + status: text("status").notNull(), + track_id: integer("track_id"), + user_id: integer("user_id").notNull(), + video: boolean("video").notNull(), + bpm: real("bpm"), + can_be_hyped: boolean("can_be_hyped").notNull(), + deleted_at: text("deleted_at"), + discussion_enabled: boolean("discussion_enabled").notNull(), + discussion_locked: boolean("discussion_locked").notNull(), + is_scoreable: boolean("is_scoreable").notNull(), + last_updated: text("last_updated").notNull(), + legacy_thread_url: text("legacy_thread_url"), + /** JSON */ + nominations_summary: text("nominations_summary"), + ranked: integer("ranked").notNull(), + ranked_date: text("ranked_date"), + storyboard: boolean("storyboard").notNull(), + submitted_date: text("submitted_date"), + /** JSON */ + availability: text("availability"), + has_favourited: boolean("has_favourited"), + /** JSON */ + current_nominations: text("current_nominations"), + /** JSON */ + description: text("description"), + /** JSON */ + genre: text("genre"), + /** JSON */ + langauge: text("langauge"), + pack_tags: text("pack_tags") + .array() + .default(sql`'{}'::text[]`), + ratings: integer("ratings") + .array() + .default(sql`'{}'::int[]`), + /** JSON */ + related_users: text("related_users"), + /** JSON */ + user: text("user"), + ...validTimestamps, }); -export const beatmapsetsFiles = pgTable('beatmapsets_files', { - id: integer('id').primaryKey(), - noVideo: boolean('no_video').notNull(), - path: text('path').notNull(), - ...timestamps, - ...validTimestamps, +export const beatmapsetsFiles = pgTable("beatmapsets_files", { + id: integer("id").primaryKey(), + noVideo: boolean("no_video").notNull(), + path: text("path").notNull(), + ...timestamps, + ...validTimestamps, }); -export const beatmapOsuFiles = pgTable('beatmap_osu_files', { - id: integer('id').primaryKey(), - path: text('path').notNull(), - ...timestamps, - ...validTimestamps, +export const beatmapOsuFiles = pgTable("beatmap_osu_files", { + id: integer("id").primaryKey(), + path: text("path").notNull(), + ...timestamps, + ...validTimestamps, }); export const beatmaps = pgTable( - 'beatmaps', - { - beatmapset_id: integer('beatmapset_id').notNull(), - difficulty_rating: real('difficulty_rating').notNull(), - id: integer('id').notNull(), - mode: text('mode').notNull(), - status: text('status').notNull(), - total_length: integer('total_length').notNull(), - user_id: integer('user_id').notNull(), - version: text('version').notNull(), - accuracy: real('accuracy').notNull(), - ar: real('ar').notNull(), - bpm: real('bpm'), - convert: boolean('convert').notNull(), - count_circles: integer('count_circles').notNull(), - count_sliders: integer('count_sliders').notNull(), - count_spinners: integer('count_spinners').notNull(), - cs: real('cs').notNull(), - deleted_at: text('deleted_at'), - drain: real('drain').notNull(), - hit_length: integer('hit_length').notNull(), - is_scoreable: boolean('is_scoreable').notNull(), - last_updated: text('last_updated').notNull(), - mode_int: integer('mode_int').notNull(), - passcount: integer('passcount').notNull(), - playcount: integer('playcount').notNull(), - ranked: integer('ranked').notNull(), - url: text('url').notNull(), - checksum: text('checksum'), - /** JSON */ - failtimes: text('failtimes'), - max_combo: integer('max_combo'), - ...validTimestamps, - }, - (beatmaps) => ({ - pk: primaryKey({ columns: [beatmaps.id, beatmaps.mode] }), - }), + "beatmaps", + { + beatmapset_id: integer("beatmapset_id").notNull(), + difficulty_rating: real("difficulty_rating").notNull(), + id: integer("id").notNull(), + mode: text("mode").notNull(), + status: text("status").notNull(), + total_length: integer("total_length").notNull(), + user_id: integer("user_id").notNull(), + version: text("version").notNull(), + accuracy: real("accuracy").notNull(), + ar: real("ar").notNull(), + bpm: real("bpm"), + convert: boolean("convert").notNull(), + count_circles: integer("count_circles").notNull(), + count_sliders: integer("count_sliders").notNull(), + count_spinners: integer("count_spinners").notNull(), + cs: real("cs").notNull(), + deleted_at: text("deleted_at"), + drain: real("drain").notNull(), + hit_length: integer("hit_length").notNull(), + is_scoreable: boolean("is_scoreable").notNull(), + last_updated: text("last_updated").notNull(), + mode_int: integer("mode_int").notNull(), + passcount: integer("passcount").notNull(), + playcount: integer("playcount").notNull(), + ranked: integer("ranked").notNull(), + url: text("url").notNull(), + checksum: text("checksum"), + /** JSON */ + failtimes: text("failtimes"), + max_combo: integer("max_combo"), + ...validTimestamps, + }, + beatmaps => ({ + pk: primaryKey({ columns: [beatmaps.id, beatmaps.mode] }), + }), ); export type Beatmapset = typeof beatmapsets.$inferSelect; diff --git a/server/src/plugins/beatmapManager.ts b/server/src/plugins/beatmapManager.ts index b81e9d1..c311340 100644 --- a/server/src/plugins/beatmapManager.ts +++ b/server/src/plugins/beatmapManager.ts @@ -1,10 +1,11 @@ -import Elysia from 'elysia'; -import { BeatmapsManager } from '../core/managers/beatmaps/beatmaps.manager'; +import Elysia from "elysia"; + +import { BeatmapsManager } from "../core/managers/beatmaps/beatmaps.manager"; export const BeatmapsManagerInstance = new BeatmapsManager(); -export const BeatmapsManagerPlugin = new Elysia({ name: 'BeatmapsManager' }).decorate( - () => ({ - BeatmapsManagerInstance, - }), +export const BeatmapsManagerPlugin = new Elysia({ name: "BeatmapsManager" }).decorate( + () => ({ + BeatmapsManagerInstance, + }), ); diff --git a/server/src/plugins/calculatorManager.ts b/server/src/plugins/calculatorManager.ts index fd578cc..1777b1a 100644 --- a/server/src/plugins/calculatorManager.ts +++ b/server/src/plugins/calculatorManager.ts @@ -1,10 +1,11 @@ -import Elysia from 'elysia'; -import { CalculatorManager } from '../core/managers/calculator/calculator.manager'; +import Elysia from "elysia"; + +import { CalculatorManager } from "../core/managers/calculator/calculator.manager"; export const CalculatorManagerInstance = new CalculatorManager(); export const CalculatorManagerPlugin = new Elysia({ - name: 'CalculatorManager', + name: "CalculatorManager", }).decorate(() => ({ - CalculatorManagerInstance, + CalculatorManagerInstance, })); diff --git a/server/src/plugins/redisInstance.ts b/server/src/plugins/redisInstance.ts index cf3c011..5b8457d 100644 --- a/server/src/plugins/redisInstance.ts +++ b/server/src/plugins/redisInstance.ts @@ -1,14 +1,15 @@ -import Elysia from 'elysia'; -import Redis from 'ioredis'; -import config from '../config'; +import Elysia from "elysia"; +import Redis from "ioredis"; + +import config from "../config"; export const RedisInstance = new Redis({ - port: config.REDIS_PORT, - host: config.REDIS_HOST, + port: config.REDIS_PORT, + host: config.REDIS_HOST, }); export const RedisInstancePlugin = new Elysia({ - name: 'RedisInstance', + name: "RedisInstance", }).decorate(() => ({ - RedisInstance, + RedisInstance, })); diff --git a/server/src/plugins/statsService.ts b/server/src/plugins/statsService.ts index 5e937b8..ac2d603 100644 --- a/server/src/plugins/statsService.ts +++ b/server/src/plugins/statsService.ts @@ -1,10 +1,11 @@ -import Elysia from 'elysia'; -import { StatsService } from '../core/services/stats.service'; +import Elysia from "elysia"; + +import { StatsService } from "../core/services/stats.service"; export const StatsServiceInstance = new StatsService(); export const StatsServicePlugin = new Elysia({ - name: 'StatsService', + name: "StatsService", }).decorate(() => ({ - StatsServiceInstance, + StatsServiceInstance, })); diff --git a/server/src/setup.ts b/server/src/setup.ts index 1157d5a..f712fa9 100644 --- a/server/src/setup.ts +++ b/server/src/setup.ts @@ -1,116 +1,116 @@ -import { Elysia, StatusMap } from 'elysia'; -import { cors } from '@elysiajs/cors'; -import swagger, { ElysiaSwaggerConfig } from '@elysiajs/swagger'; -import { logger } from '@bogeychan/elysia-logger'; -import serverTiming from '@elysiajs/server-timing'; -import { autoload } from 'elysia-autoload'; -import { ip } from 'elysia-ip'; -import { rateLimit } from 'elysia-rate-limit'; -import config from './config'; -import { requestID } from 'elysia-requestid'; +import { logger } from "@bogeychan/elysia-logger"; +import { cors } from "@elysiajs/cors"; +import serverTiming from "@elysiajs/server-timing"; +import type { ElysiaSwaggerConfig } from "@elysiajs/swagger"; +import swagger from "@elysiajs/swagger"; +import { Elysia, StatusMap } from "elysia"; +import { autoload } from "elysia-autoload"; +import { ip } from "elysia-ip"; +import { rateLimit } from "elysia-rate-limit"; +import { requestID } from "elysia-requestid"; -const swaggerOptions: ElysiaSwaggerConfig<'/docs'> = { - documentation: { - info: { - title: Bun.env.npm_package_name ?? 'Observatory API', - version: Bun.env.npm_package_version ?? '1.0.0', - description: - Bun.env.npm_package_description ?? 'API for Observatory', - }, +import config from "./config"; + +const swaggerOptions: ElysiaSwaggerConfig<"/docs"> = { + documentation: { + info: { + title: Bun.env.npm_package_name ?? "Observatory API", + version: Bun.env.npm_package_version ?? "1.0.0", + description: + Bun.env.npm_package_description ?? "API for Observatory", }, - exclude: ['/'], - path: '/docs', + }, + exclude: ["/"], + path: "/docs", }; const loggerOptions = { - transport: { - targets: [ - ...(config.IsAutomatedTesting - ? [] - : [ - { - target: 'pino-pretty', - options: { - colorize: true, - }, - }, - ]), - ...(config.LOKI_HOST - ? [ - { - target: 'pino-loki', - options: { - batching: false, - labels: { - app: - process.env.npm_package_name || 'unknown', - namespace: - process.env.NODE_ENV || 'development', - }, - host: config.LOKI_HOST, - }, - }, - ] - : []), - ], - }, + transport: { + targets: [ + ...(config.IsAutomatedTesting + ? [] + : [ + { + target: "pino-pretty", + options: { + colorize: true, + }, + }, + ]), + ...(config.LOKI_HOST + ? [ + { + target: "pino-loki", + options: { + batching: false, + labels: { + app: + process.env.npm_package_name || "unknown", + namespace: + process.env.NODE_ENV || "development", + }, + host: config.LOKI_HOST, + }, + }, + ] + : []), + ], + }, }; async function setup() { - return new Elysia({ name: 'setup' }) - .use(cors()) - .use(ip()) - .use(requestID()) - .use( - rateLimit({ - max: config.RATELIMIT_CALLS_PER_WINDOW, - duration: config.RATELIMIT_TIME_WINDOW, - skip(req) { - const token = req.headers.get('Authorization'); - const validToken = config.IGNORE_RATELIMIT_KEY; + return new Elysia({ name: "setup" }) + .use(cors()) + .use(ip()) + .use(requestID()) + .use( + rateLimit({ + max: config.RATELIMIT_CALLS_PER_WINDOW, + duration: config.RATELIMIT_TIME_WINDOW, + skip(req) { + const token = req.headers.get("Authorization"); + const validToken = config.IGNORE_RATELIMIT_KEY; - if (validToken && token === validToken) { - return true; - } + if (validToken && token === validToken) { + return true; + } - return false; - }, - }), - ) - .use(serverTiming({ enabled: !config.IsProduction })) - .use( - logger({ - ...loggerOptions, - autoLogging: true, - customProps(ctx: any) { - const statusCode = (ctx.code ?? '') - .replace(/_/g, ' ') - .replace(/[A-Z]/g, (letter: string) => - letter.toLowerCase(), - ) - .replace(/(^\w{1})|(\s+\w{1})/g, (letter: string) => - letter.toUpperCase(), - ); + return false; + }, + }), + ) + .use(serverTiming({ enabled: !config.IsProduction })) + .use( + logger({ + ...loggerOptions, + autoLogging: true, + customProps(ctx: any) { + const statusCode = (ctx.code ?? "") + .replaceAll("_", " ") + .replaceAll(/[A-Z]/g, (letter: string) => + letter.toLowerCase()) + .replaceAll(/(^\w)|(\s+\w)/g, (letter: string) => + letter.toUpperCase()); - return { - params: ctx.params, - query: ctx.query, - requestId: ctx.set.headers['X-Request-ID'], - res: { - statusCode: + return { + params: ctx.params, + query: ctx.query, + requestId: ctx.set.headers["X-Request-ID"], + res: { + statusCode: StatusMap[ - statusCode as keyof typeof StatusMap - ] ?? - ctx.set.status ?? - 500, - }, - }; - }, - }), - ) - .use(await autoload({ dir: `${__dirname}/controllers` })) - .use(swagger(swaggerOptions)) - .get('/favicon.ico', () => Bun.file('./server/public/favicon.ico')); + statusCode as keyof typeof StatusMap + ] + ?? ctx.set.status + ?? 500, + }, + }; + }, + }), + ) + .use(await autoload({ dir: `${import.meta.dirname}/controllers` })) + .use(swagger(swaggerOptions)) + .get("/favicon.ico", () => Bun.file("./server/public/favicon.ico")); } export default setup; diff --git a/server/src/types/benchmark.ts b/server/src/types/benchmark.ts index 7e509b3..f18b8a8 100644 --- a/server/src/types/benchmark.ts +++ b/server/src/types/benchmark.ts @@ -1,4 +1,4 @@ export type BenchmarkResult = { - latency?: number; - downloadSpeed?: number; + latency?: number; + downloadSpeed?: number; }; diff --git a/server/src/types/general/beatmap.ts b/server/src/types/general/beatmap.ts index 352ba61..9dce5e9 100644 --- a/server/src/types/general/beatmap.ts +++ b/server/src/types/general/beatmap.ts @@ -1,159 +1,159 @@ -import { Failtimes } from './failtimes'; -import { GameMode, GameModeInt } from './gameMode'; -import { RankStatus, RankStatusInt } from './rankStatus'; -import { Timestamp } from './timestamp'; -import { User, UserCompact } from './user'; +import type { Failtimes } from "./failtimes"; +import type { GameMode, GameModeInt } from "./gameMode"; +import type { RankStatus, RankStatusInt } from "./rankStatus"; +import type { Timestamp } from "./timestamp"; +import type { UserCompact } from "./user"; export interface Beatmapset { - id: number; - artist: string; - artist_unicode: string; - /** Username of the mapper at the time of beatmapset creation. */ - creator: string; - source: string; - /** Can be an empty string. */ - tags: string; - title: string; - title_unicode: string; - covers: Covers; - favourite_count: number; - hype: BeatmapsetHype; - nsfw: boolean; - offset: number; - play_count: number; - preview_url: string; - spotlight: boolean; - status: RankStatus; - track_id?: number | null; - user_id: number; - video: boolean; - /** Float */ - bpm: number; - can_be_hyped: boolean; - deleted_at?: Timestamp | null; - discussion_enabled: boolean; - discussion_locked: boolean; - is_scoreable: boolean; - last_updated: Timestamp; - legacy_thread_url?: string; - nominations_summary: BeatmapsetNominationsSummary; - ranked: RankStatusInt; - ranked_date?: Timestamp | null; - storyboard: boolean; - submitted_date?: Timestamp; - availability: BeatmapsetAvailability; - has_favourited?: boolean; - beatmaps?: Beatmap[]; - converts?: Beatmap[]; - current_nominations?: unknown; - description?: BeatmapsetDescription; - genre?: BeatmapsetGenre; - langauge?: BeatmapsetLanguage; - pack_tags?: string[]; - ratings?: number[]; - related_users?: UserCompact[]; + id: number; + artist: string; + artist_unicode: string; + /** Username of the mapper at the time of beatmapset creation. */ + creator: string; + source: string; + /** Can be an empty string. */ + tags: string; + title: string; + title_unicode: string; + covers: Covers; + favourite_count: number; + hype: BeatmapsetHype; + nsfw: boolean; + offset: number; + play_count: number; + preview_url: string; + spotlight: boolean; + status: RankStatus; + track_id?: number | null; + user_id: number; + video: boolean; + /** Float */ + bpm: number; + can_be_hyped: boolean; + deleted_at?: Timestamp | null; + discussion_enabled: boolean; + discussion_locked: boolean; + is_scoreable: boolean; + last_updated: Timestamp; + legacy_thread_url?: string; + nominations_summary: BeatmapsetNominationsSummary; + ranked: RankStatusInt; + ranked_date?: Timestamp | null; + storyboard: boolean; + submitted_date?: Timestamp; + availability: BeatmapsetAvailability; + has_favourited?: boolean; + beatmaps?: Beatmap[]; + converts?: Beatmap[]; + current_nominations?: unknown; + description?: BeatmapsetDescription; + genre?: BeatmapsetGenre; + langauge?: BeatmapsetLanguage; + pack_tags?: string[]; + ratings?: number[]; + related_users?: UserCompact[]; - user?: UserCompact; + user?: UserCompact; - /** Not parsed values */ - related_tags: any; + /** Not parsed values */ + related_tags: any; } export interface Beatmap { - /** Integer */ - beatmapset_id: number; - /** Float. */ - difficulty_rating: number; - /** Integer */ - id: number; - mode: GameMode; - status: RankStatus; - /** Integer */ - total_length: number; - /** Integer */ - user_id: number; - version: string; - /** Float */ - accuracy: number; - /** Float */ - ar: number; - /** Float */ - bpm?: number; - convert: boolean; - /** Integer */ - count_circles: number; - /** Integer */ - count_sliders: number; - /** Integer */ - count_spinners: number; - /** Float */ - cs: number; - deleted_at?: Timestamp | null; - /** Float */ - drain: number; - /** Integer */ - hit_length: number; - is_scoreable: boolean; - last_updated: Timestamp; - /** Integer */ - mode_int: GameModeInt; - /** Integer */ - passcount: number; - /** Integer */ - playcount: number; - ranked: RankStatusInt; - url: string; - checksum?: string; - failtimes?: Failtimes; - /** Integer */ - max_combo?: number; + /** Integer */ + beatmapset_id: number; + /** Float. */ + difficulty_rating: number; + /** Integer */ + id: number; + mode: GameMode; + status: RankStatus; + /** Integer */ + total_length: number; + /** Integer */ + user_id: number; + version: string; + /** Float */ + accuracy: number; + /** Float */ + ar: number; + /** Float */ + bpm?: number; + convert: boolean; + /** Integer */ + count_circles: number; + /** Integer */ + count_sliders: number; + /** Integer */ + count_spinners: number; + /** Float */ + cs: number; + deleted_at?: Timestamp | null; + /** Float */ + drain: number; + /** Integer */ + hit_length: number; + is_scoreable: boolean; + last_updated: Timestamp; + /** Integer */ + mode_int: GameModeInt; + /** Integer */ + passcount: number; + /** Integer */ + playcount: number; + ranked: RankStatusInt; + url: string; + checksum?: string; + failtimes?: Failtimes; + /** Integer */ + max_combo?: number; - /** Not parsed values */ - owners?: any; - current_user_tag_ids?: any; - top_tag_ids?: any; + /** Not parsed values */ + owners?: any; + current_user_tag_ids?: any; + top_tag_ids?: any; } export interface Covers { - card: string; - 'card@2x': string; - cover: string; - 'cover@2x': string; - list: string; - 'list@2x': string; - slimcover: string; - 'slimcover@2x': string; + "card": string; + "card@2x": string; + "cover": string; + "cover@2x": string; + "list": string; + "list@2x": string; + "slimcover": string; + "slimcover@2x": string; } export interface BeatmapsetDescription { - description: string; + description: string; } export interface BeatmapsetGenre { - id?: number; - name: string; + id?: number; + name: string; } export interface BeatmapsetLanguage { - id: number; - name: string; + id: number; + name: string; } export interface BeatmapsetAvailability { - download_disabled: boolean; - more_information?: string | null; + download_disabled: boolean; + more_information?: string | null; } export interface BeatmapsetHype { - /** Integer */ - current?: number; - /** Integer */ - required?: number; + /** Integer */ + current?: number; + /** Integer */ + required?: number; } export interface BeatmapsetNominationsSummary { - /** Integer */ - current?: number; - /** Integer */ - required?: number; + /** Integer */ + current?: number; + /** Integer */ + required?: number; } diff --git a/server/src/types/general/failtimes.ts b/server/src/types/general/failtimes.ts index ea7ba0a..c099faf 100644 --- a/server/src/types/general/failtimes.ts +++ b/server/src/types/general/failtimes.ts @@ -4,8 +4,8 @@ * Https://osu.ppy.sh/docs/index.html#beatmapcompact-failtimes. */ export interface Failtimes { - /** Array of length 100. */ - exit?: number[]; - /** Array of length 100. */ - fail?: number[]; + /** Array of length 100. */ + exit?: number[]; + /** Array of length 100. */ + fail?: number[]; } diff --git a/server/src/types/general/gameMod.ts b/server/src/types/general/gameMod.ts index 054548c..430ffb5 100644 --- a/server/src/types/general/gameMod.ts +++ b/server/src/types/general/gameMod.ts @@ -1,34 +1,34 @@ export enum GameModBitwise { - NoMod = 0, - NoFail = 1 << 0, - Easy = 1 << 1, - TouchDevice = 1 << 2, - Hidden = 1 << 3, - HardRock = 1 << 4, - SuddenDeath = 1 << 5, - DoubleTime = 1 << 6, - Relax = 1 << 7, - HalfTime = 1 << 8, - Nightcore = 1 << 9, - Flashlight = 1 << 10, - Autoplay = 1 << 11, - SpunOut = 1 << 12, - Relax2 = 1 << 13, - Perfect = 1 << 14, - Key4 = 1 << 15, - Key5 = 1 << 16, - Key6 = 1 << 17, - Key7 = 1 << 18, - Key8 = 1 << 19, - FadeIn = 1 << 20, - Random = 1 << 21, - Cinema = 1 << 22, - Target = 1 << 23, - Key9 = 1 << 24, - Key10 = 1 << 25, - Key1 = 1 << 26, - Key3 = 1 << 27, - Key2 = 1 << 28, - ScoreV2 = 1 << 29, - Mirror = 1 << 30, + NoMod = 0, + NoFail = 1 << 0, + Easy = 1 << 1, + TouchDevice = 1 << 2, + Hidden = 1 << 3, + HardRock = 1 << 4, + SuddenDeath = 1 << 5, + DoubleTime = 1 << 6, + Relax = 1 << 7, + HalfTime = 1 << 8, + Nightcore = 1 << 9, + Flashlight = 1 << 10, + Autoplay = 1 << 11, + SpunOut = 1 << 12, + Relax2 = 1 << 13, + Perfect = 1 << 14, + Key4 = 1 << 15, + Key5 = 1 << 16, + Key6 = 1 << 17, + Key7 = 1 << 18, + Key8 = 1 << 19, + FadeIn = 1 << 20, + Random = 1 << 21, + Cinema = 1 << 22, + Target = 1 << 23, + Key9 = 1 << 24, + Key10 = 1 << 25, + Key1 = 1 << 26, + Key3 = 1 << 27, + Key2 = 1 << 28, + ScoreV2 = 1 << 29, + Mirror = 1 << 30, } diff --git a/server/src/types/general/gameMode.ts b/server/src/types/general/gameMode.ts index e5254e4..dea2b32 100644 --- a/server/src/types/general/gameMode.ts +++ b/server/src/types/general/gameMode.ts @@ -4,14 +4,14 @@ * ([Source](https://osu.ppy.sh/docs/index.html#gamemode)) */ export enum GameMode { - /** Osu!catch */ - OSU_CATCH = 'fruits', - /** Osu!mania */ - OSU_MANIA = 'mania', - /** Osu!standard */ - OSU_STANDARD = 'osu', - /** Osu!taiko */ - OSU_TAIKO = 'taiko', + /** Osu!catch */ + OSU_CATCH = "fruits", + /** Osu!mania */ + OSU_MANIA = "mania", + /** Osu!standard */ + OSU_STANDARD = "osu", + /** Osu!taiko */ + OSU_TAIKO = "taiko", } /** @@ -20,12 +20,12 @@ export enum GameMode { * ([Undocumented but can be found in Score](https://osu.ppy.sh/docs/index.html#score)) */ export enum GameModeInt { - /** Osu!catch */ - OSU_CATCH = 2, - /** Osu!mania */ - OSU_MANIA = 1, - /** Osu!standard */ - OSU_STANDARD = 0, - /** Osu!taiko */ - OSU_TAIKO = 3, + /** Osu!catch */ + OSU_CATCH = 2, + /** Osu!mania */ + OSU_MANIA = 1, + /** Osu!standard */ + OSU_STANDARD = 0, + /** Osu!taiko */ + OSU_TAIKO = 3, } diff --git a/server/src/types/general/rankStatus.ts b/server/src/types/general/rankStatus.ts index b17dc64..2e55004 100644 --- a/server/src/types/general/rankStatus.ts +++ b/server/src/types/general/rankStatus.ts @@ -6,41 +6,41 @@ * ([Source for information about leaderboards](https://osu.ppy.sh/wiki/en/Beatmap/Category#present-categories)) */ export enum RankStatus { - /** + /** * They allow players to compete on leaderboards and gain performance points * from setting scores. */ - APPROVED = 'approved', - /** + APPROVED = "approved", + /** * These beatmaps do not have leaderboards, but they can still be downloaded * and played, and they continue to contribute to play statistics. */ - GRAVEYARD = 'graveyard', - /** + GRAVEYARD = "graveyard", + /** * They have leaderboards, but no performance points will be awarded and all * scores will be deleted if it moves out of Loved. */ - LOVED = 'loved', - /** + LOVED = "loved", + /** * Work in Progress and Pending beatmaps do not have leaderboards, but * contribute to play statistics. */ - PENDING = 'pending', - /** + PENDING = "pending", + /** * Qualified beatmaps have leaderboards, but no performance points will be * awarded and all scores will be deleted when it moves out of Qualified. */ - QUALIFIED = 'qualified', - /** + QUALIFIED = "qualified", + /** * They allow players to compete on leaderboards and gain performance points * from setting scores. */ - RANKED = 'ranked', - /** + RANKED = "ranked", + /** * Work in Progress and Pending beatmaps do not have leaderboards, but * contribute to play statistics. */ - WIP = 'wip', + WIP = "wip", } /** @@ -51,39 +51,39 @@ export enum RankStatus { * ([Source for information about leaderboards](https://osu.ppy.sh/wiki/en/Beatmap/Category#present-categories)) */ export enum RankStatusInt { - /** + /** * These beatmaps do not have leaderboards, but they can still be downloaded * and played, and they continue to contribute to play statistics. */ - GRAVEYARD = -2, - /** + GRAVEYARD = -2, + /** * Work in Progress and Pending beatmaps do not have leaderboards, but * contribute to play statistics. */ - WIP = -1, - /** + WIP = -1, + /** * Work in Progress and Pending beatmaps do not have leaderboards, but * contribute to play statistics. */ - PENDING = 0, - /** + PENDING = 0, + /** * They allow players to compete on leaderboards and gain performance points * from setting scores. */ - RANKED = 1, - /** + RANKED = 1, + /** * They allow players to compete on leaderboards and gain performance points * from setting scores. */ - APPROVED = 2, - /** + APPROVED = 2, + /** * Qualified beatmaps have leaderboards, but no performance points will be * awarded and all scores will be deleted when it moves out of Qualified. */ - QUALIFIED = 3, - /** + QUALIFIED = 3, + /** * They have leaderboards, but no performance points will be awarded and all * scores will be deleted if it moves out of Loved. */ - LOVED = 4, + LOVED = 4, } diff --git a/server/src/types/general/user.ts b/server/src/types/general/user.ts index dd98686..880da97 100644 --- a/server/src/types/general/user.ts +++ b/server/src/types/general/user.ts @@ -1,45 +1,44 @@ -import { GameMode } from './gameMode'; - -import type { ColorCode } from './colorCode'; -import type { Timestamp } from './timestamp'; +import type { ColorCode } from "./colorCode"; +import type { GameMode } from "./gameMode"; +import type { Timestamp } from "./timestamp"; export interface UserCompactCover { - custom_url: null | unknown; - id: null | string; - url: string; + custom_url: null | unknown; + id: null | string; + url: string; } /** * ([Source](https://osu.ppy.sh/docs/index.html#user-profilepage)) */ export enum ProfilePage { - BEATMAPS = 'beatmaps', - HISTORICAL = 'historical', - KUDOSU = 'kudosu', - ME = 'me', - MEDALS = 'medals', - RECENT_ACTIVITY = 'recent_activity', - TOP_RANKS = 'top_ranks', + BEATMAPS = "beatmaps", + HISTORICAL = "historical", + KUDOSU = "kudosu", + ME = "me", + MEDALS = "medals", + RECENT_ACTIVITY = "recent_activity", + TOP_RANKS = "top_ranks", } export interface UserStatisticsRulesets { - fruits: UserStatistics; - mania: UserStatistics; - osu: UserStatistics; - taiko: UserStatistics; + fruits: UserStatistics; + mania: UserStatistics; + osu: UserStatistics; + taiko: UserStatistics; } export interface UserMonthlyPlaycount { - count: number; - start_date: Timestamp; + count: number; + start_date: Timestamp; } /** * ([Source](https://osu.ppy.sh/docs/index.html#group)) */ export interface GroupDescription { - html: string; - markdown: string; + html: string; + markdown: string; } /** @@ -49,163 +48,163 @@ export interface GroupDescription { * ([Source](https://osu.ppy.sh/docs/index.html#group)) */ export interface Group { - colour?: ColorCode; - /** Whether this group displays a listing at `/groups/{id}`. */ - /** + colour?: ColorCode; + /** Whether this group displays a listing at `/groups/{id}`. */ + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. */ - description?: GroupDescription; - has_listing: boolean; - /** + description?: GroupDescription; + has_listing: boolean; + /** Whether this group associates GameModes with users' memberships. id number. */ - has_playmodes: boolean; - id: number; - /** + has_playmodes: boolean; + id: number; + /** * Unique string to identify the group. * * @example bng */ - identifier: string; - /** Whether members of this group are considered probationary. */ - is_probationary: boolean; - /** + identifier: string; + /** Whether members of this group are considered probationary. */ + is_probationary: boolean; + /** * @example Beatmap Nominators */ - name: string; - /** + name: string; + /** * Short name of the group for display. * * @example BN */ - short_name: string; + short_name: string; } /** * ([Source](https://osu.ppy.sh/docs/index.html#usergroup)) */ export interface UserGroup extends Group { - /** + /** * {@link GameMode}s associated with this membership (`null` if `has_playmodes` is unset). */ - playmodes?: GameMode[] | null; + playmodes?: GameMode[] | null; } /** * ([Source(https://osu.ppy.sh/docs/index.html#usercompact-useraccounthistory)]) */ export enum UserAccountHistoryType { - NOTE = 'note', - RESTRICTION = 'restriction', - SILENCE = 'silence', + NOTE = "note", + RESTRICTION = "restriction", + SILENCE = "silence", } /** * ([Source(https://osu.ppy.sh/docs/index.html#usercompact-useraccounthistory)]) */ export interface UserAccountHistory { - description?: string; - id: number; - /** In seconds. */ - length: number; - permanent: boolean; - timestamp: Timestamp; - type: UserAccountHistoryType; + description?: string; + id: number; + /** In seconds. */ + length: number; + permanent: boolean; + timestamp: Timestamp; + type: UserAccountHistoryType; } /** * ([Source](https://osu.ppy.sh/docs/index.html#usercompact-profilebanner)) */ export interface UserProfileBanner { - id: number; - image: string; - tournament_id: number; + id: number; + image: string; + tournament_id: number; } /** * ([Source](https://osu.ppy.sh/docs/index.html#usercompact-userbadge)) */ export interface UserBadge { - awarded_at: Timestamp; - description: string; - image_url: string; - /** + awarded_at: Timestamp; + description: string; + image_url: string; + /** * Sometimes the URL can be an empty string. */ - url: string; + url: string; } export interface UserCompactCountry { - code: string; - name: string; + code: string; + name: string; } export interface UserStatisticsLevel { - current: number; - progress: number; + current: number; + progress: number; } export interface UserStatisticsGradeCounts { - a: number; - s: number; - sh: number; - ss: number; - ssh: number; + a: number; + s: number; + sh: number; + ss: number; + ssh: number; } /** * ([Source](https://osu.ppy.sh/docs/index.html#userstatistics)) */ export interface UserStatistics { - country_rank?: number; - global_rank?: number; - grade_counts: UserStatisticsGradeCounts; - hit_accuracy: number; - is_ranked: boolean; - level: UserStatisticsLevel; - maximum_combo: number; - play_count: number; - play_time: number; - pp: number; - ranked_score: number; - replays_watched_by_others: number; - total_hits: number; - total_score: number; - variants?: UserGameModeVariant[]; + country_rank?: number; + global_rank?: number; + grade_counts: UserStatisticsGradeCounts; + hit_accuracy: number; + is_ranked: boolean; + level: UserStatisticsLevel; + maximum_combo: number; + play_count: number; + play_time: number; + pp: number; + ranked_score: number; + replays_watched_by_others: number; + total_hits: number; + total_score: number; + variants?: UserGameModeVariant[]; } export interface UserAchievement { - achieved_at: Timestamp; - achievement_id: number; + achieved_at: Timestamp; + achievement_id: number; } export interface UserCompactPage { - /** + /** * The me page HTML content. * * Is an empty string if the user has no page. */ - html: string; - /** + html: string; + /** * The me page raw text content. * * Is an empty string if the user has no page. */ - raw: string; + raw: string; } export interface UserReplaysWatchedCount { - count: number; - start_date: Timestamp; + count: number; + start_date: Timestamp; } export enum Playstyle { - KEYBOARD = 'keyboard', - MOUSE = 'mouse', - TABLET = 'tablet', - TOUCH = 'touch', + KEYBOARD = "keyboard", + MOUSE = "mouse", + TABLET = "tablet", + TOUCH = "touch", } /** @@ -214,14 +213,14 @@ export enum Playstyle { * ([Source](https://osu.ppy.sh/docs/index.html#usercompact)) */ export interface UserCompact { - /** + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link UserEndpointGet} object. */ - account_history?: UserAccountHistory[]; - /** + account_history?: UserAccountHistory[]; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * @@ -229,122 +228,122 @@ export interface UserCompact { * * This is included in a {@link UserEndpointGet} object. */ - active_tournament_banner?: UserProfileBanner | null; - /** + active_tournament_banner?: UserProfileBanner | null; + /** * Url of user's avatar. */ - avatar_url: string; - /** + avatar_url: string; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link UserEndpointGet} object. */ - badges?: UserBadge[]; - /** + badges?: UserBadge[]; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link UserEndpointMe} object. */ - beatmap_playcounts_count?: number; - /** + beatmap_playcounts_count?: number; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. */ - blocks?: unknown; - /** + blocks?: unknown; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link UserEndpointGet} object. */ - comments_count?: number; - /** + comments_count?: number; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link User} object. */ - country?: UserCompactCountry; - /** + country?: UserCompactCountry; + /** * Two-letter code representing user's country. */ - country_code: string; - /** + country_code: string; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link User} object. */ - cover?: UserCompactCover; - /** + cover?: UserCompactCover; + /** * Identifier of the default group the user belongs to. */ - default_group: string; - /** + default_group: string; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link UserEndpointGet} object. */ - favourite_beatmapset_count?: number; - /** + favourite_beatmapset_count?: number; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link UserEndpointGet} object. */ - follower_count?: number; - /** + follower_count?: number; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. */ - friends?: unknown; - /** + friends?: unknown; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link UserEndpointGet} object. */ - graveyard_beatmapset_count?: number; - /** + graveyard_beatmapset_count?: number; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link UserEndpointGet} object. */ - groups?: UserGroup[]; - /** + groups?: UserGroup[]; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link UserEndpointGet} object. */ - guest_beatmapset_count?: number; - /** + guest_beatmapset_count?: number; + /** * Unique identifier for user. * * Note: Id can be null for deleted users on beatmapSets, beatmap id with this example: 6527 */ - id?: number; - /** + id?: number; + /** * Has this account been active in the last x months? */ - is_active: boolean; - /** + is_active: boolean; + /** * Is this a bot account? */ - is_bot: boolean; - /** + is_bot: boolean; + /** * Account was deleted. */ - is_deleted: boolean; - /** + is_deleted: boolean; + /** * Is the user currently online? (either on lazer or the new website). */ - is_online: boolean; - /** + is_online: boolean; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * @@ -352,40 +351,40 @@ export interface UserCompact { * * This is included in a {@link UserEndpointMe} object. */ - is_restricted?: boolean; - /** + is_restricted?: boolean; + /** * Does this user have supporter? */ - is_supporter: boolean; - /** + is_supporter: boolean; + /** * Last access time. * `null` if the user hides online presence. * * This is included in a {@link UserEndpointGet}/{@link UserEndpointSearchUser} object. */ - last_visit?: Timestamp | null; - /** + last_visit?: Timestamp | null; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link UserEndpointGet} object. */ - loved_beatmapset_count?: number; - /** + loved_beatmapset_count?: number; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link UserEndpointGet} object. */ - mapping_follower_count?: number; - /** + mapping_follower_count?: number; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link UserEndpointGet} object. */ - monthly_playcounts?: UserMonthlyPlaycount[]; - /** + monthly_playcounts?: UserMonthlyPlaycount[]; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * @@ -393,150 +392,150 @@ export interface UserCompact { * * This is included in a {@link UserEndpointGet} object. */ - page?: UserCompactPage; - /** + page?: UserCompactPage; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link UserEndpointGet} object. */ - pending_beatmapset_count?: number; - /** + pending_beatmapset_count?: number; + /** * Whether or not the user allows PM from other than friends. */ - pm_friends_only: boolean; - /** + pm_friends_only: boolean; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link UserEndpointGet} object. */ - previous_usernames?: string[]; - /** + previous_usernames?: string[]; + /** * Colour of username/profile highlight. * * This is included in a {@link UserEndpointGet}/{@link UserEndpointSearchUser} object. */ - profile_colour?: ColorCode | null; - /** + profile_colour?: ColorCode | null; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link UserEndpointGet} object. */ - rank_history?: UserRankHistory; - /** + rank_history?: UserRankHistory; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link UserEndpointGet} object. */ - ranked_beatmapset_count?: number; - /** + ranked_beatmapset_count?: number; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link UserEndpointGet} object. */ - replays_watched_counts?: UserReplaysWatchedCount[]; - /** + replays_watched_counts?: UserReplaysWatchedCount[]; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link UserEndpointGet} object. */ - scores_best_count?: number; - /** + scores_best_count?: number; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link UserEndpointGet} object. */ - scores_first_count?: number; - /** + scores_first_count?: number; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link UserEndpointGet} object. */ - scores_pinned_count?: number; - /** + scores_pinned_count?: number; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link UserEndpointGet} object. */ - scores_recent_count?: number; - /** + scores_recent_count?: number; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link UserEndpointGet} object. */ - statistics?: UserStatistics; - /** + statistics?: UserStatistics; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link UserEndpointMe} object. */ - statistics_rulesets: UserStatisticsRulesets; - /** + statistics_rulesets: UserStatisticsRulesets; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link UserEndpointGet} object. */ - support_level?: number; - /** + support_level?: number; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. */ - unread_pm_count?: unknown; - /** + unread_pm_count?: unknown; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. * * This is included in a {@link UserEndpointGet} object. */ - user_achievements?: UserAchievement[]; - /** + user_achievements?: UserAchievement[]; + /** * May be additionally included in the response. * Relevant endpoints should list them if applicable. */ - user_preferences?: unknown; - /** + user_preferences?: unknown; + /** * User's display name. */ - username: string; + username: string; } export interface UserRankHistory { - data: number[]; - mode: GameMode; + data: number[]; + mode: GameMode; } export interface UserCompactKusodo { - available: number; - total: number; + available: number; + total: number; } export interface UserCompactCountry { - code: string; - name: string; + code: string; + name: string; } export enum GameModeVariant { - MANIA_4K = '4k', - MANIA_7K = '7k', + MANIA_4K = "4k", + MANIA_7K = "7k", } export interface UserGameModeVariant { - country_rank: number | null; - global_rank: number | null; - mode: GameMode; - pp: number; - variant: GameModeVariant; + country_rank: number | null; + global_rank: number | null; + mode: GameMode; + pp: number; + variant: GameModeVariant; } /** @@ -551,42 +550,42 @@ export interface UserGameModeVariant { * [[include:example_output/users_get_Ooi.md]] */ export interface UserEndpointGet extends User { - account_history: UserAccountHistory[]; - active_tournament_banner: UserProfileBanner | null; - badges: UserBadge[]; - beatmap_playcounts_count: number; - comments_count: number; - discord: string | null; - favourite_beatmapset_count: number; - follower_count: number; - graveyard_beatmapset_count: number; - groups: UserGroup[]; - guest_beatmapset_count: number; - interests: string | null; - last_visit: Timestamp | null; - location: string | null; - loved_beatmapset_count: number; - mapping_follower_count: number; - monthly_playcounts: UserMonthlyPlaycount[]; - occupation: string | null; - page: UserCompactPage; - pending_beatmapset_count: number; - previous_usernames: string[]; - profile_colour: ColorCode | null; - rank_history: UserRankHistory; - ranked_beatmapset_count: number; - replays_watched_counts: UserReplaysWatchedCount[]; - scores_best_count: number; - scores_first_count: number; - scores_pinned_count: number; - scores_recent_count: number; - statistics: UserStatistics; - support_level: number; - title: string | null; - title_url: string | null; - twitter: string | null; - user_achievements: UserAchievement[]; - website: string | null; + account_history: UserAccountHistory[]; + active_tournament_banner: UserProfileBanner | null; + badges: UserBadge[]; + beatmap_playcounts_count: number; + comments_count: number; + discord: string | null; + favourite_beatmapset_count: number; + follower_count: number; + graveyard_beatmapset_count: number; + groups: UserGroup[]; + guest_beatmapset_count: number; + interests: string | null; + last_visit: Timestamp | null; + location: string | null; + loved_beatmapset_count: number; + mapping_follower_count: number; + monthly_playcounts: UserMonthlyPlaycount[]; + occupation: string | null; + page: UserCompactPage; + pending_beatmapset_count: number; + previous_usernames: string[]; + profile_colour: ColorCode | null; + rank_history: UserRankHistory; + ranked_beatmapset_count: number; + replays_watched_counts: UserReplaysWatchedCount[]; + scores_best_count: number; + scores_first_count: number; + scores_pinned_count: number; + scores_recent_count: number; + statistics: UserStatistics; + support_level: number; + title: string | null; + title_url: string | null; + twitter: string | null; + user_achievements: UserAchievement[]; + website: string | null; } /** @@ -600,8 +599,8 @@ export interface UserEndpointGet extends User { * [[include:example_output/users_me_nothing.md]] */ export interface UserEndpointMe extends UserEndpointGet { - is_restricted: boolean; - statistics_rulesets: UserStatisticsRulesets; + is_restricted: boolean; + statistics_rulesets: UserStatisticsRulesets; } /** @@ -613,8 +612,8 @@ export interface UserEndpointMe extends UserEndpointGet { * For type safety just treat this object like a {@link UserCompact} object. */ export interface UserEndpointSearchUser extends UserCompact { - last_visit: Timestamp | null; - profile_colour: ColorCode | null; + last_visit: Timestamp | null; + profile_colour: ColorCode | null; } /** @@ -628,9 +627,9 @@ export interface UserEndpointSearchUser extends UserCompact { * ([Source](https://osu.ppy.sh/docs/index.html#user)) */ export interface User extends UserCompact { - country: UserCompactCountry; - cover: UserCompactCover; - /** + country: UserCompactCountry; + cover: UserCompactCover; + /** * Discord name. * `null` if the user has it not set in the settings. * @@ -641,12 +640,12 @@ export interface User extends UserCompact { * * This is included in a {@link UserEndpointGet} object. */ - discord?: string | null; - /** + discord?: string | null; + /** * Whether or not ever being a supporter in the past. */ - has_supported: boolean; - /** + has_supported: boolean; + /** * Interests string. * `null` if the user has it not set in the settings. * @@ -657,17 +656,17 @@ export interface User extends UserCompact { * * This is included in a {@link UserEndpointGet} object. */ - interests?: string | null; - is_restricted?: boolean; - /** + interests?: string | null; + is_restricted?: boolean; + /** * Date since when the user account exists. */ - join_date: Timestamp; - /** + join_date: Timestamp; + /** * https://osu.ppy.sh/wiki/en/Modding/Kudosu */ - kudosu: UserCompactKusodo; - /** + kudosu: UserCompactKusodo; + /** * Location string. * `null` if the user has it not set in the settings. * @@ -678,16 +677,16 @@ export interface User extends UserCompact { * * This is included in a {@link UserEndpointGet} object. */ - location?: string | null; - /** + location?: string | null; + /** * Maximum number of users allowed to be blocked. */ - max_blocks: number; - /** + max_blocks: number; + /** * Maximum number of friends allowed to be added. */ - max_friends: number; - /** + max_friends: number; + /** * Occupation string. * `null` if the user has it not set in the settings. * @@ -698,27 +697,27 @@ export interface User extends UserCompact { * * This is included in a {@link UserEndpointGet} object. */ - occupation?: string | null; - /** + occupation?: string | null; + /** * The default game mode the user is playing. */ - playmode: GameMode; - /** + playmode: GameMode; + /** * Device choices of the user. * `null` if the user has it not set in the settings. */ - playstyle: Playstyle[] | null; - /** + playstyle: Playstyle[] | null; + /** * Number of forum posts. * * Integer */ - post_count: number; - /** + post_count: number; + /** * Ordered array of sections in user profile page. */ - profile_order: ProfilePage[]; - /** + profile_order: ProfilePage[]; + /** * User-specific title. * `null` if the user has no title. * @@ -729,8 +728,8 @@ export interface User extends UserCompact { * * This is included in a {@link UserEndpointGet} object. */ - title?: string | null; - /** + title?: string | null; + /** * User-specific title URL. * `null` if the user has no title URL. * @@ -741,8 +740,8 @@ export interface User extends UserCompact { * * This is included in a {@link UserEndpointGet} object. */ - title_url?: string | null; - /** + title_url?: string | null; + /** * Twitter name. * `null` if the user has it not set in the settings. * @@ -753,12 +752,12 @@ export interface User extends UserCompact { * * This is included in a {@link UserEndpointGet} object. */ - twitter?: string | null; - /** + twitter?: string | null; + /** * Exists for profiles that have mania as their default game mode. */ - variants?: UserGameModeVariant[]; - /** + variants?: UserGameModeVariant[]; + /** * Website string. * `null` if the user has it not set in the settings. * @@ -769,5 +768,5 @@ export interface User extends UserCompact { * * This is included in a {@link UserEndpointGet} object. */ - website?: string | null; + website?: string | null; } diff --git a/server/src/types/redis.ts b/server/src/types/redis.ts index bbb286e..59bbc71 100644 --- a/server/src/types/redis.ts +++ b/server/src/types/redis.ts @@ -1,9 +1,9 @@ export enum RedisKeys { - BEATMAP_BY_ID = 'BEATMAP:ID:', - BEATMAP_ID_BY_HASH = 'BEATMAP_ID:HASH:', - BEATMAPSET_BY_ID = 'BEATMAPSET:ID:', - BEATMAPSET_FILE_BY_ID = 'BEATMAPSET_FILE:ID:', - BEATMAP_OSU_FILE = 'BEATMAP_OSU_FILE:ID:', - DAILY_RATE_LIMIT = 'DAILY_RATE_LIMIT:DOMAIN:', - BEATMAPS_SEARCH_RESULT = 'BEATMAPS_SEARCH_RESULT:', + BEATMAP_BY_ID = "BEATMAP:ID:", + BEATMAP_ID_BY_HASH = "BEATMAP_ID:HASH:", + BEATMAPSET_BY_ID = "BEATMAPSET:ID:", + BEATMAPSET_FILE_BY_ID = "BEATMAPSET_FILE:ID:", + BEATMAP_OSU_FILE = "BEATMAP_OSU_FILE:ID:", + DAILY_RATE_LIMIT = "DAILY_RATE_LIMIT:DOMAIN:", + BEATMAPS_SEARCH_RESULT = "BEATMAPS_SEARCH_RESULT:", } diff --git a/server/src/types/stats.ts b/server/src/types/stats.ts index aae8362..72d89d7 100644 --- a/server/src/types/stats.ts +++ b/server/src/types/stats.ts @@ -1,8 +1,8 @@ export enum TimeRange { - Lifetime = 'lifetime', - Session = 'session', - Hour = 'hour', - Day = 'day', - Week = 'week', - Month = 'month', + Lifetime = "lifetime", + Session = "session", + Hour = "hour", + Day = "day", + Week = "week", + Month = "month", } diff --git a/server/src/types/utils.ts b/server/src/types/utils.ts index 511edc8..3b64e05 100644 --- a/server/src/types/utils.ts +++ b/server/src/types/utils.ts @@ -1,7 +1,7 @@ export type DeepPartial = { - [P in keyof T]?: T[P] extends Array - ? Array> - : T[P] extends ReadonlyArray - ? ReadonlyArray> - : DeepPartial; + [P in keyof T]?: T[P] extends Array + ? Array> + : T[P] extends ReadonlyArray + ? ReadonlyArray> + : DeepPartial; }; diff --git a/server/src/utils/array.ts b/server/src/utils/array.ts index 452fe23..516d1ab 100644 --- a/server/src/utils/array.ts +++ b/server/src/utils/array.ts @@ -1,16 +1,17 @@ export function splitByCondition( - array: T[], - condition: (item: T) => boolean, + array: T[], + condition: (item: T) => boolean, ): [T[], T[]] { - return array.reduce( - ([trueArr, falseArr], item) => { - if (condition(item)) { - trueArr.push(item); - } else { - falseArr.push(item); - } - return [trueArr, falseArr]; - }, - [[], []] as [T[], T[]], - ); -} \ No newline at end of file + return array.reduce( + ([trueArr, falseArr], item) => { + if (condition(item)) { + trueArr.push(item); + } + else { + falseArr.push(item); + } + return [trueArr, falseArr]; + }, + [[], []] as [T[], T[]], + ); +} diff --git a/server/src/utils/beatmap.ts b/server/src/utils/beatmap.ts index c711ba9..ad1ca28 100644 --- a/server/src/utils/beatmap.ts +++ b/server/src/utils/beatmap.ts @@ -1,7 +1,7 @@ -import * as rosu from 'rosu-pp-js'; +import * as rosu from "rosu-pp-js"; export function TryConvertToGamemode(value: any) { - return Object.values(rosu.GameMode).includes(value as rosu.GameMode) - ? (value as rosu.GameMode) - : undefined; + return Object.values(rosu.GameMode).includes(value as rosu.GameMode) + ? (value as rosu.GameMode) + : undefined; } diff --git a/server/src/utils/date.ts b/server/src/utils/date.ts index 29578d6..86353ba 100644 --- a/server/src/utils/date.ts +++ b/server/src/utils/date.ts @@ -1,4 +1,4 @@ export function getUTCDate(): Date { - const now = new Date(); - return new Date(now.getTime() + now.getTimezoneOffset() * 60000); + const now = new Date(); + return new Date(now.getTime() + now.getTimezoneOffset() * 60000); } diff --git a/server/src/utils/logger.ts b/server/src/utils/logger.ts index ce4a47d..3066dea 100644 --- a/server/src/utils/logger.ts +++ b/server/src/utils/logger.ts @@ -1,23 +1,24 @@ -import { createPinoLogger } from '@bogeychan/elysia-logger'; -import { loggerOptions } from '../setup'; -import config from '../config'; -import { AxiosResponseLog } from '../core/abstracts/api/base-api.types'; +import { createPinoLogger } from "@bogeychan/elysia-logger"; + +import config from "../config"; +import type { AxiosResponseLog } from "../core/abstracts/api/base-api.types"; +import { loggerOptions } from "../setup"; const logger = createPinoLogger({ - ...loggerOptions, + ...loggerOptions, }); -export const logExternalRequest = (data: AxiosResponseLog) => { - const level = - data.status < 400 ? 'info' : data.status < 500 ? 'warn' : 'error'; +export function logExternalRequest(data: AxiosResponseLog) { + const level + = data.status < 400 ? "info" : data.status < 500 ? "warn" : "error"; - logger[level]({ - axios: { - ...data, - data: config.IsDebug || level != 'info' ? data : undefined, - }, - }); -}; + logger[level]({ + axios: { + ...data, + data: config.IsDebug || level !== "info" ? data : undefined, + }, + }); +} // TODO: Add more loggers here diff --git a/server/src/utils/mirrors-stats.ts b/server/src/utils/mirrors-stats.ts index 5ed57a0..1bfa14e 100644 --- a/server/src/utils/mirrors-stats.ts +++ b/server/src/utils/mirrors-stats.ts @@ -1,67 +1,67 @@ -import { TimeRange } from '../types/stats'; -import { MirrorClient } from '../core/abstracts/client/base-client.types'; -import { getUTCDate } from './date'; +import type { MirrorClient } from "../core/abstracts/client/base-client.types"; +import { TimeRange } from "../types/stats"; +import { getUTCDate } from "./date"; -export const APPLICATION_START_TIME = - getUTCDate().getTime() - Bun.nanoseconds() / 1000000; +export const APPLICATION_START_TIME + = getUTCDate().getTime() - Bun.nanoseconds() / 1000000; const MINUTE = 1000 * 60; export const TIME_RANGES_FOR_MIRRORS_STATS = [ - { - time: null, - name: TimeRange.Lifetime, - }, - { - time: new Date(APPLICATION_START_TIME).toISOString(), - name: TimeRange.Session, - }, - { - time: new Date(APPLICATION_START_TIME - MINUTE * 60).toISOString(), - name: TimeRange.Hour, - }, - { - time: new Date(APPLICATION_START_TIME - MINUTE * 60 * 24).toISOString(), - name: TimeRange.Day, - }, - { - time: new Date( - APPLICATION_START_TIME - MINUTE * 60 * 24 * 7, - ).toISOString(), - name: TimeRange.Week, - }, - { - time: new Date( - APPLICATION_START_TIME - MINUTE * 60 * 24 * 30, - ).toISOString(), - name: TimeRange.Month, - }, + { + time: null, + name: TimeRange.Lifetime, + }, + { + time: new Date(APPLICATION_START_TIME).toISOString(), + name: TimeRange.Session, + }, + { + time: new Date(APPLICATION_START_TIME - MINUTE * 60).toISOString(), + name: TimeRange.Hour, + }, + { + time: new Date(APPLICATION_START_TIME - MINUTE * 60 * 24).toISOString(), + name: TimeRange.Day, + }, + { + time: new Date( + APPLICATION_START_TIME - MINUTE * 60 * 24 * 7, + ).toISOString(), + name: TimeRange.Week, + }, + { + time: new Date( + APPLICATION_START_TIME - MINUTE * 60 * 24 * 30, + ).toISOString(), + name: TimeRange.Month, + }, ]; const successfulStatusCodes = [200, 404]; const failedStatusCodes = [500, 502, 503, 504, 429]; export function getMirrorsRequestsQueryData(clients: MirrorClient[]) { - return clients - .flatMap((c) => { - return TIME_RANGES_FOR_MIRRORS_STATS.map(({ time }) => time).map( - (createdAfter) => [ - { - baseUrl: c.client.clientConfig.baseUrl, - createdAfter, - statusCodes: successfulStatusCodes, - }, - { - baseUrl: c.client.clientConfig.baseUrl, - createdAfter, - statusCodes: failedStatusCodes, - }, - { - baseUrl: c.client.clientConfig.baseUrl, - createdAfter, - }, - ], - ); - }) - .flat(); + return clients + .flatMap((c) => { + return TIME_RANGES_FOR_MIRRORS_STATS.map(({ time }) => time).map( + createdAfter => [ + { + baseUrl: c.client.clientConfig.baseUrl, + createdAfter, + statusCodes: successfulStatusCodes, + }, + { + baseUrl: c.client.clientConfig.baseUrl, + createdAfter, + statusCodes: failedStatusCodes, + }, + { + baseUrl: c.client.clientConfig.baseUrl, + createdAfter, + }, + ], + ); + }) + .flat(); } diff --git a/server/src/utils/stats.ts b/server/src/utils/stats.ts index e03d274..54d4923 100644 --- a/server/src/utils/stats.ts +++ b/server/src/utils/stats.ts @@ -1,24 +1,24 @@ -import path from 'path'; -import { readdir, stat } from 'fs/promises'; +import { readdir, stat } from "node:fs/promises"; +import path from "node:path"; export async function getDirectoryStats(directory: string) { - const files = await readdir(directory); - const stats = files.map((file) => stat(path.join(directory, file))); + const files = await readdir(directory); + const stats = files.map(file => stat(path.join(directory, file))); - const fileStats = await Promise.all(stats); + const fileStats = await Promise.all(stats); - const result = fileStats.reduce( - (accumulator, { size }) => { - accumulator.totalSize += size; - accumulator.fileCount += 1; - return accumulator; - }, - { totalSize: 0, fileCount: 0 }, - ); + const result = fileStats.reduce( + (accumulator, { size }) => { + accumulator.totalSize += size; + accumulator.fileCount += 1; + return accumulator; + }, + { totalSize: 0, fileCount: 0 }, + ); - return result; + return result; } export function bytesToHumanReadableMegabytes(bytes: number) { - return `${(bytes / 1024 / 1024).toFixed(2)} MB`; + return `${(bytes / 1024 / 1024).toFixed(2)} MB`; } diff --git a/server/tests/calculator.service.test.ts b/server/tests/calculator.service.test.ts index cf41adc..a53c64c 100644 --- a/server/tests/calculator.service.test.ts +++ b/server/tests/calculator.service.test.ts @@ -1,406 +1,406 @@ import { - afterEach, - beforeAll, - beforeEach, - describe, - expect, - it, -} from 'bun:test'; - -import * as rosu from 'rosu-pp-js'; -import { CalculatorService } from '../src/core/managers/calculator/calculator.service'; -import { GameModBitwise } from '../src/types/general/gameMod'; -import { TryConvertToGamemode } from '../src/utils/beatmap'; -import { - ScoreShort, - Score, -} from '../src/core/managers/calculator/calculator.types'; - -describe('Calculator tests', () => { - const calculatorService = new CalculatorService(); - - let beatmapBuffer = null! as ArrayBuffer; - let beatmap = null! as rosu.Beatmap; - - beforeAll(async () => { - beatmapBuffer = await Bun.file( + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, +} from "bun:test"; +import * as rosu from "rosu-pp-js"; + +import { CalculatorService } from "../src/core/managers/calculator/calculator.service"; +import type { + Score, + ScoreShort, +} from "../src/core/managers/calculator/calculator.types"; +import { GameModBitwise } from "../src/types/general/gameMod"; +import { TryConvertToGamemode } from "../src/utils/beatmap"; + +describe("Calculator tests", () => { + const calculatorService = new CalculatorService(); + + let beatmapBuffer = null! as ArrayBuffer; + let beatmap = null! as rosu.Beatmap; + + beforeAll(async () => { + beatmapBuffer = await Bun.file( `${import.meta.dir}/data/2809623.osu.test`, - ).arrayBuffer(); - }); - - beforeEach(() => { - beatmap = calculatorService.ConvertBufferToBeatmap(beatmapBuffer); - }); - - afterEach(() => { - beatmap.free(); - }); - - it('Valid beatmap mode should be parsed', async () => { - const beatmapMode = TryConvertToGamemode(3) ?? beatmap.mode; - - expect(beatmapMode).toBe(rosu.GameMode.Mania); - }); - - it('Invalid beatmap mode should be defaulted to std', async () => { - const beatmapMode = TryConvertToGamemode(5) ?? beatmap.mode; - - expect(beatmapMode).toBe(rosu.GameMode.Osu); - }); - - it('Should calculate multiple acc values', async () => { - const accuracies = [100, 99, 98, 95]; - - const scores: ScoreShort[] = accuracies.map((a) => { - return { accuracy: a, isLazer: false, isScoreFailed: false }; - }); - - const results = calculatorService.CalculateBeatmapPerfomance( - beatmap, - scores, - ); + ).arrayBuffer(); + }); - expect(results.length).toBe(accuracies.length); - }); - - it('Should apply DT mod', async () => { - const scores: ScoreShort[] = [ - { - accuracy: 100, - isLazer: false, - isScoreFailed: false, - }, - ]; - - const scoresWithDT = scores.map((s) => { - return { - ...s, - mods: GameModBitwise.DoubleTime, - }; - }); - - const perfomanceWithoutMod = - calculatorService.CalculateBeatmapPerfomance(beatmap, scores); - - const perfomanceWithMod = calculatorService.CalculateBeatmapPerfomance( - beatmap, - scoresWithDT, - ); - - expect(perfomanceWithMod[0].difficulty.stars).toBeGreaterThan( - perfomanceWithoutMod[0].difficulty.stars, - ); - }); + beforeEach(() => { + beatmap = calculatorService.ConvertBufferToBeatmap(beatmapBuffer); + }); - it('Should convert beatmap to another gamemode', async () => { - const scoresInStd: ScoreShort[] = [ - { - accuracy: 100, - mode: rosu.GameMode.Osu, - isLazer: false, - isScoreFailed: false, - }, - ]; - - const scoresInTaiko = scoresInStd.map((s) => { - return { - ...s, - mode: rosu.GameMode.Taiko, - }; - }); - - const perfomanceStandard = calculatorService.CalculateBeatmapPerfomance( - beatmap, - scoresInStd, - ); - - const perfomanceTaiko = calculatorService.CalculateBeatmapPerfomance( - beatmap, - scoresInTaiko, - ); - - expect(perfomanceTaiko[0].difficulty.isConvert).toBe(true); - expect(perfomanceStandard[0].difficulty.stars).not.toEqual( - perfomanceTaiko[0].difficulty.stars, - ); - }); + afterEach(() => { + beatmap.free(); + }); - it('Should convert score to another gamemode with mod convertion alteration', async () => { - const EXPECTED_PP = 138.709; // https://web.archive.org/web/20251102193104/https://osu.ppy.sh/scores/2248271767 - const EXPECTED_STARS = 4.04; // https://archive.org/details/discord-7xd-xe-m-4-ofb - - const score: Score = { - accuracy: 97.33, - combo: 850, - n300: 756, - nGeki: 1650, - n100: 3, - nKatu: 124, - n50: 2, - misses: 9, - mode: rosu.GameMode.Mania, - mods: GameModBitwise.DoubleTime | GameModBitwise.Key4, - isScoreFailed: false, - isLazer: false, - }; - - const result = calculatorService.CalculateScorePerfomance( - beatmap, - score, - ); - - expect(result.difficulty.isConvert).toBe(true); - - expect(result.pp).toBeWithin(EXPECTED_PP - 0.001, EXPECTED_PP + 0.001); - expect(result.difficulty.stars).toBeWithin( - EXPECTED_STARS - 0.01, - EXPECTED_STARS + 0.01, - ); - }); + it("Valid beatmap mode should be parsed", async () => { + const beatmapMode = TryConvertToGamemode(3) ?? beatmap.mode; - it('Should convert beatmap to another gamemode with mod convertion alteration', async () => { - const EXPECTED_STARS = 4.04; // https://archive.org/details/discord-7xd-xe-m-4-ofb - - const scores: ScoreShort[] = [ - { - accuracy: 100, - mode: rosu.GameMode.Mania, - mods: GameModBitwise.DoubleTime | GameModBitwise.Key4, - isLazer: false, - isScoreFailed: false, - }, - ]; - - const result = calculatorService.CalculateBeatmapPerfomance( - beatmap, - scores, - ); - - expect(result[0].difficulty.isConvert).toBe(true); - - expect(result[0].difficulty.stars).toBeWithin( - EXPECTED_STARS - 0.01, - EXPECTED_STARS + 0.01, - ); - }); + expect(beatmapMode).toBe(rosu.GameMode.Mania); + }); - it('Calculated score pp for with hitresults provided and only accuracy provided should be the same', async () => { - const EXPECTED_PP = 663.154; // https://web.archive.org/web/20251102200440/https://osu.ppy.sh/scores/1794640344 - - const scoreWithoutHitresults: Score = { - accuracy: 99.689999999999998, - combo: 2955, - isLazer: false, - isScoreFailed: false, - }; - - const scoreWithHitresults: Score = { - combo: 2955, - n300: 1927, - nGeki: 0, - n100: 9, - nKatu: 0, - n50: 0, - misses: 0, - mode: rosu.GameMode.Osu, - mods: GameModBitwise.NoMod, - isLazer: false, - isScoreFailed: false, - }; - - const resultWithHitresults = calculatorService.CalculateScorePerfomance( - beatmap, - scoreWithHitresults, - ); - - const result = calculatorService.CalculateScorePerfomance( - beatmap, - scoreWithoutHitresults, - ); - - expect(resultWithHitresults.pp).toBeWithin( - EXPECTED_PP - 0.001, - EXPECTED_PP + 0.001, - ); - - expect(resultWithHitresults.pp).toEqual(result.pp); - }); + it("Invalid beatmap mode should be defaulted to std", async () => { + const beatmapMode = TryConvertToGamemode(5) ?? beatmap.mode; - it('Calculated score pp for with hitresults provided and only accuracy provided should be the same with score having a miss', async () => { - const EXPECTED_PP = 587.555; // https://web.archive.org/web/20251102202454/https://osu.ppy.sh/scores/2529969691 - - const scoreWithoutHitresults: Score = { - accuracy: 99.43, - combo: 1914, - isLazer: false, - isScoreFailed: false, - }; - - const scoreWithHitresults: Score = { - combo: 1914, - n300: 1920, - nGeki: 0, - n100: 15, - nKatu: 0, - n50: 0, - misses: 1, - mode: rosu.GameMode.Osu, - mods: GameModBitwise.NoMod, - isLazer: false, - isScoreFailed: false, - }; - - const resultWithHitresults = calculatorService.CalculateScorePerfomance( - beatmap, - scoreWithHitresults, - ); - - const result = calculatorService.CalculateScorePerfomance( - beatmap, - scoreWithoutHitresults, - ); - - expect(resultWithHitresults.pp).toBeWithin( - EXPECTED_PP - 0.001, - EXPECTED_PP + 0.001, - ); - - expect(resultWithHitresults.pp).toEqual(result.pp); - }); + expect(beatmapMode).toBe(rosu.GameMode.Osu); + }); - it('Calculated score pp with hitresults and compare with beatmap calculation without hitresults', async () => { - const EXPECTED_PP = 663.154; // https://web.archive.org/web/20251102200440/https://osu.ppy.sh/scores/1794640344 - - const scoreWithoutHitresults: Score = { - accuracy: 99.689999999999998, - combo: 2955, - isLazer: false, - isScoreFailed: false, - }; - - const scoreWithHitresults: Score = { - combo: 2955, - n300: 1927, - nGeki: 0, - n100: 9, - nKatu: 0, - n50: 0, - misses: 0, - mode: rosu.GameMode.Osu, - mods: GameModBitwise.NoMod, - isLazer: false, - isScoreFailed: false, - }; - - const resultWithHitresults = calculatorService.CalculateScorePerfomance( - beatmap, - scoreWithHitresults, - ); - - const results = calculatorService.CalculateBeatmapPerfomance(beatmap, [ - scoreWithoutHitresults, - ]); - - expect(resultWithHitresults.pp).toBeWithin( - EXPECTED_PP - 0.001, - EXPECTED_PP + 0.001, - ); - - expect(resultWithHitresults.pp).toEqual(results[0].pp); - }); + it("Should calculate multiple acc values", async () => { + const accuracies = [100, 99, 98, 95]; - it('Should calculate score pp: standard', async () => { - const EXPECTED_PP = 243.002; // https://web.archive.org/web/20251102193936/https://osu.ppy.sh/scores/4502844247 - - const score: Score = { - accuracy: 97.13, - mods: GameModBitwise.Hidden, - combo: 956, - n300: 1869, - n100: 34, - n50: 1, - misses: 32, - isLazer: false, - isScoreFailed: false, - }; - - const result = calculatorService.CalculateScorePerfomance( - beatmap, - score, - ); - - expect(result.pp).toBeWithin(EXPECTED_PP - 0.001, EXPECTED_PP + 0.001); + const scores: ScoreShort[] = accuracies.map((a) => { + return { accuracy: a, isLazer: false, isScoreFailed: false }; }); - it('Should calculate score pp: taiko', async () => { - const EXPECTED_PP = 97.975; // https://osu.ppy.sh/scores/1867539282 - - const score: Score = { - accuracy: 91.64, - mods: GameModBitwise.HalfTime, - mode: rosu.GameMode.Taiko, - combo: 516, - nGeki: 2577, - nKatu: 240, - misses: 126, - isLazer: false, - isScoreFailed: false, - }; - - const result = calculatorService.CalculateScorePerfomance( - beatmap, - score, - ); - - expect(result.pp).toBeWithin(EXPECTED_PP - 0.001, EXPECTED_PP + 0.001); + const results = calculatorService.CalculateBeatmapPerfomance( + beatmap, + scores, + ); + + expect(results.length).toBe(accuracies.length); + }); + + it("Should apply DT mod", async () => { + const scores: ScoreShort[] = [ + { + accuracy: 100, + isLazer: false, + isScoreFailed: false, + }, + ]; + + const scoresWithDT = scores.map((s) => { + return { + ...s, + mods: GameModBitwise.DoubleTime, + }; }); - it('Should calculate score pp: catch', async () => { - const EXPECTED_PP = 224.697; // https://osu.ppy.sh/scores/1953302746 - - const score: Score = { - accuracy: 99.18, - mods: GameModBitwise.Hidden, - mode: rosu.GameMode.Catch, - combo: 1378, - nGeki: 2946, - nKatu: 19, - misses: 8, - isLazer: false, - isScoreFailed: false, - }; - - const result = calculatorService.CalculateScorePerfomance( - beatmap, - score, - ); - - expect(result.pp).toBeWithin(EXPECTED_PP - 0.001, EXPECTED_PP + 0.001); + const perfomanceWithoutMod + = calculatorService.CalculateBeatmapPerfomance(beatmap, scores); + + const perfomanceWithMod = calculatorService.CalculateBeatmapPerfomance( + beatmap, + scoresWithDT, + ); + + expect(perfomanceWithMod[0].difficulty.stars).toBeGreaterThan( + perfomanceWithoutMod[0].difficulty.stars, + ); + }); + + it("Should convert beatmap to another gamemode", async () => { + const scoresInStd: ScoreShort[] = [ + { + accuracy: 100, + mode: rosu.GameMode.Osu, + isLazer: false, + isScoreFailed: false, + }, + ]; + + const scoresInTaiko = scoresInStd.map((s) => { + return { + ...s, + mode: rosu.GameMode.Taiko, + }; }); - it('Should calculate score pp: mania', async () => { - const EXPECTED_PP = 152.199; // https://osu.ppy.sh/scores/4111918025 - - const score: Score = { - accuracy: 99.06, - mods: GameModBitwise.DoubleTime, - mode: rosu.GameMode.Mania, - combo: 2345, - n300: 1004, - nGeki: 62, - n100: 0, - nKatu: 62, - n50: 2, - misses: 4, - isLazer: false, - isScoreFailed: false, - }; - - const result = calculatorService.CalculateScorePerfomance( - beatmap, - score, - ); - - expect(result.pp).toBeWithin(EXPECTED_PP - 0.001, EXPECTED_PP + 0.001); - }); + const perfomanceStandard = calculatorService.CalculateBeatmapPerfomance( + beatmap, + scoresInStd, + ); + + const perfomanceTaiko = calculatorService.CalculateBeatmapPerfomance( + beatmap, + scoresInTaiko, + ); + + expect(perfomanceTaiko[0].difficulty.isConvert).toBe(true); + expect(perfomanceStandard[0].difficulty.stars).not.toEqual( + perfomanceTaiko[0].difficulty.stars, + ); + }); + + it("Should convert score to another gamemode with mod convertion alteration", async () => { + const EXPECTED_PP = 138.709; // https://web.archive.org/web/20251102193104/https://osu.ppy.sh/scores/2248271767 + const EXPECTED_STARS = 4.04; // https://archive.org/details/discord-7xd-xe-m-4-ofb + + const score: Score = { + accuracy: 97.33, + combo: 850, + n300: 756, + nGeki: 1650, + n100: 3, + nKatu: 124, + n50: 2, + misses: 9, + mode: rosu.GameMode.Mania, + mods: GameModBitwise.DoubleTime | GameModBitwise.Key4, + isScoreFailed: false, + isLazer: false, + }; + + const result = calculatorService.CalculateScorePerfomance( + beatmap, + score, + ); + + expect(result.difficulty.isConvert).toBe(true); + + expect(result.pp).toBeWithin(EXPECTED_PP - 0.001, EXPECTED_PP + 0.001); + expect(result.difficulty.stars).toBeWithin( + EXPECTED_STARS - 0.01, + EXPECTED_STARS + 0.01, + ); + }); + + it("Should convert beatmap to another gamemode with mod convertion alteration", async () => { + const EXPECTED_STARS = 4.04; // https://archive.org/details/discord-7xd-xe-m-4-ofb + + const scores: ScoreShort[] = [ + { + accuracy: 100, + mode: rosu.GameMode.Mania, + mods: GameModBitwise.DoubleTime | GameModBitwise.Key4, + isLazer: false, + isScoreFailed: false, + }, + ]; + + const result = calculatorService.CalculateBeatmapPerfomance( + beatmap, + scores, + ); + + expect(result[0].difficulty.isConvert).toBe(true); + + expect(result[0].difficulty.stars).toBeWithin( + EXPECTED_STARS - 0.01, + EXPECTED_STARS + 0.01, + ); + }); + + it("Calculated score pp for with hitresults provided and only accuracy provided should be the same", async () => { + const EXPECTED_PP = 663.154; // https://web.archive.org/web/20251102200440/https://osu.ppy.sh/scores/1794640344 + + const scoreWithoutHitresults: Score = { + accuracy: 99.689999999999998, + combo: 2955, + isLazer: false, + isScoreFailed: false, + }; + + const scoreWithHitresults: Score = { + combo: 2955, + n300: 1927, + nGeki: 0, + n100: 9, + nKatu: 0, + n50: 0, + misses: 0, + mode: rosu.GameMode.Osu, + mods: GameModBitwise.NoMod, + isLazer: false, + isScoreFailed: false, + }; + + const resultWithHitresults = calculatorService.CalculateScorePerfomance( + beatmap, + scoreWithHitresults, + ); + + const result = calculatorService.CalculateScorePerfomance( + beatmap, + scoreWithoutHitresults, + ); + + expect(resultWithHitresults.pp).toBeWithin( + EXPECTED_PP - 0.001, + EXPECTED_PP + 0.001, + ); + + expect(resultWithHitresults.pp).toEqual(result.pp); + }); + + it("Calculated score pp for with hitresults provided and only accuracy provided should be the same with score having a miss", async () => { + const EXPECTED_PP = 587.555; // https://web.archive.org/web/20251102202454/https://osu.ppy.sh/scores/2529969691 + + const scoreWithoutHitresults: Score = { + accuracy: 99.43, + combo: 1914, + isLazer: false, + isScoreFailed: false, + }; + + const scoreWithHitresults: Score = { + combo: 1914, + n300: 1920, + nGeki: 0, + n100: 15, + nKatu: 0, + n50: 0, + misses: 1, + mode: rosu.GameMode.Osu, + mods: GameModBitwise.NoMod, + isLazer: false, + isScoreFailed: false, + }; + + const resultWithHitresults = calculatorService.CalculateScorePerfomance( + beatmap, + scoreWithHitresults, + ); + + const result = calculatorService.CalculateScorePerfomance( + beatmap, + scoreWithoutHitresults, + ); + + expect(resultWithHitresults.pp).toBeWithin( + EXPECTED_PP - 0.001, + EXPECTED_PP + 0.001, + ); + + expect(resultWithHitresults.pp).toEqual(result.pp); + }); + + it("Calculated score pp with hitresults and compare with beatmap calculation without hitresults", async () => { + const EXPECTED_PP = 663.154; // https://web.archive.org/web/20251102200440/https://osu.ppy.sh/scores/1794640344 + + const scoreWithoutHitresults: Score = { + accuracy: 99.689999999999998, + combo: 2955, + isLazer: false, + isScoreFailed: false, + }; + + const scoreWithHitresults: Score = { + combo: 2955, + n300: 1927, + nGeki: 0, + n100: 9, + nKatu: 0, + n50: 0, + misses: 0, + mode: rosu.GameMode.Osu, + mods: GameModBitwise.NoMod, + isLazer: false, + isScoreFailed: false, + }; + + const resultWithHitresults = calculatorService.CalculateScorePerfomance( + beatmap, + scoreWithHitresults, + ); + + const results = calculatorService.CalculateBeatmapPerfomance(beatmap, [ + scoreWithoutHitresults, + ]); + + expect(resultWithHitresults.pp).toBeWithin( + EXPECTED_PP - 0.001, + EXPECTED_PP + 0.001, + ); + + expect(resultWithHitresults.pp).toEqual(results[0].pp); + }); + + it("Should calculate score pp: standard", async () => { + const EXPECTED_PP = 243.002; // https://web.archive.org/web/20251102193936/https://osu.ppy.sh/scores/4502844247 + + const score: Score = { + accuracy: 97.13, + mods: GameModBitwise.Hidden, + combo: 956, + n300: 1869, + n100: 34, + n50: 1, + misses: 32, + isLazer: false, + isScoreFailed: false, + }; + + const result = calculatorService.CalculateScorePerfomance( + beatmap, + score, + ); + + expect(result.pp).toBeWithin(EXPECTED_PP - 0.001, EXPECTED_PP + 0.001); + }); + + it("Should calculate score pp: taiko", async () => { + const EXPECTED_PP = 97.975; // https://osu.ppy.sh/scores/1867539282 + + const score: Score = { + accuracy: 91.64, + mods: GameModBitwise.HalfTime, + mode: rosu.GameMode.Taiko, + combo: 516, + nGeki: 2577, + nKatu: 240, + misses: 126, + isLazer: false, + isScoreFailed: false, + }; + + const result = calculatorService.CalculateScorePerfomance( + beatmap, + score, + ); + + expect(result.pp).toBeWithin(EXPECTED_PP - 0.001, EXPECTED_PP + 0.001); + }); + + it("Should calculate score pp: catch", async () => { + const EXPECTED_PP = 224.697; // https://osu.ppy.sh/scores/1953302746 + + const score: Score = { + accuracy: 99.18, + mods: GameModBitwise.Hidden, + mode: rosu.GameMode.Catch, + combo: 1378, + nGeki: 2946, + nKatu: 19, + misses: 8, + isLazer: false, + isScoreFailed: false, + }; + + const result = calculatorService.CalculateScorePerfomance( + beatmap, + score, + ); + + expect(result.pp).toBeWithin(EXPECTED_PP - 0.001, EXPECTED_PP + 0.001); + }); + + it("Should calculate score pp: mania", async () => { + const EXPECTED_PP = 152.199; // https://osu.ppy.sh/scores/4111918025 + + const score: Score = { + accuracy: 99.06, + mods: GameModBitwise.DoubleTime, + mode: rosu.GameMode.Mania, + combo: 2345, + n300: 1004, + nGeki: 62, + n100: 0, + nKatu: 62, + n50: 2, + misses: 4, + isLazer: false, + isScoreFailed: false, + }; + + const result = calculatorService.CalculateScorePerfomance( + beatmap, + score, + ); + + expect(result.pp).toBeWithin(EXPECTED_PP - 0.001, EXPECTED_PP + 0.001); + }); }); diff --git a/server/tests/compitability.production.test.ts b/server/tests/compitability.production.test.ts index 16d3cf7..f6013ab 100644 --- a/server/tests/compitability.production.test.ts +++ b/server/tests/compitability.production.test.ts @@ -1,361 +1,361 @@ -import { beforeAll, describe, expect, it } from 'bun:test'; -import { BanchoClient, DirectClient } from '../src/core/domains'; - -// @ts-ignore -import json from './data/mirror.tests.json'; -import { MinoClient } from '../src/core/domains/catboy.best/mino.client'; -import config from '../src/config'; -import { NerinyanClient } from '../src/core/domains/nerinyan.moe/nerinyan.client'; -import { GatariClient } from '../src/core/domains/gatari.pw/gatari.client'; -import { OsulabsClient } from '../src/core/domains/beatmaps.download/osulabs.client'; -import { StorageManager } from '../src/core/managers/storage/storage.manager'; +import { beforeAll, describe, expect, it } from "bun:test"; + +import config from "../src/config"; +import { BanchoClient, DirectClient } from "../src/core/domains"; +import { OsulabsClient } from "../src/core/domains/beatmaps.download/osulabs.client"; +import { MinoClient } from "../src/core/domains/catboy.best/mino.client"; +import { GatariClient } from "../src/core/domains/gatari.pw/gatari.client"; +import { NerinyanClient } from "../src/core/domains/nerinyan.moe/nerinyan.client"; +import { StorageManager } from "../src/core/managers/storage/storage.manager"; +// @ts-expect-error -- JSON import +import json from "./data/mirror.tests.json"; describe.skipIf(!config.IsProduction)( - 'Production compatibility tests - USE TO CHECK IF THE MIRRORS ARE COMPATIBLE WITH THE PRODUCTION ENVIRONMENT', - () => { - const storageManager = new StorageManager(); - - const banchoClient = new BanchoClient(); - const minoClient = new MinoClient(storageManager); - const osulabsClient = new OsulabsClient(storageManager); - const directClient = new DirectClient(); - const nerinyanClient = new NerinyanClient(); - const gatariClient = new GatariClient(); - - const getRandomTest = (key: string) => { - return json.tests[key][ - Math.floor(Math.random() * json.tests[key].length) - ]; - }; - - beforeAll(() => { - config.IsAutomatedTesting = false; // Disable automated testing to allow sending requests to the APIs + "Production compatibility tests - USE TO CHECK IF THE MIRRORS ARE COMPATIBLE WITH THE PRODUCTION ENVIRONMENT", + () => { + const storageManager = new StorageManager(); + + const banchoClient = new BanchoClient(); + const minoClient = new MinoClient(storageManager); + const osulabsClient = new OsulabsClient(storageManager); + const directClient = new DirectClient(); + const nerinyanClient = new NerinyanClient(); + const gatariClient = new GatariClient(); + + const getRandomTest = (key: string) => { + return json.tests[key][ + Math.floor(Math.random() * json.tests[key].length) + ]; + }; + + beforeAll(() => { + config.IsAutomatedTesting = false; // Disable automated testing to allow sending requests to the APIs + }); + + describe("Bancho tests", () => { + const hasClientToken + = config.BANCHO_CLIENT_ID && config.BANCHO_CLIENT_SECRET + ? true + : false; + + it("Bancho: should return converted beatmap", async () => { + if (!hasClientToken) { + return it.skip("Bancho: should return converted beatmap", () => {}); + } + + const randomTest = getRandomTest("getBeatmapById"); + const { beatmapId } = randomTest; + + const beatmap = await banchoClient.getBeatmap({ + beatmapId, }); - describe('Bancho tests', () => { - const hasClientToken = - config.BANCHO_CLIENT_ID && config.BANCHO_CLIENT_SECRET - ? true - : false; - - it('Bancho: should return converted beatmap', async () => { - if (!hasClientToken) { - return it.skip('Bancho: should return converted beatmap', () => {}); - } - - const randomTest = getRandomTest('getBeatmapById'); - const { beatmapId } = randomTest; - - const beatmap = await banchoClient.getBeatmap({ - beatmapId, - }); - - expect(beatmap.result).toContainAllKeys( - Object.keys(randomTest.data), - ); - }); - - it('Bancho: should return converted beatmapset', async () => { - if (!hasClientToken) { - return it.skip('Bancho: should return converted beatmapset', () => {}); - } - - const randomTest = getRandomTest('getBeatmapsetById'); - const { beatmapSetId } = randomTest; - - const beatmap = await banchoClient.getBeatmapSet({ - beatmapSetId, - }); - - expect(beatmap.result).toContainAllKeys( - Object.keys(randomTest.data), - ); - }); - - it('Bancho: should download osu beatmap', async () => { - if (!hasClientToken) { - return it.skip('Bancho: should download osu beatmap', () => {}); - } - const randomTest = getRandomTest('downloadOsuBeatmap'); - const { beatmapId } = randomTest; - - const beatmap = await banchoClient.downloadOsuBeatmap({ - beatmapId, - }); - - const expected = await Bun.file( - `${import.meta.dir}/data/${beatmapId}.osu.test`, - ).arrayBuffer(); + expect(beatmap.result).toContainAllKeys( + Object.keys(randomTest.data), + ); + }); + + it("Bancho: should return converted beatmapset", async () => { + if (!hasClientToken) { + return it.skip("Bancho: should return converted beatmapset", () => {}); + } + + const randomTest = getRandomTest("getBeatmapsetById"); + const { beatmapSetId } = randomTest; - expect(beatmap.result?.byteLength).toBeWithin( - expected.byteLength - 1000, - expected.byteLength + 1000, - ); - }, 30000); + const beatmap = await banchoClient.getBeatmapSet({ + beatmapSetId, }); - describe('Mino tests', () => { - it('Mino: should return converted beatmap', async () => { - const randomTest = getRandomTest('getBeatmapById'); - const { beatmapId } = randomTest; + expect(beatmap.result).toContainAllKeys( + Object.keys(randomTest.data), + ); + }); - const beatmap = await minoClient.getBeatmap({ - beatmapId, - }); + it("Bancho: should download osu beatmap", async () => { + if (!hasClientToken) { + return it.skip("Bancho: should download osu beatmap", () => {}); + } + const randomTest = getRandomTest("downloadOsuBeatmap"); + const { beatmapId } = randomTest; - expect(beatmap.result).toContainAllKeys( - Object.keys(randomTest.data), - ); - }); + const beatmap = await banchoClient.downloadOsuBeatmap({ + beatmapId, + }); - it('Mino: should return converted beatmapset', async () => { - const randomTest = getRandomTest('getBeatmapsetById'); - const { beatmapSetId } = randomTest; + const expected = await Bun.file( + `${import.meta.dir}/data/${beatmapId}.osu.test`, + ).arrayBuffer(); + + expect(beatmap.result?.byteLength).toBeWithin( + expected.byteLength - 1000, + expected.byteLength + 1000, + ); + }, 30000); + }); + + describe("Mino tests", () => { + it("Mino: should return converted beatmap", async () => { + const randomTest = getRandomTest("getBeatmapById"); + const { beatmapId } = randomTest; + + const beatmap = await minoClient.getBeatmap({ + beatmapId, + }); + + expect(beatmap.result).toContainAllKeys( + Object.keys(randomTest.data), + ); + }); - const beatmap = await minoClient.getBeatmapSet({ - beatmapSetId, - }); + it("Mino: should return converted beatmapset", async () => { + const randomTest = getRandomTest("getBeatmapsetById"); + const { beatmapSetId } = randomTest; - expect(beatmap.result).toContainAllKeys( - Object.keys(randomTest.data), - ); - }); + const beatmap = await minoClient.getBeatmapSet({ + beatmapSetId, + }); - it('Mino: should download beatmap set without video', async () => { - const randomTest = getRandomTest('downloadBeatmapSet'); - const { beatmapSetId } = randomTest; + expect(beatmap.result).toContainAllKeys( + Object.keys(randomTest.data), + ); + }); - const beatmap = await minoClient.downloadBeatmapSet({ - beatmapSetId, - noVideo: true, - }); + it("Mino: should download beatmap set without video", async () => { + const randomTest = getRandomTest("downloadBeatmapSet"); + const { beatmapSetId } = randomTest; - const expected = await Bun.file( + const beatmap = await minoClient.downloadBeatmapSet({ + beatmapSetId, + noVideo: true, + }); + + const expected = await Bun.file( `${import.meta.dir}/data/${beatmapSetId}n.osz`, - ).arrayBuffer(); + ).arrayBuffer(); - expect(beatmap.result?.byteLength).toBeWithin( - expected.byteLength - 1000, - expected.byteLength + 1000, - ); - }, 30000); + expect(beatmap.result?.byteLength).toBeWithin( + expected.byteLength - 1000, + expected.byteLength + 1000, + ); + }, 30000); - it('Mino: should download beatmap set with video', async () => { - const randomTest = getRandomTest('downloadBeatmapSet'); - const { beatmapSetId } = randomTest; + it("Mino: should download beatmap set with video", async () => { + const randomTest = getRandomTest("downloadBeatmapSet"); + const { beatmapSetId } = randomTest; - const beatmap = await minoClient.downloadBeatmapSet({ - beatmapSetId, - noVideo: false, - }); + const beatmap = await minoClient.downloadBeatmapSet({ + beatmapSetId, + noVideo: false, + }); - const expected = await Bun.file( + const expected = await Bun.file( `${import.meta.dir}/data/${beatmapSetId}.osz`, - ).arrayBuffer(); + ).arrayBuffer(); - expect(beatmap.result?.byteLength).toBeWithin( - expected.byteLength - 1000, - expected.byteLength + 1000, - ); - }, 30000); + expect(beatmap.result?.byteLength).toBeWithin( + expected.byteLength - 1000, + expected.byteLength + 1000, + ); + }, 30000); - it('Mino: should download osu beatmap', async () => { - const randomTest = getRandomTest('downloadOsuBeatmap'); - const { beatmapId } = randomTest; + it("Mino: should download osu beatmap", async () => { + const randomTest = getRandomTest("downloadOsuBeatmap"); + const { beatmapId } = randomTest; - const beatmap = await minoClient.downloadOsuBeatmap({ - beatmapId, - }); + const beatmap = await minoClient.downloadOsuBeatmap({ + beatmapId, + }); - const expected = await Bun.file( + const expected = await Bun.file( `${import.meta.dir}/data/${beatmapId}.osu.test`, - ).arrayBuffer(); - - expect(beatmap.result?.byteLength).toBeWithin( - expected.byteLength - 1000, - expected.byteLength + 1000, - ); - }); + ).arrayBuffer(); + + expect(beatmap.result?.byteLength).toBeWithin( + expected.byteLength - 1000, + expected.byteLength + 1000, + ); + }); + }); + + describe("Osulabs tests", () => { + it("Osulabs: should return converted beatmap", async () => { + const randomTest = getRandomTest("getBeatmapById"); + const { beatmapId } = randomTest; + + const beatmap = await osulabsClient.getBeatmap({ + beatmapId, }); - describe('Osulabs tests', () => { - it('Osulabs: should return converted beatmap', async () => { - const randomTest = getRandomTest('getBeatmapById'); - const { beatmapId } = randomTest; + expect(beatmap.result).toContainAllKeys( + Object.keys(randomTest.data), + ); + }); - const beatmap = await osulabsClient.getBeatmap({ - beatmapId, - }); + it("Osulabs: should return converted beatmapset", async () => { + const randomTest = getRandomTest("getBeatmapsetById"); + const { beatmapSetId } = randomTest; - expect(beatmap.result).toContainAllKeys( - Object.keys(randomTest.data), - ); - }); - - it('Osulabs: should return converted beatmapset', async () => { - const randomTest = getRandomTest('getBeatmapsetById'); - const { beatmapSetId } = randomTest; - - const beatmap = await osulabsClient.getBeatmapSet({ - beatmapSetId, - }); + const beatmap = await osulabsClient.getBeatmapSet({ + beatmapSetId, + }); - expect(beatmap.result).toContainAllKeys( - Object.keys(randomTest.data), - ); - }); + expect(beatmap.result).toContainAllKeys( + Object.keys(randomTest.data), + ); + }); - it('Osulabs: should download beatmap set without video', async () => { - const randomTest = getRandomTest('downloadBeatmapSet'); - const { beatmapSetId } = randomTest; + it("Osulabs: should download beatmap set without video", async () => { + const randomTest = getRandomTest("downloadBeatmapSet"); + const { beatmapSetId } = randomTest; - const beatmap = await osulabsClient.downloadBeatmapSet({ - beatmapSetId, - noVideo: true, - }); + const beatmap = await osulabsClient.downloadBeatmapSet({ + beatmapSetId, + noVideo: true, + }); - const expected = await Bun.file( + const expected = await Bun.file( `${import.meta.dir}/data/${beatmapSetId}n.osz`, - ).arrayBuffer(); + ).arrayBuffer(); - expect(beatmap.result?.byteLength).toBeWithin( - expected.byteLength - 1000, - expected.byteLength + 1000, - ); - }, 30000); + expect(beatmap.result?.byteLength).toBeWithin( + expected.byteLength - 1000, + expected.byteLength + 1000, + ); + }, 30000); - it('Osulabs: should download beatmap set with video', async () => { - const randomTest = getRandomTest('downloadBeatmapSet'); - const { beatmapSetId } = randomTest; + it("Osulabs: should download beatmap set with video", async () => { + const randomTest = getRandomTest("downloadBeatmapSet"); + const { beatmapSetId } = randomTest; - const beatmap = await osulabsClient.downloadBeatmapSet({ - beatmapSetId, - noVideo: false, - }); + const beatmap = await osulabsClient.downloadBeatmapSet({ + beatmapSetId, + noVideo: false, + }); - const expected = await Bun.file( + const expected = await Bun.file( `${import.meta.dir}/data/${beatmapSetId}.osz`, - ).arrayBuffer(); - - expect(beatmap.result?.byteLength).toBeWithin( - expected.byteLength - 1000, - expected.byteLength + 1000, - ); - }, 30000); + ).arrayBuffer(); + + expect(beatmap.result?.byteLength).toBeWithin( + expected.byteLength - 1000, + expected.byteLength + 1000, + ); + }, 30000); + }); + + describe("Direct tests", () => { + it("Direct: should download beatmap set without video", async () => { + const randomTest = getRandomTest("downloadBeatmapSet"); + const { beatmapSetId } = randomTest; + + const beatmap = await directClient.downloadBeatmapSet({ + beatmapSetId, + noVideo: true, }); - describe('Direct tests', () => { - it('Direct: should download beatmap set without video', async () => { - const randomTest = getRandomTest('downloadBeatmapSet'); - const { beatmapSetId } = randomTest; - - const beatmap = await directClient.downloadBeatmapSet({ - beatmapSetId, - noVideo: true, - }); - - const expected = await Bun.file( + const expected = await Bun.file( `${import.meta.dir}/data/${beatmapSetId}n.osz`, - ).arrayBuffer(); + ).arrayBuffer(); - expect(beatmap.result?.byteLength).toBeWithin( - expected.byteLength - 1000, - expected.byteLength + 1000, - ); - }, 30000); + expect(beatmap.result?.byteLength).toBeWithin( + expected.byteLength - 1000, + expected.byteLength + 1000, + ); + }, 30000); - it('Direct: should download beatmap set with video', async () => { - const randomTest = getRandomTest('downloadBeatmapSet'); - const { beatmapSetId } = randomTest; + it("Direct: should download beatmap set with video", async () => { + const randomTest = getRandomTest("downloadBeatmapSet"); + const { beatmapSetId } = randomTest; - const beatmap = await directClient.downloadBeatmapSet({ - beatmapSetId, - noVideo: false, - }); - - const expected = await Bun.file( - `${import.meta.dir}/data/${beatmapSetId}.osz`, - ).arrayBuffer(); - - expect(beatmap.result?.byteLength).toBeWithin( - expected.byteLength - 1000, - expected.byteLength + 1000, - ); - }, 30000); - - it('Direct: should return converted beatmap', async () => { - const randomTest = getRandomTest('getBeatmapById'); - const { beatmapId } = randomTest; - - const beatmap = await directClient.getBeatmap({ - beatmapId, - }); - - expect(beatmap.result).toContainAllKeys( - Object.keys(randomTest.data), - ); - }); + const beatmap = await directClient.downloadBeatmapSet({ + beatmapSetId, + noVideo: false, }); - describe('Gatari tests', () => { - it('Gatari: should download beatmap set without video', async () => { - const randomTest = getRandomTest('downloadBeatmapSet'); - const { beatmapSetId } = randomTest; + const expected = await Bun.file( + `${import.meta.dir}/data/${beatmapSetId}.osz`, + ).arrayBuffer(); - const beatmap = await gatariClient.downloadBeatmapSet({ - beatmapSetId, - noVideo: true, - }); + expect(beatmap.result?.byteLength).toBeWithin( + expected.byteLength - 1000, + expected.byteLength + 1000, + ); + }, 30000); - const expected = await Bun.file( - `${import.meta.dir}/data/${beatmapSetId}n.osz`, - ).arrayBuffer(); + it("Direct: should return converted beatmap", async () => { + const randomTest = getRandomTest("getBeatmapById"); + const { beatmapId } = randomTest; - expect(beatmap.result?.byteLength).toBeWithin( - expected.byteLength - 1000, - expected.byteLength + 1000, - ); - }, 30000); + const beatmap = await directClient.getBeatmap({ + beatmapId, }); - describe('Nerinyan tests', () => { - it('Nerinyan: should download beatmap set without video', async () => { - const randomTest = getRandomTest('downloadBeatmapSet'); - const { beatmapSetId } = randomTest; + expect(beatmap.result).toContainAllKeys( + Object.keys(randomTest.data), + ); + }); + }); - const beatmap = await nerinyanClient.downloadBeatmapSet({ - beatmapSetId, - noVideo: true, - }); + describe("Gatari tests", () => { + it("Gatari: should download beatmap set without video", async () => { + const randomTest = getRandomTest("downloadBeatmapSet"); + const { beatmapSetId } = randomTest; - const expected = await Bun.file( - `${import.meta.dir}/data/${beatmapSetId}n.osz`, - ).arrayBuffer(); + const beatmap = await gatariClient.downloadBeatmapSet({ + beatmapSetId, + noVideo: true, + }); - expect(beatmap.result?.byteLength).toBeWithin( - expected.byteLength - 1000, - expected.byteLength + 1000, - ); - }, 30000); + const expected = await Bun.file( + `${import.meta.dir}/data/${beatmapSetId}n.osz`, + ).arrayBuffer(); + + expect(beatmap.result?.byteLength).toBeWithin( + expected.byteLength - 1000, + expected.byteLength + 1000, + ); + }, 30000); + }); + + describe("Nerinyan tests", () => { + it("Nerinyan: should download beatmap set without video", async () => { + const randomTest = getRandomTest("downloadBeatmapSet"); + const { beatmapSetId } = randomTest; + + const beatmap = await nerinyanClient.downloadBeatmapSet({ + beatmapSetId, + noVideo: true, + }); - it('Nerinyan: should download beatmap set with video', async () => { - const randomTest = getRandomTest('downloadBeatmapSet'); - const { beatmapSetId } = randomTest; + const expected = await Bun.file( + `${import.meta.dir}/data/${beatmapSetId}n.osz`, + ).arrayBuffer(); - const beatmap = await nerinyanClient.downloadBeatmapSet({ - beatmapSetId, - noVideo: false, - }); + expect(beatmap.result?.byteLength).toBeWithin( + expected.byteLength - 1000, + expected.byteLength + 1000, + ); + }, 30000); - const expected = await Bun.file( - `${import.meta.dir}/data/${beatmapSetId}.osz`, - ).arrayBuffer(); + it("Nerinyan: should download beatmap set with video", async () => { + const randomTest = getRandomTest("downloadBeatmapSet"); + const { beatmapSetId } = randomTest; - expect(beatmap.result?.byteLength).toBeWithin( - expected.byteLength - 1000, - expected.byteLength + 1000, - ); - }, 30000); + const beatmap = await nerinyanClient.downloadBeatmapSet({ + beatmapSetId, + noVideo: false, }); - }, + + const expected = await Bun.file( + `${import.meta.dir}/data/${beatmapSetId}.osz`, + ).arrayBuffer(); + + expect(beatmap.result?.byteLength).toBeWithin( + expected.byteLength - 1000, + expected.byteLength + 1000, + ); + }, 30000); + }); + }, ); diff --git a/server/tests/data/mirror.tests.json b/server/tests/data/mirror.tests.json index 62e8b0e..22b824d 100644 --- a/server/tests/data/mirror.tests.json +++ b/server/tests/data/mirror.tests.json @@ -1,1324 +1,5113 @@ { - "tests": { - "downloadBeatmapSet": [ + "tests": { + "downloadBeatmapSet": [ + { + "beatmapSetId": 2069845 + } + ], + "downloadOsuBeatmap": [ + { + "beatmapId": 2809623 + } + ], + "getBeatmapById": [ + { + "beatmapId": 4342177, + "data": { + "beatmapset_id": 2070848, + "difficulty_rating": 1.79, + "id": 4342177, + "mode": "osu", + "status": "ranked", + "total_length": 115, + "user_id": 685229, + "version": "Normal", + "accuracy": 4, + "ar": 5.5, + "bpm": 150, + "convert": false, + "count_circles": 39, + "count_sliders": 112, + "count_spinners": 1, + "cs": 3.5, + "deleted_at": null, + "drain": 3, + "hit_length": 104, + "is_scoreable": true, + "last_updated": "2023-11-14T05:04:26Z", + "mode_int": 0, + "passcount": 5478, + "playcount": 12511, + "ranked": 1, + "url": "https://osu.ppy.sh/beatmaps/4342177", + "checksum": "3b7c0bb338564060ab512b340a07f011", + "failtimes": { + "fail": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 117, + 54, + 63, + 72, + 18, + 72, + 0, + 45, + 90, + 99, + 18, + 27, + 54, + 0, + 36, + 36, + 54, + 9, + 18, + 18, + 9, + 54, + 18, + 9, + 27, + 9, + 63, + 0, + 0, + 0, + 9, + 36, + 9, + 27, + 0, + 9, + 72, + 9, + 18, + 36, + 45, + 18, + 18, + 36, + 27, + 9, + 45, + 9, + 27, + 0, + 9, + 9, + 0, + 36, + 0, + 9, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 18, + 0, + 0, + 0, + 0, + 9, + 9, + 0, + 0, + 9, + 0, + 36, + 0, + 9, + 0, + 0, + 0, + 9, + 9 + ], + "exit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 27, + 90, + 63, + 63, + 81, + 54, + 81, + 54, + 171, + 90, + 126, + 45, + 27, + 63, + 54, + 108, + 90, + 63, + 36, + 36, + 27, + 54, + 63, + 0, + 36, + 45, + 9, + 27, + 9, + 54, + 9, + 27, + 45, + 63, + 36, + 18, + 108, + 63, + 18, + 36, + 27, + 18, + 63, + 54, + 18, + 18, + 36, + 9, + 72, + 54, + 18, + 0, + 27, + 27, + 9, + 27, + 18, + 0, + 27, + 45, + 18, + 27, + 18, + 0, + 0, + 9, + 0, + 0, + 0, + 0, + 9, + 0, + 0, + 18, + 9, + 0, + 9, + 0, + 0, + 9, + 36, + 18, + 36, + 36, + 27, + 18, + 9, + 0, + 0, + 0, + 18 + ] + }, + "max_combo": 308 + } + } + ], + "getBeatmapsetById": [ + { + "beatmapSetId": 2070848, + "data": { + "artist": "Sasuke Haraguchi feat. Kasane Teto", + "artist_unicode": "原口沙輔 feat. 重音テト", + "covers": { + "cover": "https://assets.ppy.sh/beatmaps/2070848/covers/cover.jpg?1699938283", + "cover@2x": "https://assets.ppy.sh/beatmaps/2070848/covers/cover@2x.jpg?1699938283", + "card": "https://assets.ppy.sh/beatmaps/2070848/covers/card.jpg?1699938283", + "card@2x": "https://assets.ppy.sh/beatmaps/2070848/covers/card@2x.jpg?1699938283", + "list": "https://assets.ppy.sh/beatmaps/2070848/covers/list.jpg?1699938283", + "list@2x": "https://assets.ppy.sh/beatmaps/2070848/covers/list@2x.jpg?1699938283", + "slimcover": "https://assets.ppy.sh/beatmaps/2070848/covers/slimcover.jpg?1699938283", + "slimcover@2x": "https://assets.ppy.sh/beatmaps/2070848/covers/slimcover@2x.jpg?1699938283" + }, + "creator": "Beige", + "favourite_count": 476, + "hype": null, + "id": 2070848, + "nsfw": true, + "offset": 0, + "play_count": 242357, + "preview_url": "//b.ppy.sh/preview/2070848.mp3", + "source": "", + "spotlight": false, + "status": "ranked", + "title": "HITO Mania", + "title_unicode": "人マニア", + "track_id": null, + "user_id": 685229, + "video": true, + "bpm": 150, + "can_be_hyped": false, + "deleted_at": null, + "discussion_enabled": true, + "discussion_locked": false, + "is_scoreable": true, + "last_updated": "2023-11-14T05:04:25Z", + "legacy_thread_url": "https://osu.ppy.sh/community/forums/topics/1829858", + "nominations_summary": { + "current": 2, + "eligible_main_rulesets": ["osu"], + "required_meta": { + "main_ruleset": 2, + "non_main_ruleset": 1 + } + }, + "ranked": 1, + "ranked_date": "2023-11-22T01:45:28Z", + "storyboard": false, + "submitted_date": "2023-10-06T16:08:50Z", + "tags": "w_h_i_t_e vocaloid vippaloid utau synthesizer v ai synthv the vocaloid collection 2023 summer ボカコレ2023夏 japanese pop human enthusiast man maniac white w h i t e", + "availability": { + "download_disabled": false, + "more_information": null + }, + "beatmaps": [ + { + "beatmapset_id": 2070848, + "difficulty_rating": 6.22, + "id": 4333021, + "mode": "osu", + "status": "ranked", + "total_length": 116, + "user_id": 685229, + "version": "3333", + "accuracy": 8.8, + "ar": 9.6, + "bpm": 150, + "convert": false, + "count_circles": 252, + "count_sliders": 208, + "count_spinners": 0, + "cs": 4, + "deleted_at": null, + "drain": 5.5, + "hit_length": 105, + "is_scoreable": true, + "last_updated": "2023-11-14T05:04:25Z", + "mode_int": 0, + "passcount": 20660, + "playcount": 66547, + "ranked": 1, + "url": "https://osu.ppy.sh/beatmaps/4333021", + "checksum": "e5b685bbcb926fd4a51bdc131271cf60", + "failtimes": { + "fail": [ + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 9, + 63, + 72, + 18, + 0, + 9, + 9, + 36, + 90, + 36, + 45, + 45, + 27, + 18, + 27, + 45, + 9, + 18, + 27, + 18, + 45, + 27, + 198, + 198, + 135, + 90, + 0, + 459, + 378, + 153, + 378, + 108, + 657, + 657, + 144, + 432, + 1422, + 1035, + 387, + 225, + 126, + 45, + 99, + 45, + 45, + 54, + 63, + 63, + 9, + 18, + 45, + 9, + 0, + 9, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 171, + 126, + 0, + 0, + 63, + 0, + 81, + 270, + 171, + 153, + 225, + 63, + 72, + 36, + 9, + 27, + 180, + 234, + 423, + 189, + 216, + 135, + 432, + 72, + 27 + ], + "exit": [ + 0, + 0, + 0, + 27, + 9, + 18, + 684, + 819, + 864, + 792, + 495, + 558, + 531, + 459, + 405, + 333, + 441, + 315, + 693, + 234, + 216, + 225, + 549, + 288, + 153, + 225, + 315, + 144, + 162, + 360, + 801, + 621, + 144, + 189, + 432, + 765, + 342, + 1098, + 774, + 459, + 558, + 378, + 252, + 468, + 1485, + 693, + 243, + 387, + 153, + 657, + 684, + 360, + 360, + 252, + 225, + 405, + 279, + 441, + 324, + 63, + 54, + 171, + 198, + 45, + 18, + 27, + 126, + 342, + 234, + 63, + 18, + 0, + 0, + 0, + 18, + 27, + 405, + 99, + 27, + 0, + 0, + 99, + 126, + 162, + 90, + 36, + 63, + 63, + 135, + 27, + 36, + 81, + 180, + 90, + 216, + 108, + 54, + 306, + 297, + 450 + ] + }, + "max_combo": 687 + }, + { + "beatmapset_id": 2070848, + "difficulty_rating": 3.08, + "id": 4342176, + "mode": "osu", + "status": "ranked", + "total_length": 115, + "user_id": 685229, + "version": "Hard", + "accuracy": 6.8, + "ar": 7.7, + "bpm": 150, + "convert": false, + "count_circles": 82, + "count_sliders": 158, + "count_spinners": 1, + "cs": 3.8, + "deleted_at": null, + "drain": 4, + "hit_length": 105, + "is_scoreable": true, + "last_updated": "2023-11-14T05:04:26Z", + "mode_int": 0, + "passcount": 14892, + "playcount": 33472, + "ranked": 1, + "url": "https://osu.ppy.sh/beatmaps/4342176", + "checksum": "89e5febacb4a8668f0e0e97c1bacb4c7", + "failtimes": { + "fail": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 18, + 63, + 63, + 72, + 72, + 36, + 54, + 108, + 81, + 72, + 45, + 36, + 9, + 36, + 54, + 27, + 45, + 117, + 117, + 306, + 45, + 45, + 0, + 54, + 54, + 27, + 72, + 27, + 18, + 9, + 0, + 0, + 36, + 189, + 180, + 477, + 450, + 18, + 72, + 90, + 162, + 45, + 18, + 27, + 63, + 36, + 54, + 9, + 9, + 9, + 9, + 9, + 27, + 9, + 0, + 0, + 9, + 0, + 0, + 9, + 0, + 0, + 0, + 0, + 0, + 9, + 9, + 9, + 0, + 9, + 0, + 0, + 0, + 9, + 18, + 45, + 9, + 0, + 18, + 9, + 0, + 0, + 9, + 0, + 0, + 27, + 18, + 9, + 9, + 27, + 27, + 9, + 36 + ], + "exit": [ + 0, + 0, + 0, + 9, + 18, + 9, + 63, + 81, + 171, + 612, + 441, + 558, + 387, + 207, + 261, + 270, + 198, + 270, + 198, + 108, + 81, + 153, + 117, + 72, + 117, + 252, + 297, + 315, + 198, + 90, + 90, + 279, + 90, + 81, + 27, + 81, + 144, + 135, + 99, + 9, + 99, + 477, + 486, + 315, + 108, + 90, + 108, + 153, + 54, + 117, + 99, + 144, + 99, + 90, + 45, + 54, + 63, + 27, + 63, + 54, + 18, + 72, + 0, + 36, + 36, + 18, + 9, + 189, + 243, + 117, + 72, + 36, + 9, + 9, + 9, + 9, + 9, + 0, + 0, + 36, + 36, + 54, + 18, + 45, + 9, + 27, + 36, + 27, + 9, + 36, + 18, + 18, + 36, + 54, + 36, + 18, + 27, + 9, + 36, + 153 + ] + }, + "max_combo": 433 + }, { - "beatmapSetId": 2069845 + "beatmapset_id": 2070848, + "difficulty_rating": 1.79, + "id": 4342177, + "mode": "osu", + "status": "ranked", + "total_length": 115, + "user_id": 685229, + "version": "Normal", + "accuracy": 4, + "ar": 5.5, + "bpm": 150, + "convert": false, + "count_circles": 39, + "count_sliders": 112, + "count_spinners": 1, + "cs": 3.5, + "deleted_at": null, + "drain": 3, + "hit_length": 104, + "is_scoreable": true, + "last_updated": "2023-11-14T05:04:26Z", + "mode_int": 0, + "passcount": 5478, + "playcount": 12511, + "ranked": 1, + "url": "https://osu.ppy.sh/beatmaps/4342177", + "checksum": "3b7c0bb338564060ab512b340a07f011", + "failtimes": { + "fail": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 117, + 54, + 63, + 72, + 18, + 72, + 0, + 45, + 90, + 99, + 18, + 27, + 54, + 0, + 36, + 36, + 54, + 9, + 18, + 18, + 9, + 54, + 18, + 9, + 27, + 9, + 63, + 0, + 0, + 0, + 9, + 36, + 9, + 27, + 0, + 9, + 72, + 9, + 18, + 36, + 45, + 18, + 18, + 36, + 27, + 9, + 45, + 9, + 27, + 0, + 9, + 9, + 0, + 36, + 0, + 9, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 18, + 0, + 0, + 0, + 0, + 9, + 9, + 0, + 0, + 9, + 0, + 36, + 0, + 9, + 0, + 0, + 0, + 9, + 9 + ], + "exit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 27, + 90, + 63, + 63, + 81, + 54, + 81, + 54, + 171, + 90, + 126, + 45, + 27, + 63, + 54, + 108, + 90, + 63, + 36, + 36, + 27, + 54, + 63, + 0, + 36, + 45, + 9, + 27, + 9, + 54, + 9, + 27, + 45, + 63, + 36, + 18, + 108, + 63, + 18, + 36, + 27, + 18, + 63, + 54, + 18, + 18, + 36, + 9, + 72, + 54, + 18, + 0, + 27, + 27, + 9, + 27, + 18, + 0, + 27, + 45, + 18, + 27, + 18, + 0, + 0, + 9, + 0, + 0, + 0, + 0, + 9, + 0, + 0, + 18, + 9, + 0, + 9, + 0, + 0, + 9, + 36, + 18, + 36, + 36, + 27, + 18, + 9, + 0, + 0, + 0, + 18 + ] + }, + "max_combo": 308 + }, + { + "beatmapset_id": 2070848, + "difficulty_rating": 5.02, + "id": 4345774, + "mode": "osu", + "status": "ranked", + "total_length": 115, + "user_id": 685229, + "version": "Extra", + "accuracy": 8.3, + "ar": 9.3, + "bpm": 150, + "convert": false, + "count_circles": 196, + "count_sliders": 211, + "count_spinners": 0, + "cs": 3, + "deleted_at": null, + "drain": 5.3, + "hit_length": 104, + "is_scoreable": true, + "last_updated": "2023-11-14T05:04:27Z", + "mode_int": 0, + "passcount": 19658, + "playcount": 68122, + "ranked": 1, + "url": "https://osu.ppy.sh/beatmaps/4345774", + "checksum": "1eeb1523c281f739f2ad4d06b283207a", + "failtimes": { + "fail": [ + 0, + 0, + 0, + 0, + 0, + 0, + 18, + 36, + 288, + 243, + 378, + 351, + 369, + 405, + 684, + 333, + 90, + 135, + 135, + 261, + 36, + 729, + 387, + 108, + 45, + 54, + 27, + 9, + 81, + 72, + 180, + 63, + 270, + 261, + 306, + 126, + 189, + 630, + 63, + 630, + 1485, + 621, + 468, + 684, + 3456, + 594, + 927, + 756, + 450, + 171, + 162, + 81, + 45, + 45, + 0, + 27, + 9, + 18, + 18, + 27, + 9, + 9, + 9, + 36, + 18, + 18, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 27, + 0, + 18, + 0, + 0, + 0, + 36, + 18, + 36, + 594, + 306, + 378, + 216, + 117, + 90, + 54, + 36, + 18, + 18, + 0, + 387, + 153, + 81, + 117, + 0, + 18 + ], + "exit": [ + 0, + 0, + 0, + 45, + 63, + 36, + 225, + 873, + 1035, + 1341, + 819, + 630, + 693, + 423, + 864, + 801, + 441, + 459, + 558, + 297, + 198, + 252, + 396, + 207, + 117, + 342, + 153, + 63, + 108, + 180, + 171, + 90, + 90, + 135, + 405, + 315, + 171, + 72, + 252, + 117, + 396, + 369, + 315, + 252, + 1098, + 1098, + 315, + 450, + 234, + 369, + 441, + 243, + 162, + 72, + 90, + 261, + 360, + 261, + 99, + 108, + 54, + 99, + 72, + 63, + 27, + 18, + 9, + 360, + 252, + 99, + 54, + 18, + 18, + 18, + 0, + 9, + 9, + 63, + 0, + 18, + 9, + 9, + 27, + 234, + 270, + 180, + 108, + 72, + 72, + 54, + 54, + 90, + 27, + 18, + 135, + 333, + 45, + 108, + 72, + 477 + ] + }, + "max_combo": 639 + }, + { + "beatmapset_id": 2070848, + "difficulty_rating": 4.17, + "id": 4345775, + "mode": "osu", + "status": "ranked", + "total_length": 115, + "user_id": 685229, + "version": "Insane", + "accuracy": 7.8, + "ar": 8.5, + "bpm": 150, + "convert": false, + "count_circles": 185, + "count_sliders": 142, + "count_spinners": 1, + "cs": 4, + "deleted_at": null, + "drain": 5, + "hit_length": 105, + "is_scoreable": true, + "last_updated": "2023-11-14T05:04:28Z", + "mode_int": 0, + "passcount": 23022, + "playcount": 61705, + "ranked": 1, + "url": "https://osu.ppy.sh/beatmaps/4345775", + "checksum": "dd1591e11ec3ec99d516540dde7d50f7", + "failtimes": { + "fail": [ + 0, + 0, + 0, + 0, + 0, + 0, + 36, + 54, + 72, + 144, + 216, + 342, + 315, + 207, + 270, + 171, + 81, + 180, + 108, + 117, + 54, + 270, + 99, + 81, + 162, + 144, + 162, + 81, + 45, + 36, + 189, + 81, + 45, + 90, + 171, + 189, + 45, + 0, + 18, + 54, + 135, + 144, + 198, + 144, + 135, + 72, + 297, + 54, + 36, + 54, + 81, + 18, + 27, + 54, + 63, + 36, + 36, + 36, + 54, + 45, + 18, + 0, + 27, + 9, + 9, + 0, + 18, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 18, + 18, + 18, + 0, + 0, + 9, + 180, + 180, + 198, + 180, + 36, + 351, + 90, + 99, + 18, + 36, + 54, + 108, + 99, + 18, + 81, + 9, + 36, + 171, + 36, + 45 + ], + "exit": [ + 0, + 0, + 0, + 27, + 18, + 27, + 198, + 837, + 828, + 1224, + 567, + 909, + 1134, + 396, + 324, + 558, + 504, + 522, + 1566, + 324, + 153, + 378, + 1062, + 198, + 252, + 270, + 306, + 189, + 108, + 324, + 486, + 504, + 108, + 405, + 162, + 189, + 225, + 144, + 54, + 36, + 126, + 342, + 108, + 486, + 306, + 639, + 288, + 369, + 153, + 225, + 180, + 99, + 135, + 108, + 261, + 135, + 297, + 216, + 135, + 126, + 99, + 261, + 45, + 18, + 36, + 27, + 45, + 612, + 612, + 207, + 81, + 45, + 18, + 54, + 18, + 18, + 9, + 0, + 18, + 9, + 117, + 90, + 153, + 81, + 117, + 90, + 126, + 27, + 81, + 90, + 99, + 27, + 99, + 126, + 45, + 90, + 27, + 99, + 72, + 387 + ] + }, + "max_combo": 496 } - ], - "downloadOsuBeatmap": [ + ], + "converts": [ + { + "beatmapset_id": 2070848, + "difficulty_rating": 4.71, + "id": 4333021, + "mode": "taiko", + "status": "ranked", + "total_length": 116, + "user_id": 685229, + "version": "3333", + "accuracy": 8.8, + "ar": 9.6, + "bpm": 150, + "convert": true, + "count_circles": 252, + "count_sliders": 208, + "count_spinners": 0, + "cs": 4, + "deleted_at": null, + "drain": 5.5, + "hit_length": 105, + "is_scoreable": true, + "last_updated": "2023-11-14T05:04:25Z", + "mode_int": 1, + "passcount": 20660, + "playcount": 66547, + "ranked": 1, + "url": "https://osu.ppy.sh/beatmaps/4333021", + "checksum": "e5b685bbcb926fd4a51bdc131271cf60", + "failtimes": { + "fail": [ + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 9, + 63, + 72, + 18, + 0, + 9, + 9, + 36, + 90, + 36, + 45, + 45, + 27, + 18, + 27, + 45, + 9, + 18, + 27, + 18, + 45, + 27, + 198, + 198, + 135, + 90, + 0, + 459, + 378, + 153, + 378, + 108, + 657, + 657, + 144, + 432, + 1422, + 1035, + 387, + 225, + 126, + 45, + 99, + 45, + 45, + 54, + 63, + 63, + 9, + 18, + 45, + 9, + 0, + 9, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 171, + 126, + 0, + 0, + 63, + 0, + 81, + 270, + 171, + 153, + 225, + 63, + 72, + 36, + 9, + 27, + 180, + 234, + 423, + 189, + 216, + 135, + 432, + 72, + 27 + ], + "exit": [ + 0, + 0, + 0, + 27, + 9, + 18, + 684, + 819, + 864, + 792, + 495, + 558, + 531, + 459, + 405, + 333, + 441, + 315, + 693, + 234, + 216, + 225, + 549, + 288, + 153, + 225, + 315, + 144, + 162, + 360, + 801, + 621, + 144, + 189, + 432, + 765, + 342, + 1098, + 774, + 459, + 558, + 378, + 252, + 468, + 1485, + 693, + 243, + 387, + 153, + 657, + 684, + 360, + 360, + 252, + 225, + 405, + 279, + 441, + 324, + 63, + 54, + 171, + 198, + 45, + 18, + 27, + 126, + 342, + 234, + 63, + 18, + 0, + 0, + 0, + 18, + 27, + 405, + 99, + 27, + 0, + 0, + 99, + 126, + 162, + 90, + 36, + 63, + 63, + 135, + 27, + 36, + 81, + 180, + 90, + 216, + 108, + 54, + 306, + 297, + 450 + ] + } + }, + { + "beatmapset_id": 2070848, + "difficulty_rating": 4.94, + "id": 4333021, + "mode": "fruits", + "status": "ranked", + "total_length": 116, + "user_id": 685229, + "version": "3333", + "accuracy": 8.8, + "ar": 9.6, + "bpm": 150, + "convert": true, + "count_circles": 252, + "count_sliders": 208, + "count_spinners": 0, + "cs": 4, + "deleted_at": null, + "drain": 5.5, + "hit_length": 105, + "is_scoreable": true, + "last_updated": "2023-11-14T05:04:25Z", + "mode_int": 2, + "passcount": 20660, + "playcount": 66547, + "ranked": 1, + "url": "https://osu.ppy.sh/beatmaps/4333021", + "checksum": "e5b685bbcb926fd4a51bdc131271cf60", + "failtimes": { + "fail": [ + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 9, + 63, + 72, + 18, + 0, + 9, + 9, + 36, + 90, + 36, + 45, + 45, + 27, + 18, + 27, + 45, + 9, + 18, + 27, + 18, + 45, + 27, + 198, + 198, + 135, + 90, + 0, + 459, + 378, + 153, + 378, + 108, + 657, + 657, + 144, + 432, + 1422, + 1035, + 387, + 225, + 126, + 45, + 99, + 45, + 45, + 54, + 63, + 63, + 9, + 18, + 45, + 9, + 0, + 9, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 171, + 126, + 0, + 0, + 63, + 0, + 81, + 270, + 171, + 153, + 225, + 63, + 72, + 36, + 9, + 27, + 180, + 234, + 423, + 189, + 216, + 135, + 432, + 72, + 27 + ], + "exit": [ + 0, + 0, + 0, + 27, + 9, + 18, + 684, + 819, + 864, + 792, + 495, + 558, + 531, + 459, + 405, + 333, + 441, + 315, + 693, + 234, + 216, + 225, + 549, + 288, + 153, + 225, + 315, + 144, + 162, + 360, + 801, + 621, + 144, + 189, + 432, + 765, + 342, + 1098, + 774, + 459, + 558, + 378, + 252, + 468, + 1485, + 693, + 243, + 387, + 153, + 657, + 684, + 360, + 360, + 252, + 225, + 405, + 279, + 441, + 324, + 63, + 54, + 171, + 198, + 45, + 18, + 27, + 126, + 342, + 234, + 63, + 18, + 0, + 0, + 0, + 18, + 27, + 405, + 99, + 27, + 0, + 0, + 99, + 126, + 162, + 90, + 36, + 63, + 63, + 135, + 27, + 36, + 81, + 180, + 90, + 216, + 108, + 54, + 306, + 297, + 450 + ] + } + }, + { + "beatmapset_id": 2070848, + "difficulty_rating": 3.05, + "id": 4333021, + "mode": "mania", + "status": "ranked", + "total_length": 116, + "user_id": 685229, + "version": "[7K] 3333", + "accuracy": 8.8, + "ar": 9.6, + "bpm": 150, + "convert": true, + "count_circles": 252, + "count_sliders": 208, + "count_spinners": 0, + "cs": 7, + "deleted_at": null, + "drain": 5.5, + "hit_length": 105, + "is_scoreable": true, + "last_updated": "2023-11-14T05:04:25Z", + "mode_int": 3, + "passcount": 20660, + "playcount": 66547, + "ranked": 1, + "url": "https://osu.ppy.sh/beatmaps/4333021", + "checksum": "e5b685bbcb926fd4a51bdc131271cf60", + "failtimes": { + "fail": [ + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 9, + 63, + 72, + 18, + 0, + 9, + 9, + 36, + 90, + 36, + 45, + 45, + 27, + 18, + 27, + 45, + 9, + 18, + 27, + 18, + 45, + 27, + 198, + 198, + 135, + 90, + 0, + 459, + 378, + 153, + 378, + 108, + 657, + 657, + 144, + 432, + 1422, + 1035, + 387, + 225, + 126, + 45, + 99, + 45, + 45, + 54, + 63, + 63, + 9, + 18, + 45, + 9, + 0, + 9, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 171, + 126, + 0, + 0, + 63, + 0, + 81, + 270, + 171, + 153, + 225, + 63, + 72, + 36, + 9, + 27, + 180, + 234, + 423, + 189, + 216, + 135, + 432, + 72, + 27 + ], + "exit": [ + 0, + 0, + 0, + 27, + 9, + 18, + 684, + 819, + 864, + 792, + 495, + 558, + 531, + 459, + 405, + 333, + 441, + 315, + 693, + 234, + 216, + 225, + 549, + 288, + 153, + 225, + 315, + 144, + 162, + 360, + 801, + 621, + 144, + 189, + 432, + 765, + 342, + 1098, + 774, + 459, + 558, + 378, + 252, + 468, + 1485, + 693, + 243, + 387, + 153, + 657, + 684, + 360, + 360, + 252, + 225, + 405, + 279, + 441, + 324, + 63, + 54, + 171, + 198, + 45, + 18, + 27, + 126, + 342, + 234, + 63, + 18, + 0, + 0, + 0, + 18, + 27, + 405, + 99, + 27, + 0, + 0, + 99, + 126, + 162, + 90, + 36, + 63, + 63, + 135, + 27, + 36, + 81, + 180, + 90, + 216, + 108, + 54, + 306, + 297, + 450 + ] + } + }, + { + "beatmapset_id": 2070848, + "difficulty_rating": 3.31, + "id": 4342176, + "mode": "taiko", + "status": "ranked", + "total_length": 115, + "user_id": 685229, + "version": "Hard", + "accuracy": 6.8, + "ar": 7.7, + "bpm": 150, + "convert": true, + "count_circles": 82, + "count_sliders": 158, + "count_spinners": 1, + "cs": 3.8, + "deleted_at": null, + "drain": 4, + "hit_length": 105, + "is_scoreable": true, + "last_updated": "2023-11-14T05:04:26Z", + "mode_int": 1, + "passcount": 14892, + "playcount": 33472, + "ranked": 1, + "url": "https://osu.ppy.sh/beatmaps/4342176", + "checksum": "89e5febacb4a8668f0e0e97c1bacb4c7", + "failtimes": { + "fail": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 18, + 63, + 63, + 72, + 72, + 36, + 54, + 108, + 81, + 72, + 45, + 36, + 9, + 36, + 54, + 27, + 45, + 117, + 117, + 306, + 45, + 45, + 0, + 54, + 54, + 27, + 72, + 27, + 18, + 9, + 0, + 0, + 36, + 189, + 180, + 477, + 450, + 18, + 72, + 90, + 162, + 45, + 18, + 27, + 63, + 36, + 54, + 9, + 9, + 9, + 9, + 9, + 27, + 9, + 0, + 0, + 9, + 0, + 0, + 9, + 0, + 0, + 0, + 0, + 0, + 9, + 9, + 9, + 0, + 9, + 0, + 0, + 0, + 9, + 18, + 45, + 9, + 0, + 18, + 9, + 0, + 0, + 9, + 0, + 0, + 27, + 18, + 9, + 9, + 27, + 27, + 9, + 36 + ], + "exit": [ + 0, + 0, + 0, + 9, + 18, + 9, + 63, + 81, + 171, + 612, + 441, + 558, + 387, + 207, + 261, + 270, + 198, + 270, + 198, + 108, + 81, + 153, + 117, + 72, + 117, + 252, + 297, + 315, + 198, + 90, + 90, + 279, + 90, + 81, + 27, + 81, + 144, + 135, + 99, + 9, + 99, + 477, + 486, + 315, + 108, + 90, + 108, + 153, + 54, + 117, + 99, + 144, + 99, + 90, + 45, + 54, + 63, + 27, + 63, + 54, + 18, + 72, + 0, + 36, + 36, + 18, + 9, + 189, + 243, + 117, + 72, + 36, + 9, + 9, + 9, + 9, + 9, + 0, + 0, + 36, + 36, + 54, + 18, + 45, + 9, + 27, + 36, + 27, + 9, + 36, + 18, + 18, + 36, + 54, + 36, + 18, + 27, + 9, + 36, + 153 + ] + } + }, + { + "beatmapset_id": 2070848, + "difficulty_rating": 1.86, + "id": 4342176, + "mode": "fruits", + "status": "ranked", + "total_length": 115, + "user_id": 685229, + "version": "Hard", + "accuracy": 6.8, + "ar": 7.7, + "bpm": 150, + "convert": true, + "count_circles": 82, + "count_sliders": 158, + "count_spinners": 1, + "cs": 3.8, + "deleted_at": null, + "drain": 4, + "hit_length": 105, + "is_scoreable": true, + "last_updated": "2023-11-14T05:04:26Z", + "mode_int": 2, + "passcount": 14892, + "playcount": 33472, + "ranked": 1, + "url": "https://osu.ppy.sh/beatmaps/4342176", + "checksum": "89e5febacb4a8668f0e0e97c1bacb4c7", + "failtimes": { + "fail": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 18, + 63, + 63, + 72, + 72, + 36, + 54, + 108, + 81, + 72, + 45, + 36, + 9, + 36, + 54, + 27, + 45, + 117, + 117, + 306, + 45, + 45, + 0, + 54, + 54, + 27, + 72, + 27, + 18, + 9, + 0, + 0, + 36, + 189, + 180, + 477, + 450, + 18, + 72, + 90, + 162, + 45, + 18, + 27, + 63, + 36, + 54, + 9, + 9, + 9, + 9, + 9, + 27, + 9, + 0, + 0, + 9, + 0, + 0, + 9, + 0, + 0, + 0, + 0, + 0, + 9, + 9, + 9, + 0, + 9, + 0, + 0, + 0, + 9, + 18, + 45, + 9, + 0, + 18, + 9, + 0, + 0, + 9, + 0, + 0, + 27, + 18, + 9, + 9, + 27, + 27, + 9, + 36 + ], + "exit": [ + 0, + 0, + 0, + 9, + 18, + 9, + 63, + 81, + 171, + 612, + 441, + 558, + 387, + 207, + 261, + 270, + 198, + 270, + 198, + 108, + 81, + 153, + 117, + 72, + 117, + 252, + 297, + 315, + 198, + 90, + 90, + 279, + 90, + 81, + 27, + 81, + 144, + 135, + 99, + 9, + 99, + 477, + 486, + 315, + 108, + 90, + 108, + 153, + 54, + 117, + 99, + 144, + 99, + 90, + 45, + 54, + 63, + 27, + 63, + 54, + 18, + 72, + 0, + 36, + 36, + 18, + 9, + 189, + 243, + 117, + 72, + 36, + 9, + 9, + 9, + 9, + 9, + 0, + 0, + 36, + 36, + 54, + 18, + 45, + 9, + 27, + 36, + 27, + 9, + 36, + 18, + 18, + 36, + 54, + 36, + 18, + 27, + 9, + 36, + 153 + ] + } + }, + { + "beatmapset_id": 2070848, + "difficulty_rating": 1.78, + "id": 4342176, + "mode": "mania", + "status": "ranked", + "total_length": 115, + "user_id": 685229, + "version": "[5K] Hard", + "accuracy": 6.8, + "ar": 7.7, + "bpm": 150, + "convert": true, + "count_circles": 82, + "count_sliders": 158, + "count_spinners": 1, + "cs": 5, + "deleted_at": null, + "drain": 4, + "hit_length": 105, + "is_scoreable": true, + "last_updated": "2023-11-14T05:04:26Z", + "mode_int": 3, + "passcount": 14892, + "playcount": 33472, + "ranked": 1, + "url": "https://osu.ppy.sh/beatmaps/4342176", + "checksum": "89e5febacb4a8668f0e0e97c1bacb4c7", + "failtimes": { + "fail": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 18, + 63, + 63, + 72, + 72, + 36, + 54, + 108, + 81, + 72, + 45, + 36, + 9, + 36, + 54, + 27, + 45, + 117, + 117, + 306, + 45, + 45, + 0, + 54, + 54, + 27, + 72, + 27, + 18, + 9, + 0, + 0, + 36, + 189, + 180, + 477, + 450, + 18, + 72, + 90, + 162, + 45, + 18, + 27, + 63, + 36, + 54, + 9, + 9, + 9, + 9, + 9, + 27, + 9, + 0, + 0, + 9, + 0, + 0, + 9, + 0, + 0, + 0, + 0, + 0, + 9, + 9, + 9, + 0, + 9, + 0, + 0, + 0, + 9, + 18, + 45, + 9, + 0, + 18, + 9, + 0, + 0, + 9, + 0, + 0, + 27, + 18, + 9, + 9, + 27, + 27, + 9, + 36 + ], + "exit": [ + 0, + 0, + 0, + 9, + 18, + 9, + 63, + 81, + 171, + 612, + 441, + 558, + 387, + 207, + 261, + 270, + 198, + 270, + 198, + 108, + 81, + 153, + 117, + 72, + 117, + 252, + 297, + 315, + 198, + 90, + 90, + 279, + 90, + 81, + 27, + 81, + 144, + 135, + 99, + 9, + 99, + 477, + 486, + 315, + 108, + 90, + 108, + 153, + 54, + 117, + 99, + 144, + 99, + 90, + 45, + 54, + 63, + 27, + 63, + 54, + 18, + 72, + 0, + 36, + 36, + 18, + 9, + 189, + 243, + 117, + 72, + 36, + 9, + 9, + 9, + 9, + 9, + 0, + 0, + 36, + 36, + 54, + 18, + 45, + 9, + 27, + 36, + 27, + 9, + 36, + 18, + 18, + 36, + 54, + 36, + 18, + 27, + 9, + 36, + 153 + ] + } + }, { - "beatmapId": 2809623 + "beatmapset_id": 2070848, + "difficulty_rating": 2.39, + "id": 4342177, + "mode": "taiko", + "status": "ranked", + "total_length": 115, + "user_id": 685229, + "version": "Normal", + "accuracy": 4, + "ar": 5.5, + "bpm": 150, + "convert": true, + "count_circles": 39, + "count_sliders": 112, + "count_spinners": 1, + "cs": 3.5, + "deleted_at": null, + "drain": 3, + "hit_length": 104, + "is_scoreable": true, + "last_updated": "2023-11-14T05:04:26Z", + "mode_int": 1, + "passcount": 5478, + "playcount": 12511, + "ranked": 1, + "url": "https://osu.ppy.sh/beatmaps/4342177", + "checksum": "3b7c0bb338564060ab512b340a07f011", + "failtimes": { + "fail": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 117, + 54, + 63, + 72, + 18, + 72, + 0, + 45, + 90, + 99, + 18, + 27, + 54, + 0, + 36, + 36, + 54, + 9, + 18, + 18, + 9, + 54, + 18, + 9, + 27, + 9, + 63, + 0, + 0, + 0, + 9, + 36, + 9, + 27, + 0, + 9, + 72, + 9, + 18, + 36, + 45, + 18, + 18, + 36, + 27, + 9, + 45, + 9, + 27, + 0, + 9, + 9, + 0, + 36, + 0, + 9, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 18, + 0, + 0, + 0, + 0, + 9, + 9, + 0, + 0, + 9, + 0, + 36, + 0, + 9, + 0, + 0, + 0, + 9, + 9 + ], + "exit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 27, + 90, + 63, + 63, + 81, + 54, + 81, + 54, + 171, + 90, + 126, + 45, + 27, + 63, + 54, + 108, + 90, + 63, + 36, + 36, + 27, + 54, + 63, + 0, + 36, + 45, + 9, + 27, + 9, + 54, + 9, + 27, + 45, + 63, + 36, + 18, + 108, + 63, + 18, + 36, + 27, + 18, + 63, + 54, + 18, + 18, + 36, + 9, + 72, + 54, + 18, + 0, + 27, + 27, + 9, + 27, + 18, + 0, + 27, + 45, + 18, + 27, + 18, + 0, + 0, + 9, + 0, + 0, + 0, + 0, + 9, + 0, + 0, + 18, + 9, + 0, + 9, + 0, + 0, + 9, + 36, + 18, + 36, + 36, + 27, + 18, + 9, + 0, + 0, + 0, + 18 + ] + } + }, + { + "beatmapset_id": 2070848, + "difficulty_rating": 1.28, + "id": 4342177, + "mode": "fruits", + "status": "ranked", + "total_length": 115, + "user_id": 685229, + "version": "Normal", + "accuracy": 4, + "ar": 5.5, + "bpm": 150, + "convert": true, + "count_circles": 39, + "count_sliders": 112, + "count_spinners": 1, + "cs": 3.5, + "deleted_at": null, + "drain": 3, + "hit_length": 104, + "is_scoreable": true, + "last_updated": "2023-11-14T05:04:26Z", + "mode_int": 2, + "passcount": 5478, + "playcount": 12511, + "ranked": 1, + "url": "https://osu.ppy.sh/beatmaps/4342177", + "checksum": "3b7c0bb338564060ab512b340a07f011", + "failtimes": { + "fail": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 117, + 54, + 63, + 72, + 18, + 72, + 0, + 45, + 90, + 99, + 18, + 27, + 54, + 0, + 36, + 36, + 54, + 9, + 18, + 18, + 9, + 54, + 18, + 9, + 27, + 9, + 63, + 0, + 0, + 0, + 9, + 36, + 9, + 27, + 0, + 9, + 72, + 9, + 18, + 36, + 45, + 18, + 18, + 36, + 27, + 9, + 45, + 9, + 27, + 0, + 9, + 9, + 0, + 36, + 0, + 9, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 18, + 0, + 0, + 0, + 0, + 9, + 9, + 0, + 0, + 9, + 0, + 36, + 0, + 9, + 0, + 0, + 0, + 9, + 9 + ], + "exit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 27, + 90, + 63, + 63, + 81, + 54, + 81, + 54, + 171, + 90, + 126, + 45, + 27, + 63, + 54, + 108, + 90, + 63, + 36, + 36, + 27, + 54, + 63, + 0, + 36, + 45, + 9, + 27, + 9, + 54, + 9, + 27, + 45, + 63, + 36, + 18, + 108, + 63, + 18, + 36, + 27, + 18, + 63, + 54, + 18, + 18, + 36, + 9, + 72, + 54, + 18, + 0, + 27, + 27, + 9, + 27, + 18, + 0, + 27, + 45, + 18, + 27, + 18, + 0, + 0, + 9, + 0, + 0, + 0, + 0, + 9, + 0, + 0, + 18, + 9, + 0, + 9, + 0, + 0, + 9, + 36, + 18, + 36, + 36, + 27, + 18, + 9, + 0, + 0, + 0, + 18 + ] + } + }, + { + "beatmapset_id": 2070848, + "difficulty_rating": 1.13, + "id": 4342177, + "mode": "mania", + "status": "ranked", + "total_length": 115, + "user_id": 685229, + "version": "[4K] Normal", + "accuracy": 4, + "ar": 5.5, + "bpm": 150, + "convert": true, + "count_circles": 39, + "count_sliders": 112, + "count_spinners": 1, + "cs": 4, + "deleted_at": null, + "drain": 3, + "hit_length": 104, + "is_scoreable": true, + "last_updated": "2023-11-14T05:04:26Z", + "mode_int": 3, + "passcount": 5478, + "playcount": 12511, + "ranked": 1, + "url": "https://osu.ppy.sh/beatmaps/4342177", + "checksum": "3b7c0bb338564060ab512b340a07f011", + "failtimes": { + "fail": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 117, + 54, + 63, + 72, + 18, + 72, + 0, + 45, + 90, + 99, + 18, + 27, + 54, + 0, + 36, + 36, + 54, + 9, + 18, + 18, + 9, + 54, + 18, + 9, + 27, + 9, + 63, + 0, + 0, + 0, + 9, + 36, + 9, + 27, + 0, + 9, + 72, + 9, + 18, + 36, + 45, + 18, + 18, + 36, + 27, + 9, + 45, + 9, + 27, + 0, + 9, + 9, + 0, + 36, + 0, + 9, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 18, + 0, + 0, + 0, + 0, + 9, + 9, + 0, + 0, + 9, + 0, + 36, + 0, + 9, + 0, + 0, + 0, + 9, + 9 + ], + "exit": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 27, + 90, + 63, + 63, + 81, + 54, + 81, + 54, + 171, + 90, + 126, + 45, + 27, + 63, + 54, + 108, + 90, + 63, + 36, + 36, + 27, + 54, + 63, + 0, + 36, + 45, + 9, + 27, + 9, + 54, + 9, + 27, + 45, + 63, + 36, + 18, + 108, + 63, + 18, + 36, + 27, + 18, + 63, + 54, + 18, + 18, + 36, + 9, + 72, + 54, + 18, + 0, + 27, + 27, + 9, + 27, + 18, + 0, + 27, + 45, + 18, + 27, + 18, + 0, + 0, + 9, + 0, + 0, + 0, + 0, + 9, + 0, + 0, + 18, + 9, + 0, + 9, + 0, + 0, + 9, + 36, + 18, + 36, + 36, + 27, + 18, + 9, + 0, + 0, + 0, + 18 + ] + } + }, + { + "beatmapset_id": 2070848, + "difficulty_rating": 4.3, + "id": 4345774, + "mode": "taiko", + "status": "ranked", + "total_length": 115, + "user_id": 685229, + "version": "Extra", + "accuracy": 8.3, + "ar": 9.3, + "bpm": 150, + "convert": true, + "count_circles": 196, + "count_sliders": 211, + "count_spinners": 0, + "cs": 3, + "deleted_at": null, + "drain": 5.3, + "hit_length": 104, + "is_scoreable": true, + "last_updated": "2023-11-14T05:04:27Z", + "mode_int": 1, + "passcount": 19658, + "playcount": 68122, + "ranked": 1, + "url": "https://osu.ppy.sh/beatmaps/4345774", + "checksum": "1eeb1523c281f739f2ad4d06b283207a", + "failtimes": { + "fail": [ + 0, + 0, + 0, + 0, + 0, + 0, + 18, + 36, + 288, + 243, + 378, + 351, + 369, + 405, + 684, + 333, + 90, + 135, + 135, + 261, + 36, + 729, + 387, + 108, + 45, + 54, + 27, + 9, + 81, + 72, + 180, + 63, + 270, + 261, + 306, + 126, + 189, + 630, + 63, + 630, + 1485, + 621, + 468, + 684, + 3456, + 594, + 927, + 756, + 450, + 171, + 162, + 81, + 45, + 45, + 0, + 27, + 9, + 18, + 18, + 27, + 9, + 9, + 9, + 36, + 18, + 18, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 27, + 0, + 18, + 0, + 0, + 0, + 36, + 18, + 36, + 594, + 306, + 378, + 216, + 117, + 90, + 54, + 36, + 18, + 18, + 0, + 387, + 153, + 81, + 117, + 0, + 18 + ], + "exit": [ + 0, + 0, + 0, + 45, + 63, + 36, + 225, + 873, + 1035, + 1341, + 819, + 630, + 693, + 423, + 864, + 801, + 441, + 459, + 558, + 297, + 198, + 252, + 396, + 207, + 117, + 342, + 153, + 63, + 108, + 180, + 171, + 90, + 90, + 135, + 405, + 315, + 171, + 72, + 252, + 117, + 396, + 369, + 315, + 252, + 1098, + 1098, + 315, + 450, + 234, + 369, + 441, + 243, + 162, + 72, + 90, + 261, + 360, + 261, + 99, + 108, + 54, + 99, + 72, + 63, + 27, + 18, + 9, + 360, + 252, + 99, + 54, + 18, + 18, + 18, + 0, + 9, + 9, + 63, + 0, + 18, + 9, + 9, + 27, + 234, + 270, + 180, + 108, + 72, + 72, + 54, + 54, + 90, + 27, + 18, + 135, + 333, + 45, + 108, + 72, + 477 + ] + } + }, + { + "beatmapset_id": 2070848, + "difficulty_rating": 4.18, + "id": 4345774, + "mode": "fruits", + "status": "ranked", + "total_length": 115, + "user_id": 685229, + "version": "Extra", + "accuracy": 8.3, + "ar": 9.3, + "bpm": 150, + "convert": true, + "count_circles": 196, + "count_sliders": 211, + "count_spinners": 0, + "cs": 3, + "deleted_at": null, + "drain": 5.3, + "hit_length": 104, + "is_scoreable": true, + "last_updated": "2023-11-14T05:04:27Z", + "mode_int": 2, + "passcount": 19658, + "playcount": 68122, + "ranked": 1, + "url": "https://osu.ppy.sh/beatmaps/4345774", + "checksum": "1eeb1523c281f739f2ad4d06b283207a", + "failtimes": { + "fail": [ + 0, + 0, + 0, + 0, + 0, + 0, + 18, + 36, + 288, + 243, + 378, + 351, + 369, + 405, + 684, + 333, + 90, + 135, + 135, + 261, + 36, + 729, + 387, + 108, + 45, + 54, + 27, + 9, + 81, + 72, + 180, + 63, + 270, + 261, + 306, + 126, + 189, + 630, + 63, + 630, + 1485, + 621, + 468, + 684, + 3456, + 594, + 927, + 756, + 450, + 171, + 162, + 81, + 45, + 45, + 0, + 27, + 9, + 18, + 18, + 27, + 9, + 9, + 9, + 36, + 18, + 18, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 27, + 0, + 18, + 0, + 0, + 0, + 36, + 18, + 36, + 594, + 306, + 378, + 216, + 117, + 90, + 54, + 36, + 18, + 18, + 0, + 387, + 153, + 81, + 117, + 0, + 18 + ], + "exit": [ + 0, + 0, + 0, + 45, + 63, + 36, + 225, + 873, + 1035, + 1341, + 819, + 630, + 693, + 423, + 864, + 801, + 441, + 459, + 558, + 297, + 198, + 252, + 396, + 207, + 117, + 342, + 153, + 63, + 108, + 180, + 171, + 90, + 90, + 135, + 405, + 315, + 171, + 72, + 252, + 117, + 396, + 369, + 315, + 252, + 1098, + 1098, + 315, + 450, + 234, + 369, + 441, + 243, + 162, + 72, + 90, + 261, + 360, + 261, + 99, + 108, + 54, + 99, + 72, + 63, + 27, + 18, + 9, + 360, + 252, + 99, + 54, + 18, + 18, + 18, + 0, + 9, + 9, + 63, + 0, + 18, + 9, + 9, + 27, + 234, + 270, + 180, + 108, + 72, + 72, + 54, + 54, + 90, + 27, + 18, + 135, + 333, + 45, + 108, + 72, + 477 + ] + } + }, + { + "beatmapset_id": 2070848, + "difficulty_rating": 2.57, + "id": 4345774, + "mode": "mania", + "status": "ranked", + "total_length": 115, + "user_id": 685229, + "version": "[7K] Extra", + "accuracy": 8.3, + "ar": 9.3, + "bpm": 150, + "convert": true, + "count_circles": 196, + "count_sliders": 211, + "count_spinners": 0, + "cs": 7, + "deleted_at": null, + "drain": 5.3, + "hit_length": 104, + "is_scoreable": true, + "last_updated": "2023-11-14T05:04:27Z", + "mode_int": 3, + "passcount": 19658, + "playcount": 68122, + "ranked": 1, + "url": "https://osu.ppy.sh/beatmaps/4345774", + "checksum": "1eeb1523c281f739f2ad4d06b283207a", + "failtimes": { + "fail": [ + 0, + 0, + 0, + 0, + 0, + 0, + 18, + 36, + 288, + 243, + 378, + 351, + 369, + 405, + 684, + 333, + 90, + 135, + 135, + 261, + 36, + 729, + 387, + 108, + 45, + 54, + 27, + 9, + 81, + 72, + 180, + 63, + 270, + 261, + 306, + 126, + 189, + 630, + 63, + 630, + 1485, + 621, + 468, + 684, + 3456, + 594, + 927, + 756, + 450, + 171, + 162, + 81, + 45, + 45, + 0, + 27, + 9, + 18, + 18, + 27, + 9, + 9, + 9, + 36, + 18, + 18, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 27, + 0, + 18, + 0, + 0, + 0, + 36, + 18, + 36, + 594, + 306, + 378, + 216, + 117, + 90, + 54, + 36, + 18, + 18, + 0, + 387, + 153, + 81, + 117, + 0, + 18 + ], + "exit": [ + 0, + 0, + 0, + 45, + 63, + 36, + 225, + 873, + 1035, + 1341, + 819, + 630, + 693, + 423, + 864, + 801, + 441, + 459, + 558, + 297, + 198, + 252, + 396, + 207, + 117, + 342, + 153, + 63, + 108, + 180, + 171, + 90, + 90, + 135, + 405, + 315, + 171, + 72, + 252, + 117, + 396, + 369, + 315, + 252, + 1098, + 1098, + 315, + 450, + 234, + 369, + 441, + 243, + 162, + 72, + 90, + 261, + 360, + 261, + 99, + 108, + 54, + 99, + 72, + 63, + 27, + 18, + 9, + 360, + 252, + 99, + 54, + 18, + 18, + 18, + 0, + 9, + 9, + 63, + 0, + 18, + 9, + 9, + 27, + 234, + 270, + 180, + 108, + 72, + 72, + 54, + 54, + 90, + 27, + 18, + 135, + 333, + 45, + 108, + 72, + 477 + ] + } + }, + { + "beatmapset_id": 2070848, + "difficulty_rating": 3.79, + "id": 4345775, + "mode": "taiko", + "status": "ranked", + "total_length": 115, + "user_id": 685229, + "version": "Insane", + "accuracy": 7.8, + "ar": 8.5, + "bpm": 150, + "convert": true, + "count_circles": 185, + "count_sliders": 142, + "count_spinners": 1, + "cs": 4, + "deleted_at": null, + "drain": 5, + "hit_length": 105, + "is_scoreable": true, + "last_updated": "2023-11-14T05:04:28Z", + "mode_int": 1, + "passcount": 23022, + "playcount": 61705, + "ranked": 1, + "url": "https://osu.ppy.sh/beatmaps/4345775", + "checksum": "dd1591e11ec3ec99d516540dde7d50f7", + "failtimes": { + "fail": [ + 0, + 0, + 0, + 0, + 0, + 0, + 36, + 54, + 72, + 144, + 216, + 342, + 315, + 207, + 270, + 171, + 81, + 180, + 108, + 117, + 54, + 270, + 99, + 81, + 162, + 144, + 162, + 81, + 45, + 36, + 189, + 81, + 45, + 90, + 171, + 189, + 45, + 0, + 18, + 54, + 135, + 144, + 198, + 144, + 135, + 72, + 297, + 54, + 36, + 54, + 81, + 18, + 27, + 54, + 63, + 36, + 36, + 36, + 54, + 45, + 18, + 0, + 27, + 9, + 9, + 0, + 18, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 18, + 18, + 18, + 0, + 0, + 9, + 180, + 180, + 198, + 180, + 36, + 351, + 90, + 99, + 18, + 36, + 54, + 108, + 99, + 18, + 81, + 9, + 36, + 171, + 36, + 45 + ], + "exit": [ + 0, + 0, + 0, + 27, + 18, + 27, + 198, + 837, + 828, + 1224, + 567, + 909, + 1134, + 396, + 324, + 558, + 504, + 522, + 1566, + 324, + 153, + 378, + 1062, + 198, + 252, + 270, + 306, + 189, + 108, + 324, + 486, + 504, + 108, + 405, + 162, + 189, + 225, + 144, + 54, + 36, + 126, + 342, + 108, + 486, + 306, + 639, + 288, + 369, + 153, + 225, + 180, + 99, + 135, + 108, + 261, + 135, + 297, + 216, + 135, + 126, + 99, + 261, + 45, + 18, + 36, + 27, + 45, + 612, + 612, + 207, + 81, + 45, + 18, + 54, + 18, + 18, + 9, + 0, + 18, + 9, + 117, + 90, + 153, + 81, + 117, + 90, + 126, + 27, + 81, + 90, + 99, + 27, + 99, + 126, + 45, + 90, + 27, + 99, + 72, + 387 + ] + } + }, + { + "beatmapset_id": 2070848, + "difficulty_rating": 3.02, + "id": 4345775, + "mode": "fruits", + "status": "ranked", + "total_length": 115, + "user_id": 685229, + "version": "Insane", + "accuracy": 7.8, + "ar": 8.5, + "bpm": 150, + "convert": true, + "count_circles": 185, + "count_sliders": 142, + "count_spinners": 1, + "cs": 4, + "deleted_at": null, + "drain": 5, + "hit_length": 105, + "is_scoreable": true, + "last_updated": "2023-11-14T05:04:28Z", + "mode_int": 2, + "passcount": 23022, + "playcount": 61705, + "ranked": 1, + "url": "https://osu.ppy.sh/beatmaps/4345775", + "checksum": "dd1591e11ec3ec99d516540dde7d50f7", + "failtimes": { + "fail": [ + 0, + 0, + 0, + 0, + 0, + 0, + 36, + 54, + 72, + 144, + 216, + 342, + 315, + 207, + 270, + 171, + 81, + 180, + 108, + 117, + 54, + 270, + 99, + 81, + 162, + 144, + 162, + 81, + 45, + 36, + 189, + 81, + 45, + 90, + 171, + 189, + 45, + 0, + 18, + 54, + 135, + 144, + 198, + 144, + 135, + 72, + 297, + 54, + 36, + 54, + 81, + 18, + 27, + 54, + 63, + 36, + 36, + 36, + 54, + 45, + 18, + 0, + 27, + 9, + 9, + 0, + 18, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 18, + 18, + 18, + 0, + 0, + 9, + 180, + 180, + 198, + 180, + 36, + 351, + 90, + 99, + 18, + 36, + 54, + 108, + 99, + 18, + 81, + 9, + 36, + 171, + 36, + 45 + ], + "exit": [ + 0, + 0, + 0, + 27, + 18, + 27, + 198, + 837, + 828, + 1224, + 567, + 909, + 1134, + 396, + 324, + 558, + 504, + 522, + 1566, + 324, + 153, + 378, + 1062, + 198, + 252, + 270, + 306, + 189, + 108, + 324, + 486, + 504, + 108, + 405, + 162, + 189, + 225, + 144, + 54, + 36, + 126, + 342, + 108, + 486, + 306, + 639, + 288, + 369, + 153, + 225, + 180, + 99, + 135, + 108, + 261, + 135, + 297, + 216, + 135, + 126, + 99, + 261, + 45, + 18, + 36, + 27, + 45, + 612, + 612, + 207, + 81, + 45, + 18, + 54, + 18, + 18, + 9, + 0, + 18, + 9, + 117, + 90, + 153, + 81, + 117, + 90, + 126, + 27, + 81, + 90, + 99, + 27, + 99, + 126, + 45, + 90, + 27, + 99, + 72, + 387 + ] + } + }, + { + "beatmapset_id": 2070848, + "difficulty_rating": 2.38, + "id": 4345775, + "mode": "mania", + "status": "ranked", + "total_length": 115, + "user_id": 685229, + "version": "[7K] Insane", + "accuracy": 7.8, + "ar": 8.5, + "bpm": 150, + "convert": true, + "count_circles": 185, + "count_sliders": 142, + "count_spinners": 1, + "cs": 7, + "deleted_at": null, + "drain": 5, + "hit_length": 105, + "is_scoreable": true, + "last_updated": "2023-11-14T05:04:28Z", + "mode_int": 3, + "passcount": 23022, + "playcount": 61705, + "ranked": 1, + "url": "https://osu.ppy.sh/beatmaps/4345775", + "checksum": "dd1591e11ec3ec99d516540dde7d50f7", + "failtimes": { + "fail": [ + 0, + 0, + 0, + 0, + 0, + 0, + 36, + 54, + 72, + 144, + 216, + 342, + 315, + 207, + 270, + 171, + 81, + 180, + 108, + 117, + 54, + 270, + 99, + 81, + 162, + 144, + 162, + 81, + 45, + 36, + 189, + 81, + 45, + 90, + 171, + 189, + 45, + 0, + 18, + 54, + 135, + 144, + 198, + 144, + 135, + 72, + 297, + 54, + 36, + 54, + 81, + 18, + 27, + 54, + 63, + 36, + 36, + 36, + 54, + 45, + 18, + 0, + 27, + 9, + 9, + 0, + 18, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 18, + 18, + 18, + 0, + 0, + 9, + 180, + 180, + 198, + 180, + 36, + 351, + 90, + 99, + 18, + 36, + 54, + 108, + 99, + 18, + 81, + 9, + 36, + 171, + 36, + 45 + ], + "exit": [ + 0, + 0, + 0, + 27, + 18, + 27, + 198, + 837, + 828, + 1224, + 567, + 909, + 1134, + 396, + 324, + 558, + 504, + 522, + 1566, + 324, + 153, + 378, + 1062, + 198, + 252, + 270, + 306, + 189, + 108, + 324, + 486, + 504, + 108, + 405, + 162, + 189, + 225, + 144, + 54, + 36, + 126, + 342, + 108, + 486, + 306, + 639, + 288, + 369, + 153, + 225, + 180, + 99, + 135, + 108, + 261, + 135, + 297, + 216, + 135, + 126, + 99, + 261, + 45, + 18, + 36, + 27, + 45, + 612, + 612, + 207, + 81, + 45, + 18, + 54, + 18, + 18, + 9, + 0, + 18, + 9, + 117, + 90, + 153, + 81, + 117, + 90, + 126, + 27, + 81, + 90, + 99, + 27, + 99, + 126, + 45, + 90, + 27, + 99, + 72, + 387 + ] + } } - ], - "getBeatmapById": [ + ], + "current_nominations": [ + { + "beatmapset_id": 2070848, + "rulesets": ["osu"], + "reset": false, + "user_id": 1634445 + }, { - "beatmapId": 4342177, - "data": { - "beatmapset_id": 2070848, - "difficulty_rating": 1.79, - "id": 4342177, - "mode": "osu", - "status": "ranked", - "total_length": 115, - "user_id": 685229, - "version": "Normal", - "accuracy": 4, - "ar": 5.5, - "bpm": 150, - "convert": false, - "count_circles": 39, - "count_sliders": 112, - "count_spinners": 1, - "cs": 3.5, - "deleted_at": null, - "drain": 3, - "hit_length": 104, - "is_scoreable": true, - "last_updated": "2023-11-14T05:04:26Z", - "mode_int": 0, - "passcount": 5478, - "playcount": 12511, - "ranked": 1, - "url": "https://osu.ppy.sh/beatmaps/4342177", - "checksum": "3b7c0bb338564060ab512b340a07f011", - "failtimes": { - "fail": [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 117, 54, 63, 72, 18, 72, - 0, 45, 90, 99, 18, 27, 54, 0, 36, 36, 54, 9, 18, 18, - 9, 54, 18, 9, 27, 9, 63, 0, 0, 0, 9, 36, 9, 27, 0, - 9, 72, 9, 18, 36, 45, 18, 18, 36, 27, 9, 45, 9, 27, - 0, 9, 9, 0, 36, 0, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 9, 9, 0, 0, 9, 0, - 36, 0, 9, 0, 0, 0, 9, 9 - ], - "exit": [ - 0, 0, 0, 0, 0, 0, 0, 0, 9, 27, 90, 63, 63, 81, 54, - 81, 54, 171, 90, 126, 45, 27, 63, 54, 108, 90, 63, - 36, 36, 27, 54, 63, 0, 36, 45, 9, 27, 9, 54, 9, 27, - 45, 63, 36, 18, 108, 63, 18, 36, 27, 18, 63, 54, 18, - 18, 36, 9, 72, 54, 18, 0, 27, 27, 9, 27, 18, 0, 27, - 45, 18, 27, 18, 0, 0, 9, 0, 0, 0, 0, 9, 0, 0, 18, 9, - 0, 9, 0, 0, 9, 36, 18, 36, 36, 27, 18, 9, 0, 0, 0, - 18 - ] - }, - "max_combo": 308 - } + "beatmapset_id": 2070848, + "rulesets": ["osu"], + "reset": false, + "user_id": 2688581 } - ], - "getBeatmapsetById": [ + ], + "description": { + "description": "
Use this space to tell the world about your map. It helps to include a list of changes as your map is modded!

BN : Dailycare / Luscent
" + }, + "genre": { + "id": 10, + "name": "Electronic" + }, + "language": { + "id": 3, + "name": "Japanese" + }, + "pack_tags": ["S1364"], + "ratings": [0, 3, 1, 1, 2, 1, 3, 2, 13, 37, 263], + "related_users": [ + { + "avatar_url": "https://a.ppy.sh/685229?1726669685.jpeg", + "country_code": "KR", + "default_group": "default", + "id": 685229, + "is_active": true, + "is_bot": false, + "is_deleted": false, + "is_online": false, + "is_supporter": false, + "last_visit": null, + "pm_friends_only": false, + "profile_colour": null, + "username": "Beige" + }, + { + "avatar_url": "https://a.ppy.sh/1634445?1710517165.png", + "country_code": "KR", + "default_group": "bng", + "id": 1634445, + "is_active": true, + "is_bot": false, + "is_deleted": false, + "is_online": false, + "is_supporter": true, + "last_visit": null, + "pm_friends_only": false, + "profile_colour": "#A347EB", + "username": "Dailycare" + }, { - "beatmapSetId": 2070848, - "data": { - "artist": "Sasuke Haraguchi feat. Kasane Teto", - "artist_unicode": "原口沙輔 feat. 重音テト", - "covers": { - "cover": "https://assets.ppy.sh/beatmaps/2070848/covers/cover.jpg?1699938283", - "cover@2x": "https://assets.ppy.sh/beatmaps/2070848/covers/cover@2x.jpg?1699938283", - "card": "https://assets.ppy.sh/beatmaps/2070848/covers/card.jpg?1699938283", - "card@2x": "https://assets.ppy.sh/beatmaps/2070848/covers/card@2x.jpg?1699938283", - "list": "https://assets.ppy.sh/beatmaps/2070848/covers/list.jpg?1699938283", - "list@2x": "https://assets.ppy.sh/beatmaps/2070848/covers/list@2x.jpg?1699938283", - "slimcover": "https://assets.ppy.sh/beatmaps/2070848/covers/slimcover.jpg?1699938283", - "slimcover@2x": "https://assets.ppy.sh/beatmaps/2070848/covers/slimcover@2x.jpg?1699938283" - }, - "creator": "Beige", - "favourite_count": 476, - "hype": null, - "id": 2070848, - "nsfw": true, - "offset": 0, - "play_count": 242357, - "preview_url": "//b.ppy.sh/preview/2070848.mp3", - "source": "", - "spotlight": false, - "status": "ranked", - "title": "HITO Mania", - "title_unicode": "人マニア", - "track_id": null, - "user_id": 685229, - "video": true, - "bpm": 150, - "can_be_hyped": false, - "deleted_at": null, - "discussion_enabled": true, - "discussion_locked": false, - "is_scoreable": true, - "last_updated": "2023-11-14T05:04:25Z", - "legacy_thread_url": "https://osu.ppy.sh/community/forums/topics/1829858", - "nominations_summary": { - "current": 2, - "eligible_main_rulesets": ["osu"], - "required_meta": { - "main_ruleset": 2, - "non_main_ruleset": 1 - } - }, - "ranked": 1, - "ranked_date": "2023-11-22T01:45:28Z", - "storyboard": false, - "submitted_date": "2023-10-06T16:08:50Z", - "tags": "w_h_i_t_e vocaloid vippaloid utau synthesizer v ai synthv the vocaloid collection 2023 summer ボカコレ2023夏 japanese pop human enthusiast man maniac white w h i t e", - "availability": { - "download_disabled": false, - "more_information": null - }, - "beatmaps": [ - { - "beatmapset_id": 2070848, - "difficulty_rating": 6.22, - "id": 4333021, - "mode": "osu", - "status": "ranked", - "total_length": 116, - "user_id": 685229, - "version": "3333", - "accuracy": 8.8, - "ar": 9.6, - "bpm": 150, - "convert": false, - "count_circles": 252, - "count_sliders": 208, - "count_spinners": 0, - "cs": 4, - "deleted_at": null, - "drain": 5.5, - "hit_length": 105, - "is_scoreable": true, - "last_updated": "2023-11-14T05:04:25Z", - "mode_int": 0, - "passcount": 20660, - "playcount": 66547, - "ranked": 1, - "url": "https://osu.ppy.sh/beatmaps/4333021", - "checksum": "e5b685bbcb926fd4a51bdc131271cf60", - "failtimes": { - "fail": [ - 0, 0, 0, 0, 0, 0, 9, 9, 63, 72, 18, 0, 9, 9, - 36, 90, 36, 45, 45, 27, 18, 27, 45, 9, 18, - 27, 18, 45, 27, 198, 198, 135, 90, 0, 459, - 378, 153, 378, 108, 657, 657, 144, 432, - 1422, 1035, 387, 225, 126, 45, 99, 45, 45, - 54, 63, 63, 9, 18, 45, 9, 0, 9, 9, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 171, 126, 0, - 0, 63, 0, 81, 270, 171, 153, 225, 63, 72, - 36, 9, 27, 180, 234, 423, 189, 216, 135, - 432, 72, 27 - ], - "exit": [ - 0, 0, 0, 27, 9, 18, 684, 819, 864, 792, 495, - 558, 531, 459, 405, 333, 441, 315, 693, 234, - 216, 225, 549, 288, 153, 225, 315, 144, 162, - 360, 801, 621, 144, 189, 432, 765, 342, - 1098, 774, 459, 558, 378, 252, 468, 1485, - 693, 243, 387, 153, 657, 684, 360, 360, 252, - 225, 405, 279, 441, 324, 63, 54, 171, 198, - 45, 18, 27, 126, 342, 234, 63, 18, 0, 0, 0, - 18, 27, 405, 99, 27, 0, 0, 99, 126, 162, 90, - 36, 63, 63, 135, 27, 36, 81, 180, 90, 216, - 108, 54, 306, 297, 450 - ] - }, - "max_combo": 687 - }, - { - "beatmapset_id": 2070848, - "difficulty_rating": 3.08, - "id": 4342176, - "mode": "osu", - "status": "ranked", - "total_length": 115, - "user_id": 685229, - "version": "Hard", - "accuracy": 6.8, - "ar": 7.7, - "bpm": 150, - "convert": false, - "count_circles": 82, - "count_sliders": 158, - "count_spinners": 1, - "cs": 3.8, - "deleted_at": null, - "drain": 4, - "hit_length": 105, - "is_scoreable": true, - "last_updated": "2023-11-14T05:04:26Z", - "mode_int": 0, - "passcount": 14892, - "playcount": 33472, - "ranked": 1, - "url": "https://osu.ppy.sh/beatmaps/4342176", - "checksum": "89e5febacb4a8668f0e0e97c1bacb4c7", - "failtimes": { - "fail": [ - 0, 0, 0, 0, 0, 0, 0, 18, 63, 63, 72, 72, 36, - 54, 108, 81, 72, 45, 36, 9, 36, 54, 27, 45, - 117, 117, 306, 45, 45, 0, 54, 54, 27, 72, - 27, 18, 9, 0, 0, 36, 189, 180, 477, 450, 18, - 72, 90, 162, 45, 18, 27, 63, 36, 54, 9, 9, - 9, 9, 9, 27, 9, 0, 0, 9, 0, 0, 9, 0, 0, 0, - 0, 0, 9, 9, 9, 0, 9, 0, 0, 0, 9, 18, 45, 9, - 0, 18, 9, 0, 0, 9, 0, 0, 27, 18, 9, 9, 27, - 27, 9, 36 - ], - "exit": [ - 0, 0, 0, 9, 18, 9, 63, 81, 171, 612, 441, - 558, 387, 207, 261, 270, 198, 270, 198, 108, - 81, 153, 117, 72, 117, 252, 297, 315, 198, - 90, 90, 279, 90, 81, 27, 81, 144, 135, 99, - 9, 99, 477, 486, 315, 108, 90, 108, 153, 54, - 117, 99, 144, 99, 90, 45, 54, 63, 27, 63, - 54, 18, 72, 0, 36, 36, 18, 9, 189, 243, 117, - 72, 36, 9, 9, 9, 9, 9, 0, 0, 36, 36, 54, 18, - 45, 9, 27, 36, 27, 9, 36, 18, 18, 36, 54, - 36, 18, 27, 9, 36, 153 - ] - }, - "max_combo": 433 - }, - { - "beatmapset_id": 2070848, - "difficulty_rating": 1.79, - "id": 4342177, - "mode": "osu", - "status": "ranked", - "total_length": 115, - "user_id": 685229, - "version": "Normal", - "accuracy": 4, - "ar": 5.5, - "bpm": 150, - "convert": false, - "count_circles": 39, - "count_sliders": 112, - "count_spinners": 1, - "cs": 3.5, - "deleted_at": null, - "drain": 3, - "hit_length": 104, - "is_scoreable": true, - "last_updated": "2023-11-14T05:04:26Z", - "mode_int": 0, - "passcount": 5478, - "playcount": 12511, - "ranked": 1, - "url": "https://osu.ppy.sh/beatmaps/4342177", - "checksum": "3b7c0bb338564060ab512b340a07f011", - "failtimes": { - "fail": [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 117, 54, 63, 72, - 18, 72, 0, 45, 90, 99, 18, 27, 54, 0, 36, - 36, 54, 9, 18, 18, 9, 54, 18, 9, 27, 9, 63, - 0, 0, 0, 9, 36, 9, 27, 0, 9, 72, 9, 18, 36, - 45, 18, 18, 36, 27, 9, 45, 9, 27, 0, 9, 9, - 0, 36, 0, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 9, 9, 0, - 0, 9, 0, 36, 0, 9, 0, 0, 0, 9, 9 - ], - "exit": [ - 0, 0, 0, 0, 0, 0, 0, 0, 9, 27, 90, 63, 63, - 81, 54, 81, 54, 171, 90, 126, 45, 27, 63, - 54, 108, 90, 63, 36, 36, 27, 54, 63, 0, 36, - 45, 9, 27, 9, 54, 9, 27, 45, 63, 36, 18, - 108, 63, 18, 36, 27, 18, 63, 54, 18, 18, 36, - 9, 72, 54, 18, 0, 27, 27, 9, 27, 18, 0, 27, - 45, 18, 27, 18, 0, 0, 9, 0, 0, 0, 0, 9, 0, - 0, 18, 9, 0, 9, 0, 0, 9, 36, 18, 36, 36, 27, - 18, 9, 0, 0, 0, 18 - ] - }, - "max_combo": 308 - }, - { - "beatmapset_id": 2070848, - "difficulty_rating": 5.02, - "id": 4345774, - "mode": "osu", - "status": "ranked", - "total_length": 115, - "user_id": 685229, - "version": "Extra", - "accuracy": 8.3, - "ar": 9.3, - "bpm": 150, - "convert": false, - "count_circles": 196, - "count_sliders": 211, - "count_spinners": 0, - "cs": 3, - "deleted_at": null, - "drain": 5.3, - "hit_length": 104, - "is_scoreable": true, - "last_updated": "2023-11-14T05:04:27Z", - "mode_int": 0, - "passcount": 19658, - "playcount": 68122, - "ranked": 1, - "url": "https://osu.ppy.sh/beatmaps/4345774", - "checksum": "1eeb1523c281f739f2ad4d06b283207a", - "failtimes": { - "fail": [ - 0, 0, 0, 0, 0, 0, 18, 36, 288, 243, 378, - 351, 369, 405, 684, 333, 90, 135, 135, 261, - 36, 729, 387, 108, 45, 54, 27, 9, 81, 72, - 180, 63, 270, 261, 306, 126, 189, 630, 63, - 630, 1485, 621, 468, 684, 3456, 594, 927, - 756, 450, 171, 162, 81, 45, 45, 0, 27, 9, - 18, 18, 27, 9, 9, 9, 36, 18, 18, 0, 0, 0, 0, - 0, 0, 0, 9, 27, 0, 18, 0, 0, 0, 36, 18, 36, - 594, 306, 378, 216, 117, 90, 54, 36, 18, 18, - 0, 387, 153, 81, 117, 0, 18 - ], - "exit": [ - 0, 0, 0, 45, 63, 36, 225, 873, 1035, 1341, - 819, 630, 693, 423, 864, 801, 441, 459, 558, - 297, 198, 252, 396, 207, 117, 342, 153, 63, - 108, 180, 171, 90, 90, 135, 405, 315, 171, - 72, 252, 117, 396, 369, 315, 252, 1098, - 1098, 315, 450, 234, 369, 441, 243, 162, 72, - 90, 261, 360, 261, 99, 108, 54, 99, 72, 63, - 27, 18, 9, 360, 252, 99, 54, 18, 18, 18, 0, - 9, 9, 63, 0, 18, 9, 9, 27, 234, 270, 180, - 108, 72, 72, 54, 54, 90, 27, 18, 135, 333, - 45, 108, 72, 477 - ] - }, - "max_combo": 639 - }, - { - "beatmapset_id": 2070848, - "difficulty_rating": 4.17, - "id": 4345775, - "mode": "osu", - "status": "ranked", - "total_length": 115, - "user_id": 685229, - "version": "Insane", - "accuracy": 7.8, - "ar": 8.5, - "bpm": 150, - "convert": false, - "count_circles": 185, - "count_sliders": 142, - "count_spinners": 1, - "cs": 4, - "deleted_at": null, - "drain": 5, - "hit_length": 105, - "is_scoreable": true, - "last_updated": "2023-11-14T05:04:28Z", - "mode_int": 0, - "passcount": 23022, - "playcount": 61705, - "ranked": 1, - "url": "https://osu.ppy.sh/beatmaps/4345775", - "checksum": "dd1591e11ec3ec99d516540dde7d50f7", - "failtimes": { - "fail": [ - 0, 0, 0, 0, 0, 0, 36, 54, 72, 144, 216, 342, - 315, 207, 270, 171, 81, 180, 108, 117, 54, - 270, 99, 81, 162, 144, 162, 81, 45, 36, 189, - 81, 45, 90, 171, 189, 45, 0, 18, 54, 135, - 144, 198, 144, 135, 72, 297, 54, 36, 54, 81, - 18, 27, 54, 63, 36, 36, 36, 54, 45, 18, 0, - 27, 9, 9, 0, 18, 0, 0, 0, 0, 0, 0, 0, 18, - 18, 18, 0, 0, 9, 180, 180, 198, 180, 36, - 351, 90, 99, 18, 36, 54, 108, 99, 18, 81, 9, - 36, 171, 36, 45 - ], - "exit": [ - 0, 0, 0, 27, 18, 27, 198, 837, 828, 1224, - 567, 909, 1134, 396, 324, 558, 504, 522, - 1566, 324, 153, 378, 1062, 198, 252, 270, - 306, 189, 108, 324, 486, 504, 108, 405, 162, - 189, 225, 144, 54, 36, 126, 342, 108, 486, - 306, 639, 288, 369, 153, 225, 180, 99, 135, - 108, 261, 135, 297, 216, 135, 126, 99, 261, - 45, 18, 36, 27, 45, 612, 612, 207, 81, 45, - 18, 54, 18, 18, 9, 0, 18, 9, 117, 90, 153, - 81, 117, 90, 126, 27, 81, 90, 99, 27, 99, - 126, 45, 90, 27, 99, 72, 387 - ] - }, - "max_combo": 496 - } - ], - "converts": [ - { - "beatmapset_id": 2070848, - "difficulty_rating": 4.71, - "id": 4333021, - "mode": "taiko", - "status": "ranked", - "total_length": 116, - "user_id": 685229, - "version": "3333", - "accuracy": 8.8, - "ar": 9.6, - "bpm": 150, - "convert": true, - "count_circles": 252, - "count_sliders": 208, - "count_spinners": 0, - "cs": 4, - "deleted_at": null, - "drain": 5.5, - "hit_length": 105, - "is_scoreable": true, - "last_updated": "2023-11-14T05:04:25Z", - "mode_int": 1, - "passcount": 20660, - "playcount": 66547, - "ranked": 1, - "url": "https://osu.ppy.sh/beatmaps/4333021", - "checksum": "e5b685bbcb926fd4a51bdc131271cf60", - "failtimes": { - "fail": [ - 0, 0, 0, 0, 0, 0, 9, 9, 63, 72, 18, 0, 9, 9, - 36, 90, 36, 45, 45, 27, 18, 27, 45, 9, 18, - 27, 18, 45, 27, 198, 198, 135, 90, 0, 459, - 378, 153, 378, 108, 657, 657, 144, 432, - 1422, 1035, 387, 225, 126, 45, 99, 45, 45, - 54, 63, 63, 9, 18, 45, 9, 0, 9, 9, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 171, 126, 0, - 0, 63, 0, 81, 270, 171, 153, 225, 63, 72, - 36, 9, 27, 180, 234, 423, 189, 216, 135, - 432, 72, 27 - ], - "exit": [ - 0, 0, 0, 27, 9, 18, 684, 819, 864, 792, 495, - 558, 531, 459, 405, 333, 441, 315, 693, 234, - 216, 225, 549, 288, 153, 225, 315, 144, 162, - 360, 801, 621, 144, 189, 432, 765, 342, - 1098, 774, 459, 558, 378, 252, 468, 1485, - 693, 243, 387, 153, 657, 684, 360, 360, 252, - 225, 405, 279, 441, 324, 63, 54, 171, 198, - 45, 18, 27, 126, 342, 234, 63, 18, 0, 0, 0, - 18, 27, 405, 99, 27, 0, 0, 99, 126, 162, 90, - 36, 63, 63, 135, 27, 36, 81, 180, 90, 216, - 108, 54, 306, 297, 450 - ] - } - }, - { - "beatmapset_id": 2070848, - "difficulty_rating": 4.94, - "id": 4333021, - "mode": "fruits", - "status": "ranked", - "total_length": 116, - "user_id": 685229, - "version": "3333", - "accuracy": 8.8, - "ar": 9.6, - "bpm": 150, - "convert": true, - "count_circles": 252, - "count_sliders": 208, - "count_spinners": 0, - "cs": 4, - "deleted_at": null, - "drain": 5.5, - "hit_length": 105, - "is_scoreable": true, - "last_updated": "2023-11-14T05:04:25Z", - "mode_int": 2, - "passcount": 20660, - "playcount": 66547, - "ranked": 1, - "url": "https://osu.ppy.sh/beatmaps/4333021", - "checksum": "e5b685bbcb926fd4a51bdc131271cf60", - "failtimes": { - "fail": [ - 0, 0, 0, 0, 0, 0, 9, 9, 63, 72, 18, 0, 9, 9, - 36, 90, 36, 45, 45, 27, 18, 27, 45, 9, 18, - 27, 18, 45, 27, 198, 198, 135, 90, 0, 459, - 378, 153, 378, 108, 657, 657, 144, 432, - 1422, 1035, 387, 225, 126, 45, 99, 45, 45, - 54, 63, 63, 9, 18, 45, 9, 0, 9, 9, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 171, 126, 0, - 0, 63, 0, 81, 270, 171, 153, 225, 63, 72, - 36, 9, 27, 180, 234, 423, 189, 216, 135, - 432, 72, 27 - ], - "exit": [ - 0, 0, 0, 27, 9, 18, 684, 819, 864, 792, 495, - 558, 531, 459, 405, 333, 441, 315, 693, 234, - 216, 225, 549, 288, 153, 225, 315, 144, 162, - 360, 801, 621, 144, 189, 432, 765, 342, - 1098, 774, 459, 558, 378, 252, 468, 1485, - 693, 243, 387, 153, 657, 684, 360, 360, 252, - 225, 405, 279, 441, 324, 63, 54, 171, 198, - 45, 18, 27, 126, 342, 234, 63, 18, 0, 0, 0, - 18, 27, 405, 99, 27, 0, 0, 99, 126, 162, 90, - 36, 63, 63, 135, 27, 36, 81, 180, 90, 216, - 108, 54, 306, 297, 450 - ] - } - }, - { - "beatmapset_id": 2070848, - "difficulty_rating": 3.05, - "id": 4333021, - "mode": "mania", - "status": "ranked", - "total_length": 116, - "user_id": 685229, - "version": "[7K] 3333", - "accuracy": 8.8, - "ar": 9.6, - "bpm": 150, - "convert": true, - "count_circles": 252, - "count_sliders": 208, - "count_spinners": 0, - "cs": 7, - "deleted_at": null, - "drain": 5.5, - "hit_length": 105, - "is_scoreable": true, - "last_updated": "2023-11-14T05:04:25Z", - "mode_int": 3, - "passcount": 20660, - "playcount": 66547, - "ranked": 1, - "url": "https://osu.ppy.sh/beatmaps/4333021", - "checksum": "e5b685bbcb926fd4a51bdc131271cf60", - "failtimes": { - "fail": [ - 0, 0, 0, 0, 0, 0, 9, 9, 63, 72, 18, 0, 9, 9, - 36, 90, 36, 45, 45, 27, 18, 27, 45, 9, 18, - 27, 18, 45, 27, 198, 198, 135, 90, 0, 459, - 378, 153, 378, 108, 657, 657, 144, 432, - 1422, 1035, 387, 225, 126, 45, 99, 45, 45, - 54, 63, 63, 9, 18, 45, 9, 0, 9, 9, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 171, 126, 0, - 0, 63, 0, 81, 270, 171, 153, 225, 63, 72, - 36, 9, 27, 180, 234, 423, 189, 216, 135, - 432, 72, 27 - ], - "exit": [ - 0, 0, 0, 27, 9, 18, 684, 819, 864, 792, 495, - 558, 531, 459, 405, 333, 441, 315, 693, 234, - 216, 225, 549, 288, 153, 225, 315, 144, 162, - 360, 801, 621, 144, 189, 432, 765, 342, - 1098, 774, 459, 558, 378, 252, 468, 1485, - 693, 243, 387, 153, 657, 684, 360, 360, 252, - 225, 405, 279, 441, 324, 63, 54, 171, 198, - 45, 18, 27, 126, 342, 234, 63, 18, 0, 0, 0, - 18, 27, 405, 99, 27, 0, 0, 99, 126, 162, 90, - 36, 63, 63, 135, 27, 36, 81, 180, 90, 216, - 108, 54, 306, 297, 450 - ] - } - }, - { - "beatmapset_id": 2070848, - "difficulty_rating": 3.31, - "id": 4342176, - "mode": "taiko", - "status": "ranked", - "total_length": 115, - "user_id": 685229, - "version": "Hard", - "accuracy": 6.8, - "ar": 7.7, - "bpm": 150, - "convert": true, - "count_circles": 82, - "count_sliders": 158, - "count_spinners": 1, - "cs": 3.8, - "deleted_at": null, - "drain": 4, - "hit_length": 105, - "is_scoreable": true, - "last_updated": "2023-11-14T05:04:26Z", - "mode_int": 1, - "passcount": 14892, - "playcount": 33472, - "ranked": 1, - "url": "https://osu.ppy.sh/beatmaps/4342176", - "checksum": "89e5febacb4a8668f0e0e97c1bacb4c7", - "failtimes": { - "fail": [ - 0, 0, 0, 0, 0, 0, 0, 18, 63, 63, 72, 72, 36, - 54, 108, 81, 72, 45, 36, 9, 36, 54, 27, 45, - 117, 117, 306, 45, 45, 0, 54, 54, 27, 72, - 27, 18, 9, 0, 0, 36, 189, 180, 477, 450, 18, - 72, 90, 162, 45, 18, 27, 63, 36, 54, 9, 9, - 9, 9, 9, 27, 9, 0, 0, 9, 0, 0, 9, 0, 0, 0, - 0, 0, 9, 9, 9, 0, 9, 0, 0, 0, 9, 18, 45, 9, - 0, 18, 9, 0, 0, 9, 0, 0, 27, 18, 9, 9, 27, - 27, 9, 36 - ], - "exit": [ - 0, 0, 0, 9, 18, 9, 63, 81, 171, 612, 441, - 558, 387, 207, 261, 270, 198, 270, 198, 108, - 81, 153, 117, 72, 117, 252, 297, 315, 198, - 90, 90, 279, 90, 81, 27, 81, 144, 135, 99, - 9, 99, 477, 486, 315, 108, 90, 108, 153, 54, - 117, 99, 144, 99, 90, 45, 54, 63, 27, 63, - 54, 18, 72, 0, 36, 36, 18, 9, 189, 243, 117, - 72, 36, 9, 9, 9, 9, 9, 0, 0, 36, 36, 54, 18, - 45, 9, 27, 36, 27, 9, 36, 18, 18, 36, 54, - 36, 18, 27, 9, 36, 153 - ] - } - }, - { - "beatmapset_id": 2070848, - "difficulty_rating": 1.86, - "id": 4342176, - "mode": "fruits", - "status": "ranked", - "total_length": 115, - "user_id": 685229, - "version": "Hard", - "accuracy": 6.8, - "ar": 7.7, - "bpm": 150, - "convert": true, - "count_circles": 82, - "count_sliders": 158, - "count_spinners": 1, - "cs": 3.8, - "deleted_at": null, - "drain": 4, - "hit_length": 105, - "is_scoreable": true, - "last_updated": "2023-11-14T05:04:26Z", - "mode_int": 2, - "passcount": 14892, - "playcount": 33472, - "ranked": 1, - "url": "https://osu.ppy.sh/beatmaps/4342176", - "checksum": "89e5febacb4a8668f0e0e97c1bacb4c7", - "failtimes": { - "fail": [ - 0, 0, 0, 0, 0, 0, 0, 18, 63, 63, 72, 72, 36, - 54, 108, 81, 72, 45, 36, 9, 36, 54, 27, 45, - 117, 117, 306, 45, 45, 0, 54, 54, 27, 72, - 27, 18, 9, 0, 0, 36, 189, 180, 477, 450, 18, - 72, 90, 162, 45, 18, 27, 63, 36, 54, 9, 9, - 9, 9, 9, 27, 9, 0, 0, 9, 0, 0, 9, 0, 0, 0, - 0, 0, 9, 9, 9, 0, 9, 0, 0, 0, 9, 18, 45, 9, - 0, 18, 9, 0, 0, 9, 0, 0, 27, 18, 9, 9, 27, - 27, 9, 36 - ], - "exit": [ - 0, 0, 0, 9, 18, 9, 63, 81, 171, 612, 441, - 558, 387, 207, 261, 270, 198, 270, 198, 108, - 81, 153, 117, 72, 117, 252, 297, 315, 198, - 90, 90, 279, 90, 81, 27, 81, 144, 135, 99, - 9, 99, 477, 486, 315, 108, 90, 108, 153, 54, - 117, 99, 144, 99, 90, 45, 54, 63, 27, 63, - 54, 18, 72, 0, 36, 36, 18, 9, 189, 243, 117, - 72, 36, 9, 9, 9, 9, 9, 0, 0, 36, 36, 54, 18, - 45, 9, 27, 36, 27, 9, 36, 18, 18, 36, 54, - 36, 18, 27, 9, 36, 153 - ] - } - }, - { - "beatmapset_id": 2070848, - "difficulty_rating": 1.78, - "id": 4342176, - "mode": "mania", - "status": "ranked", - "total_length": 115, - "user_id": 685229, - "version": "[5K] Hard", - "accuracy": 6.8, - "ar": 7.7, - "bpm": 150, - "convert": true, - "count_circles": 82, - "count_sliders": 158, - "count_spinners": 1, - "cs": 5, - "deleted_at": null, - "drain": 4, - "hit_length": 105, - "is_scoreable": true, - "last_updated": "2023-11-14T05:04:26Z", - "mode_int": 3, - "passcount": 14892, - "playcount": 33472, - "ranked": 1, - "url": "https://osu.ppy.sh/beatmaps/4342176", - "checksum": "89e5febacb4a8668f0e0e97c1bacb4c7", - "failtimes": { - "fail": [ - 0, 0, 0, 0, 0, 0, 0, 18, 63, 63, 72, 72, 36, - 54, 108, 81, 72, 45, 36, 9, 36, 54, 27, 45, - 117, 117, 306, 45, 45, 0, 54, 54, 27, 72, - 27, 18, 9, 0, 0, 36, 189, 180, 477, 450, 18, - 72, 90, 162, 45, 18, 27, 63, 36, 54, 9, 9, - 9, 9, 9, 27, 9, 0, 0, 9, 0, 0, 9, 0, 0, 0, - 0, 0, 9, 9, 9, 0, 9, 0, 0, 0, 9, 18, 45, 9, - 0, 18, 9, 0, 0, 9, 0, 0, 27, 18, 9, 9, 27, - 27, 9, 36 - ], - "exit": [ - 0, 0, 0, 9, 18, 9, 63, 81, 171, 612, 441, - 558, 387, 207, 261, 270, 198, 270, 198, 108, - 81, 153, 117, 72, 117, 252, 297, 315, 198, - 90, 90, 279, 90, 81, 27, 81, 144, 135, 99, - 9, 99, 477, 486, 315, 108, 90, 108, 153, 54, - 117, 99, 144, 99, 90, 45, 54, 63, 27, 63, - 54, 18, 72, 0, 36, 36, 18, 9, 189, 243, 117, - 72, 36, 9, 9, 9, 9, 9, 0, 0, 36, 36, 54, 18, - 45, 9, 27, 36, 27, 9, 36, 18, 18, 36, 54, - 36, 18, 27, 9, 36, 153 - ] - } - }, - { - "beatmapset_id": 2070848, - "difficulty_rating": 2.39, - "id": 4342177, - "mode": "taiko", - "status": "ranked", - "total_length": 115, - "user_id": 685229, - "version": "Normal", - "accuracy": 4, - "ar": 5.5, - "bpm": 150, - "convert": true, - "count_circles": 39, - "count_sliders": 112, - "count_spinners": 1, - "cs": 3.5, - "deleted_at": null, - "drain": 3, - "hit_length": 104, - "is_scoreable": true, - "last_updated": "2023-11-14T05:04:26Z", - "mode_int": 1, - "passcount": 5478, - "playcount": 12511, - "ranked": 1, - "url": "https://osu.ppy.sh/beatmaps/4342177", - "checksum": "3b7c0bb338564060ab512b340a07f011", - "failtimes": { - "fail": [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 117, 54, 63, 72, - 18, 72, 0, 45, 90, 99, 18, 27, 54, 0, 36, - 36, 54, 9, 18, 18, 9, 54, 18, 9, 27, 9, 63, - 0, 0, 0, 9, 36, 9, 27, 0, 9, 72, 9, 18, 36, - 45, 18, 18, 36, 27, 9, 45, 9, 27, 0, 9, 9, - 0, 36, 0, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 9, 9, 0, - 0, 9, 0, 36, 0, 9, 0, 0, 0, 9, 9 - ], - "exit": [ - 0, 0, 0, 0, 0, 0, 0, 0, 9, 27, 90, 63, 63, - 81, 54, 81, 54, 171, 90, 126, 45, 27, 63, - 54, 108, 90, 63, 36, 36, 27, 54, 63, 0, 36, - 45, 9, 27, 9, 54, 9, 27, 45, 63, 36, 18, - 108, 63, 18, 36, 27, 18, 63, 54, 18, 18, 36, - 9, 72, 54, 18, 0, 27, 27, 9, 27, 18, 0, 27, - 45, 18, 27, 18, 0, 0, 9, 0, 0, 0, 0, 9, 0, - 0, 18, 9, 0, 9, 0, 0, 9, 36, 18, 36, 36, 27, - 18, 9, 0, 0, 0, 18 - ] - } - }, - { - "beatmapset_id": 2070848, - "difficulty_rating": 1.28, - "id": 4342177, - "mode": "fruits", - "status": "ranked", - "total_length": 115, - "user_id": 685229, - "version": "Normal", - "accuracy": 4, - "ar": 5.5, - "bpm": 150, - "convert": true, - "count_circles": 39, - "count_sliders": 112, - "count_spinners": 1, - "cs": 3.5, - "deleted_at": null, - "drain": 3, - "hit_length": 104, - "is_scoreable": true, - "last_updated": "2023-11-14T05:04:26Z", - "mode_int": 2, - "passcount": 5478, - "playcount": 12511, - "ranked": 1, - "url": "https://osu.ppy.sh/beatmaps/4342177", - "checksum": "3b7c0bb338564060ab512b340a07f011", - "failtimes": { - "fail": [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 117, 54, 63, 72, - 18, 72, 0, 45, 90, 99, 18, 27, 54, 0, 36, - 36, 54, 9, 18, 18, 9, 54, 18, 9, 27, 9, 63, - 0, 0, 0, 9, 36, 9, 27, 0, 9, 72, 9, 18, 36, - 45, 18, 18, 36, 27, 9, 45, 9, 27, 0, 9, 9, - 0, 36, 0, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 9, 9, 0, - 0, 9, 0, 36, 0, 9, 0, 0, 0, 9, 9 - ], - "exit": [ - 0, 0, 0, 0, 0, 0, 0, 0, 9, 27, 90, 63, 63, - 81, 54, 81, 54, 171, 90, 126, 45, 27, 63, - 54, 108, 90, 63, 36, 36, 27, 54, 63, 0, 36, - 45, 9, 27, 9, 54, 9, 27, 45, 63, 36, 18, - 108, 63, 18, 36, 27, 18, 63, 54, 18, 18, 36, - 9, 72, 54, 18, 0, 27, 27, 9, 27, 18, 0, 27, - 45, 18, 27, 18, 0, 0, 9, 0, 0, 0, 0, 9, 0, - 0, 18, 9, 0, 9, 0, 0, 9, 36, 18, 36, 36, 27, - 18, 9, 0, 0, 0, 18 - ] - } - }, - { - "beatmapset_id": 2070848, - "difficulty_rating": 1.13, - "id": 4342177, - "mode": "mania", - "status": "ranked", - "total_length": 115, - "user_id": 685229, - "version": "[4K] Normal", - "accuracy": 4, - "ar": 5.5, - "bpm": 150, - "convert": true, - "count_circles": 39, - "count_sliders": 112, - "count_spinners": 1, - "cs": 4, - "deleted_at": null, - "drain": 3, - "hit_length": 104, - "is_scoreable": true, - "last_updated": "2023-11-14T05:04:26Z", - "mode_int": 3, - "passcount": 5478, - "playcount": 12511, - "ranked": 1, - "url": "https://osu.ppy.sh/beatmaps/4342177", - "checksum": "3b7c0bb338564060ab512b340a07f011", - "failtimes": { - "fail": [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 117, 54, 63, 72, - 18, 72, 0, 45, 90, 99, 18, 27, 54, 0, 36, - 36, 54, 9, 18, 18, 9, 54, 18, 9, 27, 9, 63, - 0, 0, 0, 9, 36, 9, 27, 0, 9, 72, 9, 18, 36, - 45, 18, 18, 36, 27, 9, 45, 9, 27, 0, 9, 9, - 0, 36, 0, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 9, 9, 0, - 0, 9, 0, 36, 0, 9, 0, 0, 0, 9, 9 - ], - "exit": [ - 0, 0, 0, 0, 0, 0, 0, 0, 9, 27, 90, 63, 63, - 81, 54, 81, 54, 171, 90, 126, 45, 27, 63, - 54, 108, 90, 63, 36, 36, 27, 54, 63, 0, 36, - 45, 9, 27, 9, 54, 9, 27, 45, 63, 36, 18, - 108, 63, 18, 36, 27, 18, 63, 54, 18, 18, 36, - 9, 72, 54, 18, 0, 27, 27, 9, 27, 18, 0, 27, - 45, 18, 27, 18, 0, 0, 9, 0, 0, 0, 0, 9, 0, - 0, 18, 9, 0, 9, 0, 0, 9, 36, 18, 36, 36, 27, - 18, 9, 0, 0, 0, 18 - ] - } - }, - { - "beatmapset_id": 2070848, - "difficulty_rating": 4.3, - "id": 4345774, - "mode": "taiko", - "status": "ranked", - "total_length": 115, - "user_id": 685229, - "version": "Extra", - "accuracy": 8.3, - "ar": 9.3, - "bpm": 150, - "convert": true, - "count_circles": 196, - "count_sliders": 211, - "count_spinners": 0, - "cs": 3, - "deleted_at": null, - "drain": 5.3, - "hit_length": 104, - "is_scoreable": true, - "last_updated": "2023-11-14T05:04:27Z", - "mode_int": 1, - "passcount": 19658, - "playcount": 68122, - "ranked": 1, - "url": "https://osu.ppy.sh/beatmaps/4345774", - "checksum": "1eeb1523c281f739f2ad4d06b283207a", - "failtimes": { - "fail": [ - 0, 0, 0, 0, 0, 0, 18, 36, 288, 243, 378, - 351, 369, 405, 684, 333, 90, 135, 135, 261, - 36, 729, 387, 108, 45, 54, 27, 9, 81, 72, - 180, 63, 270, 261, 306, 126, 189, 630, 63, - 630, 1485, 621, 468, 684, 3456, 594, 927, - 756, 450, 171, 162, 81, 45, 45, 0, 27, 9, - 18, 18, 27, 9, 9, 9, 36, 18, 18, 0, 0, 0, 0, - 0, 0, 0, 9, 27, 0, 18, 0, 0, 0, 36, 18, 36, - 594, 306, 378, 216, 117, 90, 54, 36, 18, 18, - 0, 387, 153, 81, 117, 0, 18 - ], - "exit": [ - 0, 0, 0, 45, 63, 36, 225, 873, 1035, 1341, - 819, 630, 693, 423, 864, 801, 441, 459, 558, - 297, 198, 252, 396, 207, 117, 342, 153, 63, - 108, 180, 171, 90, 90, 135, 405, 315, 171, - 72, 252, 117, 396, 369, 315, 252, 1098, - 1098, 315, 450, 234, 369, 441, 243, 162, 72, - 90, 261, 360, 261, 99, 108, 54, 99, 72, 63, - 27, 18, 9, 360, 252, 99, 54, 18, 18, 18, 0, - 9, 9, 63, 0, 18, 9, 9, 27, 234, 270, 180, - 108, 72, 72, 54, 54, 90, 27, 18, 135, 333, - 45, 108, 72, 477 - ] - } - }, - { - "beatmapset_id": 2070848, - "difficulty_rating": 4.18, - "id": 4345774, - "mode": "fruits", - "status": "ranked", - "total_length": 115, - "user_id": 685229, - "version": "Extra", - "accuracy": 8.3, - "ar": 9.3, - "bpm": 150, - "convert": true, - "count_circles": 196, - "count_sliders": 211, - "count_spinners": 0, - "cs": 3, - "deleted_at": null, - "drain": 5.3, - "hit_length": 104, - "is_scoreable": true, - "last_updated": "2023-11-14T05:04:27Z", - "mode_int": 2, - "passcount": 19658, - "playcount": 68122, - "ranked": 1, - "url": "https://osu.ppy.sh/beatmaps/4345774", - "checksum": "1eeb1523c281f739f2ad4d06b283207a", - "failtimes": { - "fail": [ - 0, 0, 0, 0, 0, 0, 18, 36, 288, 243, 378, - 351, 369, 405, 684, 333, 90, 135, 135, 261, - 36, 729, 387, 108, 45, 54, 27, 9, 81, 72, - 180, 63, 270, 261, 306, 126, 189, 630, 63, - 630, 1485, 621, 468, 684, 3456, 594, 927, - 756, 450, 171, 162, 81, 45, 45, 0, 27, 9, - 18, 18, 27, 9, 9, 9, 36, 18, 18, 0, 0, 0, 0, - 0, 0, 0, 9, 27, 0, 18, 0, 0, 0, 36, 18, 36, - 594, 306, 378, 216, 117, 90, 54, 36, 18, 18, - 0, 387, 153, 81, 117, 0, 18 - ], - "exit": [ - 0, 0, 0, 45, 63, 36, 225, 873, 1035, 1341, - 819, 630, 693, 423, 864, 801, 441, 459, 558, - 297, 198, 252, 396, 207, 117, 342, 153, 63, - 108, 180, 171, 90, 90, 135, 405, 315, 171, - 72, 252, 117, 396, 369, 315, 252, 1098, - 1098, 315, 450, 234, 369, 441, 243, 162, 72, - 90, 261, 360, 261, 99, 108, 54, 99, 72, 63, - 27, 18, 9, 360, 252, 99, 54, 18, 18, 18, 0, - 9, 9, 63, 0, 18, 9, 9, 27, 234, 270, 180, - 108, 72, 72, 54, 54, 90, 27, 18, 135, 333, - 45, 108, 72, 477 - ] - } - }, - { - "beatmapset_id": 2070848, - "difficulty_rating": 2.57, - "id": 4345774, - "mode": "mania", - "status": "ranked", - "total_length": 115, - "user_id": 685229, - "version": "[7K] Extra", - "accuracy": 8.3, - "ar": 9.3, - "bpm": 150, - "convert": true, - "count_circles": 196, - "count_sliders": 211, - "count_spinners": 0, - "cs": 7, - "deleted_at": null, - "drain": 5.3, - "hit_length": 104, - "is_scoreable": true, - "last_updated": "2023-11-14T05:04:27Z", - "mode_int": 3, - "passcount": 19658, - "playcount": 68122, - "ranked": 1, - "url": "https://osu.ppy.sh/beatmaps/4345774", - "checksum": "1eeb1523c281f739f2ad4d06b283207a", - "failtimes": { - "fail": [ - 0, 0, 0, 0, 0, 0, 18, 36, 288, 243, 378, - 351, 369, 405, 684, 333, 90, 135, 135, 261, - 36, 729, 387, 108, 45, 54, 27, 9, 81, 72, - 180, 63, 270, 261, 306, 126, 189, 630, 63, - 630, 1485, 621, 468, 684, 3456, 594, 927, - 756, 450, 171, 162, 81, 45, 45, 0, 27, 9, - 18, 18, 27, 9, 9, 9, 36, 18, 18, 0, 0, 0, 0, - 0, 0, 0, 9, 27, 0, 18, 0, 0, 0, 36, 18, 36, - 594, 306, 378, 216, 117, 90, 54, 36, 18, 18, - 0, 387, 153, 81, 117, 0, 18 - ], - "exit": [ - 0, 0, 0, 45, 63, 36, 225, 873, 1035, 1341, - 819, 630, 693, 423, 864, 801, 441, 459, 558, - 297, 198, 252, 396, 207, 117, 342, 153, 63, - 108, 180, 171, 90, 90, 135, 405, 315, 171, - 72, 252, 117, 396, 369, 315, 252, 1098, - 1098, 315, 450, 234, 369, 441, 243, 162, 72, - 90, 261, 360, 261, 99, 108, 54, 99, 72, 63, - 27, 18, 9, 360, 252, 99, 54, 18, 18, 18, 0, - 9, 9, 63, 0, 18, 9, 9, 27, 234, 270, 180, - 108, 72, 72, 54, 54, 90, 27, 18, 135, 333, - 45, 108, 72, 477 - ] - } - }, - { - "beatmapset_id": 2070848, - "difficulty_rating": 3.79, - "id": 4345775, - "mode": "taiko", - "status": "ranked", - "total_length": 115, - "user_id": 685229, - "version": "Insane", - "accuracy": 7.8, - "ar": 8.5, - "bpm": 150, - "convert": true, - "count_circles": 185, - "count_sliders": 142, - "count_spinners": 1, - "cs": 4, - "deleted_at": null, - "drain": 5, - "hit_length": 105, - "is_scoreable": true, - "last_updated": "2023-11-14T05:04:28Z", - "mode_int": 1, - "passcount": 23022, - "playcount": 61705, - "ranked": 1, - "url": "https://osu.ppy.sh/beatmaps/4345775", - "checksum": "dd1591e11ec3ec99d516540dde7d50f7", - "failtimes": { - "fail": [ - 0, 0, 0, 0, 0, 0, 36, 54, 72, 144, 216, 342, - 315, 207, 270, 171, 81, 180, 108, 117, 54, - 270, 99, 81, 162, 144, 162, 81, 45, 36, 189, - 81, 45, 90, 171, 189, 45, 0, 18, 54, 135, - 144, 198, 144, 135, 72, 297, 54, 36, 54, 81, - 18, 27, 54, 63, 36, 36, 36, 54, 45, 18, 0, - 27, 9, 9, 0, 18, 0, 0, 0, 0, 0, 0, 0, 18, - 18, 18, 0, 0, 9, 180, 180, 198, 180, 36, - 351, 90, 99, 18, 36, 54, 108, 99, 18, 81, 9, - 36, 171, 36, 45 - ], - "exit": [ - 0, 0, 0, 27, 18, 27, 198, 837, 828, 1224, - 567, 909, 1134, 396, 324, 558, 504, 522, - 1566, 324, 153, 378, 1062, 198, 252, 270, - 306, 189, 108, 324, 486, 504, 108, 405, 162, - 189, 225, 144, 54, 36, 126, 342, 108, 486, - 306, 639, 288, 369, 153, 225, 180, 99, 135, - 108, 261, 135, 297, 216, 135, 126, 99, 261, - 45, 18, 36, 27, 45, 612, 612, 207, 81, 45, - 18, 54, 18, 18, 9, 0, 18, 9, 117, 90, 153, - 81, 117, 90, 126, 27, 81, 90, 99, 27, 99, - 126, 45, 90, 27, 99, 72, 387 - ] - } - }, - { - "beatmapset_id": 2070848, - "difficulty_rating": 3.02, - "id": 4345775, - "mode": "fruits", - "status": "ranked", - "total_length": 115, - "user_id": 685229, - "version": "Insane", - "accuracy": 7.8, - "ar": 8.5, - "bpm": 150, - "convert": true, - "count_circles": 185, - "count_sliders": 142, - "count_spinners": 1, - "cs": 4, - "deleted_at": null, - "drain": 5, - "hit_length": 105, - "is_scoreable": true, - "last_updated": "2023-11-14T05:04:28Z", - "mode_int": 2, - "passcount": 23022, - "playcount": 61705, - "ranked": 1, - "url": "https://osu.ppy.sh/beatmaps/4345775", - "checksum": "dd1591e11ec3ec99d516540dde7d50f7", - "failtimes": { - "fail": [ - 0, 0, 0, 0, 0, 0, 36, 54, 72, 144, 216, 342, - 315, 207, 270, 171, 81, 180, 108, 117, 54, - 270, 99, 81, 162, 144, 162, 81, 45, 36, 189, - 81, 45, 90, 171, 189, 45, 0, 18, 54, 135, - 144, 198, 144, 135, 72, 297, 54, 36, 54, 81, - 18, 27, 54, 63, 36, 36, 36, 54, 45, 18, 0, - 27, 9, 9, 0, 18, 0, 0, 0, 0, 0, 0, 0, 18, - 18, 18, 0, 0, 9, 180, 180, 198, 180, 36, - 351, 90, 99, 18, 36, 54, 108, 99, 18, 81, 9, - 36, 171, 36, 45 - ], - "exit": [ - 0, 0, 0, 27, 18, 27, 198, 837, 828, 1224, - 567, 909, 1134, 396, 324, 558, 504, 522, - 1566, 324, 153, 378, 1062, 198, 252, 270, - 306, 189, 108, 324, 486, 504, 108, 405, 162, - 189, 225, 144, 54, 36, 126, 342, 108, 486, - 306, 639, 288, 369, 153, 225, 180, 99, 135, - 108, 261, 135, 297, 216, 135, 126, 99, 261, - 45, 18, 36, 27, 45, 612, 612, 207, 81, 45, - 18, 54, 18, 18, 9, 0, 18, 9, 117, 90, 153, - 81, 117, 90, 126, 27, 81, 90, 99, 27, 99, - 126, 45, 90, 27, 99, 72, 387 - ] - } - }, - { - "beatmapset_id": 2070848, - "difficulty_rating": 2.38, - "id": 4345775, - "mode": "mania", - "status": "ranked", - "total_length": 115, - "user_id": 685229, - "version": "[7K] Insane", - "accuracy": 7.8, - "ar": 8.5, - "bpm": 150, - "convert": true, - "count_circles": 185, - "count_sliders": 142, - "count_spinners": 1, - "cs": 7, - "deleted_at": null, - "drain": 5, - "hit_length": 105, - "is_scoreable": true, - "last_updated": "2023-11-14T05:04:28Z", - "mode_int": 3, - "passcount": 23022, - "playcount": 61705, - "ranked": 1, - "url": "https://osu.ppy.sh/beatmaps/4345775", - "checksum": "dd1591e11ec3ec99d516540dde7d50f7", - "failtimes": { - "fail": [ - 0, 0, 0, 0, 0, 0, 36, 54, 72, 144, 216, 342, - 315, 207, 270, 171, 81, 180, 108, 117, 54, - 270, 99, 81, 162, 144, 162, 81, 45, 36, 189, - 81, 45, 90, 171, 189, 45, 0, 18, 54, 135, - 144, 198, 144, 135, 72, 297, 54, 36, 54, 81, - 18, 27, 54, 63, 36, 36, 36, 54, 45, 18, 0, - 27, 9, 9, 0, 18, 0, 0, 0, 0, 0, 0, 0, 18, - 18, 18, 0, 0, 9, 180, 180, 198, 180, 36, - 351, 90, 99, 18, 36, 54, 108, 99, 18, 81, 9, - 36, 171, 36, 45 - ], - "exit": [ - 0, 0, 0, 27, 18, 27, 198, 837, 828, 1224, - 567, 909, 1134, 396, 324, 558, 504, 522, - 1566, 324, 153, 378, 1062, 198, 252, 270, - 306, 189, 108, 324, 486, 504, 108, 405, 162, - 189, 225, 144, 54, 36, 126, 342, 108, 486, - 306, 639, 288, 369, 153, 225, 180, 99, 135, - 108, 261, 135, 297, 216, 135, 126, 99, 261, - 45, 18, 36, 27, 45, 612, 612, 207, 81, 45, - 18, 54, 18, 18, 9, 0, 18, 9, 117, 90, 153, - 81, 117, 90, 126, 27, 81, 90, 99, 27, 99, - 126, 45, 90, 27, 99, 72, 387 - ] - } - } - ], - "current_nominations": [ - { - "beatmapset_id": 2070848, - "rulesets": ["osu"], - "reset": false, - "user_id": 1634445 - }, - { - "beatmapset_id": 2070848, - "rulesets": ["osu"], - "reset": false, - "user_id": 2688581 - } - ], - "description": { - "description": "
Use this space to tell the world about your map. It helps to include a list of changes as your map is modded!

BN : Dailycare / Luscent
" - }, - "genre": { - "id": 10, - "name": "Electronic" - }, - "language": { - "id": 3, - "name": "Japanese" - }, - "pack_tags": ["S1364"], - "ratings": [0, 3, 1, 1, 2, 1, 3, 2, 13, 37, 263], - "related_users": [ - { - "avatar_url": "https://a.ppy.sh/685229?1726669685.jpeg", - "country_code": "KR", - "default_group": "default", - "id": 685229, - "is_active": true, - "is_bot": false, - "is_deleted": false, - "is_online": false, - "is_supporter": false, - "last_visit": null, - "pm_friends_only": false, - "profile_colour": null, - "username": "Beige" - }, - { - "avatar_url": "https://a.ppy.sh/1634445?1710517165.png", - "country_code": "KR", - "default_group": "bng", - "id": 1634445, - "is_active": true, - "is_bot": false, - "is_deleted": false, - "is_online": false, - "is_supporter": true, - "last_visit": null, - "pm_friends_only": false, - "profile_colour": "#A347EB", - "username": "Dailycare" - }, - { - "avatar_url": "https://a.ppy.sh/2688581?1712358947.png", - "country_code": "KR", - "default_group": "nat", - "id": 2688581, - "is_active": true, - "is_bot": false, - "is_deleted": false, - "is_online": false, - "is_supporter": true, - "last_visit": null, - "pm_friends_only": false, - "profile_colour": "#fa3703", - "username": "Luscent" - } - ], - "user": { - "avatar_url": "https://a.ppy.sh/685229?1726669685.jpeg", - "country_code": "KR", - "default_group": "default", - "id": 685229, - "is_active": true, - "is_bot": false, - "is_deleted": false, - "is_online": false, - "is_supporter": false, - "last_visit": null, - "pm_friends_only": false, - "profile_colour": null, - "username": "Beige" - } - } + "avatar_url": "https://a.ppy.sh/2688581?1712358947.png", + "country_code": "KR", + "default_group": "nat", + "id": 2688581, + "is_active": true, + "is_bot": false, + "is_deleted": false, + "is_online": false, + "is_supporter": true, + "last_visit": null, + "pm_friends_only": false, + "profile_colour": "#fa3703", + "username": "Luscent" } - ] - } + ], + "user": { + "avatar_url": "https://a.ppy.sh/685229?1726669685.jpeg", + "country_code": "KR", + "default_group": "default", + "id": 685229, + "is_active": true, + "is_bot": false, + "is_deleted": false, + "is_online": false, + "is_supporter": false, + "last_visit": null, + "pm_friends_only": false, + "profile_colour": null, + "username": "Beige" + } + } + } + ] + } } diff --git a/server/tests/mirrors.manager.test.ts b/server/tests/mirrors.manager.test.ts index 7a8f05b..280431b 100644 --- a/server/tests/mirrors.manager.test.ts +++ b/server/tests/mirrors.manager.test.ts @@ -1,1851 +1,1854 @@ +import assert from "node:assert"; + +import { faker } from "@faker-js/faker"; import { - beforeAll, - beforeEach, - describe, - expect, - jest, - mock, - setSystemTime, - test, -} from 'bun:test'; - -import { MirrorsManager } from '../src/core/managers/mirrors/mirrors.manager'; -import { StorageManager } from '../src/core/managers/storage/storage.manager'; -import { BanchoClient } from '../src/core/domains/osu.ppy.sh/bancho.client'; -import { Mocker } from './utils/mocker'; -import { ClientAbilities } from '../src/core/abstracts/client/base-client.types'; -import { BaseClient } from '../src/core/abstracts/client/base-client.abstract'; -import { MinoClient } from '../src/core/domains/catboy.best/mino.client'; -import { OsulabsClient } from '../src/core/domains/beatmaps.download/osulabs.client'; -import { GatariClient } from '../src/core/domains/gatari.pw/gatari.client'; -import { NerinyanClient } from '../src/core/domains/nerinyan.moe/nerinyan.client'; -import assert from 'assert'; -import { faker } from '@faker-js/faker'; -import config from '../src/config'; -import { DirectClient } from '../src/core/domains'; - -const mirrors: (new (...args: any[]) => BaseClient)[] = [ - MinoClient, - BanchoClient, - GatariClient, - NerinyanClient, - OsulabsClient, - DirectClient, + beforeAll, + beforeEach, + describe, + expect, + jest, + mock, + setSystemTime, + test, +} from "bun:test"; + +import config from "../src/config"; +import type { BaseClient } from "../src/core/abstracts/client/base-client.abstract"; +import { ClientAbilities } from "../src/core/abstracts/client/base-client.types"; +import { DirectClient } from "../src/core/domains"; +import { OsulabsClient } from "../src/core/domains/beatmaps.download/osulabs.client"; +import { MinoClient } from "../src/core/domains/catboy.best/mino.client"; +import { GatariClient } from "../src/core/domains/gatari.pw/gatari.client"; +import { NerinyanClient } from "../src/core/domains/nerinyan.moe/nerinyan.client"; +import { BanchoClient } from "../src/core/domains/osu.ppy.sh/bancho.client"; +import { MirrorsManager } from "../src/core/managers/mirrors/mirrors.manager"; +import type { StorageManager } from "../src/core/managers/storage/storage.manager"; +import { Mocker } from "./utils/mocker"; + +const mirrors: Array BaseClient> = [ + MinoClient, + BanchoClient, + GatariClient, + NerinyanClient, + OsulabsClient, + DirectClient, ]; -const getMirrorsWithAbility = (ability: ClientAbilities) => { - return mirrors.filter((mirror) => - new mirror().clientConfig.abilities.includes(ability), - ); -}; +function getMirrorsWithAbility(ability: ClientAbilities) { + return mirrors.filter(Mirror => + new Mirror().clientConfig.abilities.includes(ability), + ); +} -describe('MirrorsManager', () => { - let mirrorsManager: MirrorsManager; - let mockStorageManager: StorageManager; +describe("MirrorsManager", () => { + let mirrorsManager: MirrorsManager; + let mockStorageManager: StorageManager; - beforeAll(async () => { - await Mocker.ensureDatabaseInitialized(); + beforeAll(async () => { + await Mocker.ensureDatabaseInitialized(); - mockStorageManager = { - getBeatmapSet: mock(async () => undefined), - insertBeatmapset: mock(async () => {}), - } as unknown as StorageManager; - }); + mockStorageManager = { + getBeatmapSet: mock(async () => {}), + insertBeatmapset: mock(async () => {}), + } as unknown as StorageManager; + }); - beforeEach(async () => { - jest.restoreAllMocks(); - mirrorsManager = null!; - Mocker.mockMirrorsBenchmark(); - }); + beforeEach(async () => { + jest.restoreAllMocks(); + mirrorsManager = null!; + Mocker.mockMirrorsBenchmark(); + }); - const getMirrorClient = ( - mirror: new (...args: any[]) => BaseClient, - initMirrors: (new (...args: any[]) => BaseClient)[] = [], - ) => { - const mirrorsToInitialize = [...initMirrors, mirror]; + const getMirrorClient = ( + mirror: new (...args: any[]) => BaseClient, + initMirrors: Array BaseClient> = [], + ) => { + const mirrorsToInitialize = [...initMirrors, mirror]; - config.MirrorsToIgnore = mirrors - .filter((m) => !mirrorsToInitialize.includes(m)) - .map((m) => m.name.slice(0, -6).toLowerCase()); + config.MirrorsToIgnore = mirrors + .filter(m => !mirrorsToInitialize.includes(m)) + .map(m => m.name.slice(0, -6).toLowerCase()); - let isMirrorsManagerInitialized = mirrorsManager !== null; + const isMirrorsManagerInitialized = mirrorsManager !== null; - if (!isMirrorsManagerInitialized) { - mirrorsManager = new MirrorsManager(mockStorageManager); + if (!isMirrorsManagerInitialized) { + mirrorsManager = new MirrorsManager(mockStorageManager); - assert( - // @ts-expect-error accessing protected property for testing - mirrorsManager.clients.length === mirrorsToInitialize.length, + assert( + // @ts-expect-error accessing protected property for testing + mirrorsManager.clients.length === mirrorsToInitialize.length, // @ts-expect-error accessing protected property for testing `Expected ${mirrorsToInitialize.length} clients, got ${mirrorsManager.clients.length}`, - ); - } - - // @ts-expect-error accessing protected property for testing - const client = mirrorsManager.clients.find( - (c: any) => c.client instanceof mirror, - )?.client; - - assert(client, `Expected client ${mirror.name} not found`); - - if (client instanceof BanchoClient) { - Mocker.mockRequest( - client, - 'banchoService', - 'getBanchoClientToken', - 'test', - ); - } - - return client; - }; - - describe('General methods', () => { - describe('GetBeatmapSetById', () => { - const mirrors = getMirrorsWithAbility( - ClientAbilities.GetBeatmapSetById, - ); - - test.each(mirrors)( + ); + } + + // @ts-expect-error accessing protected property for testing + const client = mirrorsManager.clients.find( + (c: any) => c.client instanceof mirror, + )?.client; + + assert(client, `Expected client ${mirror.name} not found`); + + if (client instanceof BanchoClient) { + Mocker.mockRequest( + client, + "banchoService", + "getBanchoClientToken", + "test", + ); + } + + return client; + }; + + describe("General methods", () => { + describe("GetBeatmapSetById", () => { + const mirrors = getMirrorsWithAbility( + ClientAbilities.GetBeatmapSetById, + ); + + test.each(mirrors)( `$name: Should successfully fetch a beatmapset by id`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const beatmapSetId = faker.number.int({ - min: 1, - max: 1000000, - }); + const beatmapSetId = faker.number.int({ + min: 1, + max: 1000000, + }); - const { mockBeatmapset, mockBeatmap } = - Mocker.getClientMockMethods(client); + const { mockBeatmapset } + = Mocker.getClientMockMethods(client); - mockBeatmapset({ - data: { - id: beatmapSetId, - }, - }); + mockBeatmapset({ + data: { + id: beatmapSetId, + }, + }); - const result = await mirrorsManager.getBeatmapSet({ - beatmapSetId, - }); + const result = await mirrorsManager.getBeatmapSet({ + beatmapSetId, + }); - expect(result.status).toBe(200); - expect(result.result).not.toBeNull(); - expect(result.result?.id).toBe(beatmapSetId); + expect(result.status).toBe(200); + expect(result.result).not.toBeNull(); + expect(result.result?.id).toBe(beatmapSetId); }, - ); + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully update ratelimit during get beatmapset by id request`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const { generateBeatmapset } = - Mocker.getClientGenerateMethods(client); + const { generateBeatmapset } + = Mocker.getClientGenerateMethods(client); - const beatmapSetId = faker.number.int({ - min: 1, - max: 1000000, - }); + const beatmapSetId = faker.number.int({ + min: 1, + max: 1000000, + }); - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: generateBeatmapset({ id: beatmapSetId }), - status: 200, - headers: {}, - }, - ); - - const request = mirrorsManager.getBeatmapSet({ - beatmapSetId, - }); + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: generateBeatmapset({ id: beatmapSetId }), + status: 200, + headers: {}, + }, + ); + + const request = mirrorsManager.getBeatmapSet({ + beatmapSetId, + }); - // Skip a tick to check if is on cooldown - await new Promise((r) => setTimeout(r, 0)); + // Skip a tick to check if is on cooldown + await new Promise(r => setTimeout(r, 0)); - let capacity = client.getCapacity( - ClientAbilities.GetBeatmapSetById, - ); + let capacity = client.getCapacity( + ClientAbilities.GetBeatmapSetById, + ); - expect(capacity.remaining).toBeLessThan(capacity.limit); + expect(capacity.remaining).toBeLessThan(capacity.limit); - const awaitedResult = await request; + const awaitedResult = await request; - expect(mockApiGet).toHaveBeenCalledTimes(1); + expect(mockApiGet).toHaveBeenCalledTimes(1); - capacity = client.getCapacity( - ClientAbilities.GetBeatmapSetById, - ); + capacity = client.getCapacity( + ClientAbilities.GetBeatmapSetById, + ); - expect(awaitedResult.status).toBe(200); - expect(awaitedResult.result).not.toBeNull(); - expect(awaitedResult.result?.id).toBe(beatmapSetId); + expect(awaitedResult.status).toBe(200); + expect(awaitedResult.result).not.toBeNull(); + expect(awaitedResult.result?.id).toBe(beatmapSetId); - expect(capacity.remaining).toBeLessThan(capacity.limit); + expect(capacity.remaining).toBeLessThan(capacity.limit); }, - ); + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully return 404 when beatmapset is not found`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const beatmapSetId = faker.number.int({ - min: 1, - max: 1000000, - }); + const beatmapSetId = faker.number.int({ + min: 1, + max: 1000000, + }); - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: null, - status: 404, - headers: {}, - }, - ); - - const request = mirrorsManager.getBeatmapSet({ - beatmapSetId, - }); + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: null, + status: 404, + headers: {}, + }, + ); - const awaitedResult = await request; + const request = mirrorsManager.getBeatmapSet({ + beatmapSetId, + }); - expect(mockApiGet).toHaveBeenCalledTimes(1); + const awaitedResult = await request; - expect(awaitedResult.status).toBe(404); - expect(awaitedResult.result).toBeNull(); + expect(mockApiGet).toHaveBeenCalledTimes(1); + + expect(awaitedResult.status).toBe(404); + expect(awaitedResult.result).toBeNull(); }, - ); + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully return 502 when API request fails and no other mirrors are available`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const beatmapSetId = faker.number.int({ - min: 1, - max: 1000000, - }); + const beatmapSetId = faker.number.int({ + min: 1, + max: 1000000, + }); - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: null, - status: 500, - headers: {}, - }, - ); - - const request = mirrorsManager.getBeatmapSet({ - beatmapSetId, - }); + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: null, + status: 500, + headers: {}, + }, + ); - const awaitedResult = await request; + const request = mirrorsManager.getBeatmapSet({ + beatmapSetId, + }); - expect(mockApiGet).toHaveBeenCalledTimes(1); + const awaitedResult = await request; - expect(awaitedResult.status).toBe(502); - expect(awaitedResult.result).toBeNull(); + expect(mockApiGet).toHaveBeenCalledTimes(1); + + expect(awaitedResult.status).toBe(502); + expect(awaitedResult.result).toBeNull(); }, - ); - }); + ); + }); - describe('GetBeatmapById', () => { - const mirrors = getMirrorsWithAbility( - ClientAbilities.GetBeatmapById, - ); + describe("GetBeatmapById", () => { + const mirrors = getMirrorsWithAbility( + ClientAbilities.GetBeatmapById, + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully fetch a beatmap by id`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const beatmapId = faker.number.int({ - min: 1, - max: 1000000, - }); + const beatmapId = faker.number.int({ + min: 1, + max: 1000000, + }); - const { mockBeatmap } = Mocker.getClientMockMethods(client); - mockBeatmap({ - data: { - id: beatmapId, - }, - }); + const { mockBeatmap } = Mocker.getClientMockMethods(client); + mockBeatmap({ + data: { + id: beatmapId, + }, + }); - const result = await mirrorsManager.getBeatmap({ - beatmapId, - }); + const result = await mirrorsManager.getBeatmap({ + beatmapId, + }); - expect(result.status).toBe(200); - expect(result.result).not.toBeNull(); - expect(result.result?.id).toBe(beatmapId); + expect(result.status).toBe(200); + expect(result.result).not.toBeNull(); + expect(result.result?.id).toBe(beatmapId); }, - ); + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully update ratelimit during get beatmap by id request`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const { generateBeatmap } = - Mocker.getClientGenerateMethods(client); + const { generateBeatmap } + = Mocker.getClientGenerateMethods(client); - const beatmapId = faker.number.int({ - min: 1, - max: 1000000, - }); + const beatmapId = faker.number.int({ + min: 1, + max: 1000000, + }); - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: generateBeatmap({ id: beatmapId }), - status: 200, - headers: {}, - }, - ); - - const request = mirrorsManager.getBeatmap({ - beatmapId, - }); + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: generateBeatmap({ id: beatmapId }), + status: 200, + headers: {}, + }, + ); - // Skip a tick to check if is on cooldown - await new Promise((r) => setTimeout(r, 0)); + const request = mirrorsManager.getBeatmap({ + beatmapId, + }); - let capacity = client.getCapacity( - ClientAbilities.GetBeatmapById, - ); + // Skip a tick to check if is on cooldown + await new Promise(r => setTimeout(r, 0)); - expect(capacity.remaining).toBeLessThan(capacity.limit); + let capacity = client.getCapacity( + ClientAbilities.GetBeatmapById, + ); - const awaitedResult = await request; + expect(capacity.remaining).toBeLessThan(capacity.limit); - expect(mockApiGet).toHaveBeenCalledTimes(1); + const awaitedResult = await request; - capacity = client.getCapacity( - ClientAbilities.GetBeatmapById, - ); + expect(mockApiGet).toHaveBeenCalledTimes(1); - expect(awaitedResult.status).toBe(200); - expect(awaitedResult.result).not.toBeNull(); - expect(awaitedResult.result?.id).toBe(beatmapId); + capacity = client.getCapacity( + ClientAbilities.GetBeatmapById, + ); - expect(capacity.remaining).toBeLessThan(capacity.limit); + expect(awaitedResult.status).toBe(200); + expect(awaitedResult.result).not.toBeNull(); + expect(awaitedResult.result?.id).toBe(beatmapId); + + expect(capacity.remaining).toBeLessThan(capacity.limit); }, - ); + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully return 404 when beatmap is not found`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const beatmapId = faker.number.int({ - min: 1, - max: 1000000, - }); + const beatmapId = faker.number.int({ + min: 1, + max: 1000000, + }); - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: null, - status: 404, - headers: {}, - }, - ); - - const request = mirrorsManager.getBeatmap({ - beatmapId, - }); + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: null, + status: 404, + headers: {}, + }, + ); - const awaitedResult = await request; + const request = mirrorsManager.getBeatmap({ + beatmapId, + }); + + const awaitedResult = await request; - expect(mockApiGet).toHaveBeenCalledTimes(1); + expect(mockApiGet).toHaveBeenCalledTimes(1); - expect(awaitedResult.status).toBe(404); - expect(awaitedResult.result).toBeNull(); + expect(awaitedResult.status).toBe(404); + expect(awaitedResult.result).toBeNull(); }, - ); + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully return 502 when API request fails and no other mirrors are available`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const beatmapId = faker.number.int({ - min: 1, - max: 1000000, - }); + const beatmapId = faker.number.int({ + min: 1, + max: 1000000, + }); - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: null, - status: 500, - headers: {}, - }, - ); - - const request = mirrorsManager.getBeatmap({ - beatmapId, - }); + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: null, + status: 500, + headers: {}, + }, + ); + + const request = mirrorsManager.getBeatmap({ + beatmapId, + }); - const awaitedResult = await request; + const awaitedResult = await request; - expect(mockApiGet).toHaveBeenCalledTimes(1); + expect(mockApiGet).toHaveBeenCalledTimes(1); - expect(awaitedResult.status).toBe(502); - expect(awaitedResult.result).toBeNull(); + expect(awaitedResult.status).toBe(502); + expect(awaitedResult.result).toBeNull(); }, - ); - }); + ); + }); - describe('GetBeatmapByHash', () => { - const mirrors = getMirrorsWithAbility( - ClientAbilities.GetBeatmapByHash, - ); + describe("GetBeatmapByHash", () => { + const mirrors = getMirrorsWithAbility( + ClientAbilities.GetBeatmapByHash, + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully fetch a beatmap by hash`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const beatmapHash = faker.string.uuid(); + const beatmapHash = faker.string.uuid(); - const { mockBeatmap } = Mocker.getClientMockMethods(client); + const { mockBeatmap } = Mocker.getClientMockMethods(client); - mockBeatmap({ - data: { - checksum: beatmapHash, - }, - }); + mockBeatmap({ + data: { + checksum: beatmapHash, + }, + }); - const result = await mirrorsManager.getBeatmap({ - beatmapHash, - }); + const result = await mirrorsManager.getBeatmap({ + beatmapHash, + }); - expect(result.status).toBe(200); - expect(result.result).not.toBeNull(); - expect(result.result?.checksum).toBe(beatmapHash); + expect(result.status).toBe(200); + expect(result.result).not.toBeNull(); + expect(result.result?.checksum).toBe(beatmapHash); }, - ); + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully update ratelimit during get beatmap by hash request`, async (mirror) => { - const client = getMirrorClient(mirror); - - const beatmapHash = faker.string.uuid(); - - const { generateBeatmap, generateBeatmapset } = - Mocker.getClientGenerateMethods(client); - - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: generateBeatmap({ - checksum: beatmapHash, - }), - status: 200, - headers: {}, - }, - ); - - const request = mirrorsManager.getBeatmap({ - beatmapHash, - }); + const client = getMirrorClient(mirror); + + const beatmapHash = faker.string.uuid(); - // Skip a tick to check if is on cooldown - await new Promise((r) => setTimeout(r, 0)); + const { generateBeatmap } + = Mocker.getClientGenerateMethods(client); - let capacity = client.getCapacity( - ClientAbilities.GetBeatmapByHash, - ); + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: generateBeatmap({ + checksum: beatmapHash, + }), + status: 200, + headers: {}, + }, + ); - expect(capacity.remaining).toBeLessThan(capacity.limit); + const request = mirrorsManager.getBeatmap({ + beatmapHash, + }); - const awaitedResult = await request; + // Skip a tick to check if is on cooldown + await new Promise(r => setTimeout(r, 0)); - expect(mockApiGet).toHaveBeenCalledTimes(1); + let capacity = client.getCapacity( + ClientAbilities.GetBeatmapByHash, + ); - capacity = client.getCapacity( - ClientAbilities.GetBeatmapByHash, - ); + expect(capacity.remaining).toBeLessThan(capacity.limit); - expect(awaitedResult.status).toBe(200); - expect(awaitedResult.result).not.toBeNull(); + const awaitedResult = await request; - expect(awaitedResult.result?.checksum).toBe(beatmapHash); + expect(mockApiGet).toHaveBeenCalledTimes(1); - expect(capacity.remaining).toBeLessThan(capacity.limit); + capacity = client.getCapacity( + ClientAbilities.GetBeatmapByHash, + ); + + expect(awaitedResult.status).toBe(200); + expect(awaitedResult.result).not.toBeNull(); + + expect(awaitedResult.result?.checksum).toBe(beatmapHash); + + expect(capacity.remaining).toBeLessThan(capacity.limit); }, - ); + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully return 404 when beatmap is not found`, async (mirror) => { - const client = getMirrorClient(mirror); - - const beatmapHash = faker.string.uuid(); - - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: null, - status: 404, - headers: {}, - }, - ); - - const request = mirrorsManager.getBeatmap({ - beatmapHash, - }); + const client = getMirrorClient(mirror); + + const beatmapHash = faker.string.uuid(); - const awaitedResult = await request; + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: null, + status: 404, + headers: {}, + }, + ); + + const request = mirrorsManager.getBeatmap({ + beatmapHash, + }); - expect(mockApiGet).toHaveBeenCalledTimes(1); + const awaitedResult = await request; - expect(awaitedResult.status).toBe(404); - expect(awaitedResult.result).toBeNull(); + expect(mockApiGet).toHaveBeenCalledTimes(1); + + expect(awaitedResult.status).toBe(404); + expect(awaitedResult.result).toBeNull(); }, - ); + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully return 502 when API request fails and no other mirrors are available`, async (mirror) => { - const client = getMirrorClient(mirror); - - const beatmapHash = faker.string.uuid(); - - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: null, - status: 500, - headers: {}, - }, - ); - - const request = mirrorsManager.getBeatmap({ - beatmapHash, - }); + const client = getMirrorClient(mirror); - const awaitedResult = await request; + const beatmapHash = faker.string.uuid(); - expect(mockApiGet).toHaveBeenCalledTimes(1); + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: null, + status: 500, + headers: {}, + }, + ); + + const request = mirrorsManager.getBeatmap({ + beatmapHash, + }); + + const awaitedResult = await request; + + expect(mockApiGet).toHaveBeenCalledTimes(1); - expect(awaitedResult.status).toBe(502); - expect(awaitedResult.result).toBeNull(); + expect(awaitedResult.status).toBe(502); + expect(awaitedResult.result).toBeNull(); }, - ); - }); + ); + }); - describe('DownloadBeatmapSetById', () => { - const mirrors = getMirrorsWithAbility( - ClientAbilities.DownloadBeatmapSetById, - ); + describe("DownloadBeatmapSetById", () => { + const mirrors = getMirrorsWithAbility( + ClientAbilities.DownloadBeatmapSetById, + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully download a beatmap set by id`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const beatmapSetId = faker.number.int({ - min: 1, - max: 1000000, - }); + const beatmapSetId = faker.number.int({ + min: 1, + max: 1000000, + }); - const { mockArrayBuffer } = - Mocker.getClientMockMethods(client); + const { mockArrayBuffer } + = Mocker.getClientMockMethods(client); - mockArrayBuffer(); + mockArrayBuffer(); - const result = await mirrorsManager.downloadBeatmapSet({ - beatmapSetId, - }); + const result = await mirrorsManager.downloadBeatmapSet({ + beatmapSetId, + }); - expect(result.status).toBe(200); - expect(result.result).not.toBeNull(); - expect(result.result?.byteLength).toBe(1024); + expect(result.status).toBe(200); + expect(result.result).not.toBeNull(); + expect(result.result?.byteLength).toBe(1024); }, - ); + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully update ratelimit during download beatmap set by id request`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const beatmapSetId = faker.number.int({ - min: 1, - max: 1000000, - }); + const beatmapSetId = faker.number.int({ + min: 1, + max: 1000000, + }); - const { generateArrayBuffer } = - Mocker.getClientGenerateMethods(client); - - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: generateArrayBuffer(), - status: 200, - headers: {}, - }, - ); - - const request = mirrorsManager.downloadBeatmapSet({ - beatmapSetId, - }); + const { generateArrayBuffer } + = Mocker.getClientGenerateMethods(client); - // Skip a tick to check if is on cooldown - await new Promise((r) => setTimeout(r, 0)); + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: generateArrayBuffer(), + status: 200, + headers: {}, + }, + ); + + const request = mirrorsManager.downloadBeatmapSet({ + beatmapSetId, + }); - let capacity = client.getCapacity( - ClientAbilities.DownloadBeatmapSetById, - ); + // Skip a tick to check if is on cooldown + await new Promise(r => setTimeout(r, 0)); - expect(capacity.remaining).toBeLessThan(capacity.limit); + let capacity = client.getCapacity( + ClientAbilities.DownloadBeatmapSetById, + ); - const awaitedResult = await request; + expect(capacity.remaining).toBeLessThan(capacity.limit); - expect(mockApiGet).toHaveBeenCalledTimes(1); + const awaitedResult = await request; - capacity = client.getCapacity( - ClientAbilities.DownloadBeatmapSetById, - ); + expect(mockApiGet).toHaveBeenCalledTimes(1); - expect(awaitedResult.status).toBe(200); - expect(awaitedResult.result).not.toBeNull(); + capacity = client.getCapacity( + ClientAbilities.DownloadBeatmapSetById, + ); - expect(awaitedResult.result?.byteLength).toBe(1024); + expect(awaitedResult.status).toBe(200); + expect(awaitedResult.result).not.toBeNull(); - expect(capacity.remaining).toBeLessThan(capacity.limit); + expect(awaitedResult.result?.byteLength).toBe(1024); + + expect(capacity.remaining).toBeLessThan(capacity.limit); }, - ); + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully return 404 when download beatmap set by id is not found`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const beatmapSetId = faker.number.int({ - min: 1, - max: 1000000, - }); + const beatmapSetId = faker.number.int({ + min: 1, + max: 1000000, + }); - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: null, - status: 404, - headers: {}, - }, - ); - - const request = mirrorsManager.downloadBeatmapSet({ - beatmapSetId, - }); + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: null, + status: 404, + headers: {}, + }, + ); - const awaitedResult = await request; + const request = mirrorsManager.downloadBeatmapSet({ + beatmapSetId, + }); - expect(mockApiGet).toHaveBeenCalledTimes(1); + const awaitedResult = await request; - expect(awaitedResult.status).toBe(404); - expect(awaitedResult.result).toBeNull(); + expect(mockApiGet).toHaveBeenCalledTimes(1); + + expect(awaitedResult.status).toBe(404); + expect(awaitedResult.result).toBeNull(); }, - ); + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully return 502 when API request fails and no other mirrors are available when downloading beatmap set by id`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const beatmapSetId = faker.number.int({ - min: 1, - max: 1000000, - }); + const beatmapSetId = faker.number.int({ + min: 1, + max: 1000000, + }); - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: null, - status: 500, - headers: {}, - }, - ); - - const request = mirrorsManager.downloadBeatmapSet({ - beatmapSetId, - }); + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: null, + status: 500, + headers: {}, + }, + ); + + const request = mirrorsManager.downloadBeatmapSet({ + beatmapSetId, + }); - const awaitedResult = await request; + const awaitedResult = await request; - expect(mockApiGet).toHaveBeenCalledTimes(1); + expect(mockApiGet).toHaveBeenCalledTimes(1); - expect(awaitedResult.status).toBe(502); - expect(awaitedResult.result).toBeNull(); + expect(awaitedResult.status).toBe(502); + expect(awaitedResult.result).toBeNull(); }, - ); - }); + ); + }); - describe('DownloadBeatmapSetByIdNoVideo', () => { - const mirrors = getMirrorsWithAbility( - ClientAbilities.DownloadBeatmapSetByIdNoVideo, - ); + describe("DownloadBeatmapSetByIdNoVideo", () => { + const mirrors = getMirrorsWithAbility( + ClientAbilities.DownloadBeatmapSetByIdNoVideo, + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully download a beatmap set by id without video`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const beatmapSetId = faker.number.int({ - min: 1, - max: 1000000, - }); + const beatmapSetId = faker.number.int({ + min: 1, + max: 1000000, + }); - const { mockArrayBuffer } = - Mocker.getClientMockMethods(client); + const { mockArrayBuffer } + = Mocker.getClientMockMethods(client); - mockArrayBuffer(); + mockArrayBuffer(); - const result = await mirrorsManager.downloadBeatmapSet({ - beatmapSetId, - noVideo: true, - }); + const result = await mirrorsManager.downloadBeatmapSet({ + beatmapSetId, + noVideo: true, + }); - expect(result.status).toBe(200); - expect(result.result).not.toBeNull(); - expect(result.result?.byteLength).toBe(1024); + expect(result.status).toBe(200); + expect(result.result).not.toBeNull(); + expect(result.result?.byteLength).toBe(1024); }, - ); + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully update ratelimit during download beatmap set by id without video request`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const beatmapSetId = faker.number.int({ - min: 1, - max: 1000000, - }); + const beatmapSetId = faker.number.int({ + min: 1, + max: 1000000, + }); - const { generateArrayBuffer } = - Mocker.getClientGenerateMethods(client); - - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: generateArrayBuffer(), - status: 200, - headers: {}, - }, - ); - - const request = mirrorsManager.downloadBeatmapSet({ - beatmapSetId, - noVideo: true, - }); + const { generateArrayBuffer } + = Mocker.getClientGenerateMethods(client); + + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: generateArrayBuffer(), + status: 200, + headers: {}, + }, + ); + + const request = mirrorsManager.downloadBeatmapSet({ + beatmapSetId, + noVideo: true, + }); - // Skip a tick to check if is on cooldown - await new Promise((r) => setTimeout(r, 0)); + // Skip a tick to check if is on cooldown + await new Promise(r => setTimeout(r, 0)); - let capacity = client.getCapacity( - ClientAbilities.DownloadBeatmapSetByIdNoVideo, - ); + let capacity = client.getCapacity( + ClientAbilities.DownloadBeatmapSetByIdNoVideo, + ); - expect(capacity.remaining).toBeLessThan(capacity.limit); + expect(capacity.remaining).toBeLessThan(capacity.limit); - const awaitedResult = await request; + const awaitedResult = await request; - expect(mockApiGet).toHaveBeenCalledTimes(1); + expect(mockApiGet).toHaveBeenCalledTimes(1); - capacity = client.getCapacity( - ClientAbilities.DownloadBeatmapSetByIdNoVideo, - ); + capacity = client.getCapacity( + ClientAbilities.DownloadBeatmapSetByIdNoVideo, + ); - expect(awaitedResult.status).toBe(200); - expect(awaitedResult.result).not.toBeNull(); + expect(awaitedResult.status).toBe(200); + expect(awaitedResult.result).not.toBeNull(); - expect(awaitedResult.result?.byteLength).toBe(1024); + expect(awaitedResult.result?.byteLength).toBe(1024); - expect(capacity.remaining).toBeLessThan(capacity.limit); + expect(capacity.remaining).toBeLessThan(capacity.limit); }, - ); + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully return 404 when download beatmap set by id without video is not found`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const beatmapSetId = faker.number.int({ - min: 1, - max: 1000000, - }); + const beatmapSetId = faker.number.int({ + min: 1, + max: 1000000, + }); - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: null, - status: 404, - headers: {}, - }, - ); - - const request = mirrorsManager.downloadBeatmapSet({ - beatmapSetId, - noVideo: true, - }); + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: null, + status: 404, + headers: {}, + }, + ); - const awaitedResult = await request; + const request = mirrorsManager.downloadBeatmapSet({ + beatmapSetId, + noVideo: true, + }); - expect(mockApiGet).toHaveBeenCalledTimes(1); + const awaitedResult = await request; - expect(awaitedResult.status).toBe(404); - expect(awaitedResult.result).toBeNull(); + expect(mockApiGet).toHaveBeenCalledTimes(1); + + expect(awaitedResult.status).toBe(404); + expect(awaitedResult.result).toBeNull(); }, - ); + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully return 502 when API request fails and no other mirrors are available when downloading beatmap set by id without video`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const beatmapSetId = faker.number.int({ - min: 1, - max: 1000000, - }); + const beatmapSetId = faker.number.int({ + min: 1, + max: 1000000, + }); - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: null, - status: 500, - headers: {}, - }, - ); - - const request = mirrorsManager.downloadBeatmapSet({ - beatmapSetId, - noVideo: true, - }); + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: null, + status: 500, + headers: {}, + }, + ); - const awaitedResult = await request; + const request = mirrorsManager.downloadBeatmapSet({ + beatmapSetId, + noVideo: true, + }); - expect(mockApiGet).toHaveBeenCalledTimes(1); + const awaitedResult = await request; - expect(awaitedResult.status).toBe(502); - expect(awaitedResult.result).toBeNull(); + expect(mockApiGet).toHaveBeenCalledTimes(1); + + expect(awaitedResult.status).toBe(502); + expect(awaitedResult.result).toBeNull(); }, - ); - }); + ); + }); - describe('GetBeatmapsetsByBeatmapIds', () => { - const mirrors = getMirrorsWithAbility( - ClientAbilities.GetBeatmapsetsByBeatmapIds, - ); + describe("GetBeatmapsetsByBeatmapIds", () => { + const mirrors = getMirrorsWithAbility( + ClientAbilities.GetBeatmapsetsByBeatmapIds, + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully fetch beatmapsets by beatmap ids`, async (mirror) => { - const client = getMirrorClient(mirror); - - const beatmapIds = [ - faker.number.int({ min: 1, max: 1000000 }), - faker.number.int({ min: 1, max: 1000000 }), - ]; - - const { generateBeatmapset, generateBeatmap } = - Mocker.getClientGenerateMethods(client); - - const beatmapset1 = generateBeatmapset({ - id: faker.number.int({ min: 1, max: 1000000 }), - }); - const beatmapset2 = generateBeatmapset({ - id: faker.number.int({ min: 1, max: 1000000 }), - }); + const client = getMirrorClient(mirror); + + const beatmapIds = [ + faker.number.int({ min: 1, max: 1000000 }), + faker.number.int({ min: 1, max: 1000000 }), + ]; + + const { generateBeatmapset, generateBeatmap } + = Mocker.getClientGenerateMethods(client); + + const beatmapset1 = generateBeatmapset({ + id: faker.number.int({ min: 1, max: 1000000 }), + }); + const beatmapset2 = generateBeatmapset({ + id: faker.number.int({ min: 1, max: 1000000 }), + }); + + const beatmap1 = generateBeatmap({ + id: beatmapIds[0], + beatmapset_id: beatmapset1.id, + }); + const beatmap2 = generateBeatmap({ + id: beatmapIds[1], + beatmapset_id: beatmapset2.id, + }); + + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: { + beatmaps: [ + { ...beatmap1, beatmapset: beatmapset1 }, + { ...beatmap2, beatmapset: beatmapset2 }, + ], + }, + status: 200, + headers: {}, + }, + ); - const beatmap1 = generateBeatmap({ - id: beatmapIds[0], - beatmapset_id: beatmapset1.id, - }); - const beatmap2 = generateBeatmap({ - id: beatmapIds[1], - beatmapset_id: beatmapset2.id, + const result + = await mirrorsManager.getBeatmapsetsByBeatmapIds({ + beatmapIds, }); - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: { - beatmaps: [ - { ...beatmap1, beatmapset: beatmapset1 }, - { ...beatmap2, beatmapset: beatmapset2 }, - ], - }, - status: 200, - headers: {}, - }, - ); - - const result = - await mirrorsManager.getBeatmapsetsByBeatmapIds({ - beatmapIds, - }); - - expect(mockApiGet).toHaveBeenCalledTimes(1); - expect(result.status).toBe(200); - expect(result.result).not.toBeNull(); - expect(Array.isArray(result.result)).toBe(true); - expect(result.result?.length).toBeGreaterThan(0); + expect(mockApiGet).toHaveBeenCalledTimes(1); + expect(result.status).toBe(200); + expect(result.result).not.toBeNull(); + expect(Array.isArray(result.result)).toBe(true); + expect(result.result?.length).toBeGreaterThan(0); }, - ); + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully update ratelimit during get beatmapsets by beatmap ids request`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const beatmapIds = [ - faker.number.int({ min: 1, max: 1000000 }), - ]; + const beatmapIds = [ + faker.number.int({ min: 1, max: 1000000 }), + ]; - const { generateBeatmapset, generateBeatmap } = - Mocker.getClientGenerateMethods(client); + const { generateBeatmapset, generateBeatmap } + = Mocker.getClientGenerateMethods(client); - const beatmapset = generateBeatmapset({ - id: faker.number.int({ min: 1, max: 1000000 }), - }); + const beatmapset = generateBeatmapset({ + id: faker.number.int({ min: 1, max: 1000000 }), + }); - const beatmap = generateBeatmap({ - id: beatmapIds[0], - beatmapset_id: beatmapset.id, - }); + const beatmap = generateBeatmap({ + id: beatmapIds[0], + beatmapset_id: beatmapset.id, + }); - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: { - beatmaps: [{ ...beatmap, beatmapset }], - }, - status: 200, - headers: {}, - }, - ); - - const request = mirrorsManager.getBeatmapsetsByBeatmapIds({ - beatmapIds, - }); + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: { + beatmaps: [{ ...beatmap, beatmapset }], + }, + status: 200, + headers: {}, + }, + ); + + const request = mirrorsManager.getBeatmapsetsByBeatmapIds({ + beatmapIds, + }); - // Skip a tick to check if is on cooldown - await new Promise((r) => setTimeout(r, 0)); + // Skip a tick to check if is on cooldown + await new Promise(r => setTimeout(r, 0)); - let capacity = client.getCapacity( - ClientAbilities.GetBeatmapsetsByBeatmapIds, - ); + let capacity = client.getCapacity( + ClientAbilities.GetBeatmapsetsByBeatmapIds, + ); - expect(capacity.remaining).toBeLessThan(capacity.limit); + expect(capacity.remaining).toBeLessThan(capacity.limit); - const awaitedResult = await request; + const awaitedResult = await request; - expect(mockApiGet).toHaveBeenCalledTimes(1); + expect(mockApiGet).toHaveBeenCalledTimes(1); - capacity = client.getCapacity( - ClientAbilities.GetBeatmapsetsByBeatmapIds, - ); + capacity = client.getCapacity( + ClientAbilities.GetBeatmapsetsByBeatmapIds, + ); - expect(awaitedResult.status).toBe(200); - expect(awaitedResult.result).not.toBeNull(); - expect(Array.isArray(awaitedResult.result)).toBe(true); + expect(awaitedResult.status).toBe(200); + expect(awaitedResult.result).not.toBeNull(); + expect(Array.isArray(awaitedResult.result)).toBe(true); - expect(capacity.remaining).toBeLessThan(capacity.limit); + expect(capacity.remaining).toBeLessThan(capacity.limit); }, - ); + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully return 404 when beatmapsets are not found`, async (mirror) => { - const client = getMirrorClient(mirror); - - const beatmapIds = [ - faker.number.int({ min: 1, max: 1000000 }), - ]; - - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: null, - status: 404, - headers: {}, - }, - ); - - const request = mirrorsManager.getBeatmapsetsByBeatmapIds({ - beatmapIds, - }); + const client = getMirrorClient(mirror); + + const beatmapIds = [ + faker.number.int({ min: 1, max: 1000000 }), + ]; - const awaitedResult = await request; + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: null, + status: 404, + headers: {}, + }, + ); + + const request = mirrorsManager.getBeatmapsetsByBeatmapIds({ + beatmapIds, + }); - expect(mockApiGet).toHaveBeenCalledTimes(1); + const awaitedResult = await request; - expect(awaitedResult.status).toBe(404); - expect(awaitedResult.result).toBeNull(); + expect(mockApiGet).toHaveBeenCalledTimes(1); + + expect(awaitedResult.status).toBe(404); + expect(awaitedResult.result).toBeNull(); }, - ); + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully return 502 when API request fails and no other mirrors are available`, async (mirror) => { - const client = getMirrorClient(mirror); - - const beatmapIds = [ - faker.number.int({ min: 1, max: 1000000 }), - ]; - - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: null, - status: 500, - headers: {}, - }, - ); - - const request = mirrorsManager.getBeatmapsetsByBeatmapIds({ - beatmapIds, - }); + const client = getMirrorClient(mirror); + + const beatmapIds = [ + faker.number.int({ min: 1, max: 1000000 }), + ]; + + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: null, + status: 500, + headers: {}, + }, + ); + + const request = mirrorsManager.getBeatmapsetsByBeatmapIds({ + beatmapIds, + }); - const awaitedResult = await request; + const awaitedResult = await request; - expect(mockApiGet).toHaveBeenCalledTimes(1); + expect(mockApiGet).toHaveBeenCalledTimes(1); - expect(awaitedResult.status).toBe(502); - expect(awaitedResult.result).toBeNull(); + expect(awaitedResult.status).toBe(502); + expect(awaitedResult.result).toBeNull(); }, - ); - }); + ); + }); - describe('DownloadOsuBeatmap', () => { - const mirrors = getMirrorsWithAbility( - ClientAbilities.DownloadOsuBeatmap, - ); + describe("DownloadOsuBeatmap", () => { + const mirrors = getMirrorsWithAbility( + ClientAbilities.DownloadOsuBeatmap, + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully download an osu beatmap`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const beatmapId = faker.number.int({ - min: 1, - max: 1000000, - }); + const beatmapId = faker.number.int({ + min: 1, + max: 1000000, + }); - const { mockArrayBuffer } = - Mocker.getClientMockMethods(client); + const { mockArrayBuffer } + = Mocker.getClientMockMethods(client); - mockArrayBuffer(); + mockArrayBuffer(); - const result = await mirrorsManager.downloadOsuBeatmap({ - beatmapId, - }); + const result = await mirrorsManager.downloadOsuBeatmap({ + beatmapId, + }); - expect(result.status).toBe(200); - expect(result.result).not.toBeNull(); - expect(result.result?.byteLength).toBe(1024); + expect(result.status).toBe(200); + expect(result.result).not.toBeNull(); + expect(result.result?.byteLength).toBe(1024); }, - ); + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully update ratelimit during download osu beatmap request`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const beatmapId = faker.number.int({ - min: 1, - max: 1000000, - }); + const beatmapId = faker.number.int({ + min: 1, + max: 1000000, + }); - const { generateArrayBuffer } = - Mocker.getClientGenerateMethods(client); - - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: generateArrayBuffer(), - status: 200, - headers: {}, - }, - ); - - const request = mirrorsManager.downloadOsuBeatmap({ - beatmapId, - }); + const { generateArrayBuffer } + = Mocker.getClientGenerateMethods(client); + + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: generateArrayBuffer(), + status: 200, + headers: {}, + }, + ); + + const request = mirrorsManager.downloadOsuBeatmap({ + beatmapId, + }); - // Skip a tick to check if is on cooldown - await new Promise((r) => setTimeout(r, 0)); + // Skip a tick to check if is on cooldown + await new Promise(r => setTimeout(r, 0)); - let capacity = client.getCapacity( - ClientAbilities.DownloadOsuBeatmap, - ); + let capacity = client.getCapacity( + ClientAbilities.DownloadOsuBeatmap, + ); - expect(capacity.remaining).toBeLessThan(capacity.limit); + expect(capacity.remaining).toBeLessThan(capacity.limit); - const awaitedResult = await request; + const awaitedResult = await request; - expect(mockApiGet).toHaveBeenCalledTimes(1); + expect(mockApiGet).toHaveBeenCalledTimes(1); - capacity = client.getCapacity( - ClientAbilities.DownloadOsuBeatmap, - ); + capacity = client.getCapacity( + ClientAbilities.DownloadOsuBeatmap, + ); - expect(awaitedResult.status).toBe(200); - expect(awaitedResult.result).not.toBeNull(); - expect(awaitedResult.result?.byteLength).toBe(1024); + expect(awaitedResult.status).toBe(200); + expect(awaitedResult.result).not.toBeNull(); + expect(awaitedResult.result?.byteLength).toBe(1024); - expect(capacity.remaining).toBeLessThan(capacity.limit); + expect(capacity.remaining).toBeLessThan(capacity.limit); }, - ); + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully return 404 when download osu beatmap is not found`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const beatmapId = faker.number.int({ - min: 1, - max: 1000000, - }); + const beatmapId = faker.number.int({ + min: 1, + max: 1000000, + }); - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: null, - status: 404, - headers: {}, - }, - ); - - const request = mirrorsManager.downloadOsuBeatmap({ - beatmapId, - }); + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: null, + status: 404, + headers: {}, + }, + ); - const awaitedResult = await request; + const request = mirrorsManager.downloadOsuBeatmap({ + beatmapId, + }); - expect(mockApiGet).toHaveBeenCalledTimes(1); + const awaitedResult = await request; - expect(awaitedResult.status).toBe(404); - expect(awaitedResult.result).toBeNull(); + expect(mockApiGet).toHaveBeenCalledTimes(1); + + expect(awaitedResult.status).toBe(404); + expect(awaitedResult.result).toBeNull(); }, - ); + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully return 502 when API request fails and no other mirrors are available when downloading osu beatmap`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const beatmapId = faker.number.int({ - min: 1, - max: 1000000, - }); + const beatmapId = faker.number.int({ + min: 1, + max: 1000000, + }); - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: null, - status: 500, - headers: {}, - }, - ); - - const request = mirrorsManager.downloadOsuBeatmap({ - beatmapId, - }); + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: null, + status: 500, + headers: {}, + }, + ); - const awaitedResult = await request; + const request = mirrorsManager.downloadOsuBeatmap({ + beatmapId, + }); + + const awaitedResult = await request; - expect(mockApiGet).toHaveBeenCalledTimes(1); + expect(mockApiGet).toHaveBeenCalledTimes(1); - expect(awaitedResult.status).toBe(502); - expect(awaitedResult.result).toBeNull(); + expect(awaitedResult.status).toBe(502); + expect(awaitedResult.result).toBeNull(); }, - ); - }); + ); + }); - describe('GetBeatmapByIdWithSomeNonBeatmapValues', () => { - const mirrors = getMirrorsWithAbility( - ClientAbilities.GetBeatmapByIdWithSomeNonBeatmapValues, - ); + describe("GetBeatmapByIdWithSomeNonBeatmapValues", () => { + const mirrors = getMirrorsWithAbility( + ClientAbilities.GetBeatmapByIdWithSomeNonBeatmapValues, + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully fetch a beatmap by id`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const beatmapId = faker.number.int({ - min: 1, - max: 1000000, - }); + const beatmapId = faker.number.int({ + min: 1, + max: 1000000, + }); - const { mockBeatmap } = Mocker.getClientMockMethods(client); - mockBeatmap({ - data: { - id: beatmapId, - }, - }); + const { mockBeatmap } = Mocker.getClientMockMethods(client); + mockBeatmap({ + data: { + id: beatmapId, + }, + }); - const result = await mirrorsManager.getBeatmap({ - beatmapId, - allowMissingNonBeatmapValues: true, - }); + const result = await mirrorsManager.getBeatmap({ + beatmapId, + allowMissingNonBeatmapValues: true, + }); - expect(result.status).toBe(200); - expect(result.result).not.toBeNull(); - expect(result.result?.id).toBe(beatmapId); + expect(result.status).toBe(200); + expect(result.result).not.toBeNull(); + expect(result.result?.id).toBe(beatmapId); }, - ); + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully update ratelimit during get beatmap by id request`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const { generateBeatmap } = - Mocker.getClientGenerateMethods(client); + const { generateBeatmap } + = Mocker.getClientGenerateMethods(client); - const beatmapId = faker.number.int({ - min: 1, - max: 1000000, - }); + const beatmapId = faker.number.int({ + min: 1, + max: 1000000, + }); - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: generateBeatmap({ id: beatmapId }), - status: 200, - headers: {}, - }, - ); - - const request = mirrorsManager.getBeatmap({ - beatmapId, - allowMissingNonBeatmapValues: true, - }); + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: generateBeatmap({ id: beatmapId }), + status: 200, + headers: {}, + }, + ); + + const request = mirrorsManager.getBeatmap({ + beatmapId, + allowMissingNonBeatmapValues: true, + }); - // Skip a tick to check if is on cooldown - await new Promise((r) => setTimeout(r, 0)); + // Skip a tick to check if is on cooldown + await new Promise(r => setTimeout(r, 0)); - let capacity = client.getCapacity( - ClientAbilities.GetBeatmapById, - ); + let capacity = client.getCapacity( + ClientAbilities.GetBeatmapById, + ); - expect(capacity.remaining).toBeLessThan(capacity.limit); + expect(capacity.remaining).toBeLessThan(capacity.limit); - const awaitedResult = await request; + const awaitedResult = await request; - expect(mockApiGet).toHaveBeenCalledTimes(1); + expect(mockApiGet).toHaveBeenCalledTimes(1); - capacity = client.getCapacity( - ClientAbilities.GetBeatmapById, - ); + capacity = client.getCapacity( + ClientAbilities.GetBeatmapById, + ); - expect(awaitedResult.status).toBe(200); - expect(awaitedResult.result).not.toBeNull(); - expect(awaitedResult.result?.id).toBe(beatmapId); + expect(awaitedResult.status).toBe(200); + expect(awaitedResult.result).not.toBeNull(); + expect(awaitedResult.result?.id).toBe(beatmapId); - expect(capacity.remaining).toBeLessThan(capacity.limit); + expect(capacity.remaining).toBeLessThan(capacity.limit); }, - ); + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully return 404 when beatmap is not found`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const beatmapId = faker.number.int({ - min: 1, - max: 1000000, - }); + const beatmapId = faker.number.int({ + min: 1, + max: 1000000, + }); - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: null, - status: 404, - headers: {}, - }, - ); - - const request = mirrorsManager.getBeatmap({ - beatmapId, - allowMissingNonBeatmapValues: true, - }); + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: null, + status: 404, + headers: {}, + }, + ); + + const request = mirrorsManager.getBeatmap({ + beatmapId, + allowMissingNonBeatmapValues: true, + }); - const awaitedResult = await request; + const awaitedResult = await request; - expect(mockApiGet).toHaveBeenCalledTimes(1); + expect(mockApiGet).toHaveBeenCalledTimes(1); - expect(awaitedResult.status).toBe(404); - expect(awaitedResult.result).toBeNull(); + expect(awaitedResult.status).toBe(404); + expect(awaitedResult.result).toBeNull(); }, - ); + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully return 502 when API request fails and no other mirrors are available`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const beatmapId = faker.number.int({ - min: 1, - max: 1000000, - }); + const beatmapId = faker.number.int({ + min: 1, + max: 1000000, + }); - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: null, - status: 500, - headers: {}, - }, - ); - - const request = mirrorsManager.getBeatmap({ - beatmapId, - allowMissingNonBeatmapValues: true, - }); + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: null, + status: 500, + headers: {}, + }, + ); - const awaitedResult = await request; + const request = mirrorsManager.getBeatmap({ + beatmapId, + allowMissingNonBeatmapValues: true, + }); + + const awaitedResult = await request; - expect(mockApiGet).toHaveBeenCalledTimes(1); + expect(mockApiGet).toHaveBeenCalledTimes(1); - expect(awaitedResult.status).toBe(502); - expect(awaitedResult.result).toBeNull(); + expect(awaitedResult.status).toBe(502); + expect(awaitedResult.result).toBeNull(); }, - ); - }); + ); + }); - describe('GetBeatmapByHashWithSomeNonBeatmapValues', () => { - const mirrors = getMirrorsWithAbility( - ClientAbilities.GetBeatmapByHashWithSomeNonBeatmapValues, - ); + describe("GetBeatmapByHashWithSomeNonBeatmapValues", () => { + const mirrors = getMirrorsWithAbility( + ClientAbilities.GetBeatmapByHashWithSomeNonBeatmapValues, + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully fetch a beatmap by hash`, async (mirror) => { - const client = getMirrorClient(mirror); + const client = getMirrorClient(mirror); - const beatmapHash = faker.string.uuid(); + const beatmapHash = faker.string.uuid(); - const { mockBeatmap } = Mocker.getClientMockMethods(client); + const { mockBeatmap } = Mocker.getClientMockMethods(client); - mockBeatmap({ - data: { - checksum: beatmapHash, - }, - }); + mockBeatmap({ + data: { + checksum: beatmapHash, + }, + }); - const result = await mirrorsManager.getBeatmap({ - beatmapHash, - allowMissingNonBeatmapValues: true, - }); + const result = await mirrorsManager.getBeatmap({ + beatmapHash, + allowMissingNonBeatmapValues: true, + }); - expect(result.status).toBe(200); - expect(result.result).not.toBeNull(); - expect(result.result?.checksum).toBe(beatmapHash); + expect(result.status).toBe(200); + expect(result.result).not.toBeNull(); + expect(result.result?.checksum).toBe(beatmapHash); }, - ); + ); - test.each(mirrors)( + test.each(mirrors)( `$name: Should successfully update ratelimit during get beatmap by hash request`, async (mirror) => { - const client = getMirrorClient(mirror); - - const beatmapHash = faker.string.uuid(); - - const { generateBeatmap, generateBeatmapset } = - Mocker.getClientGenerateMethods(client); - - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: generateBeatmap({ - checksum: beatmapHash, - }), - status: 200, - headers: {}, - }, - ); - - const request = mirrorsManager.getBeatmap({ - beatmapHash, - allowMissingNonBeatmapValues: true, - }); + const client = getMirrorClient(mirror); - // Skip a tick to check if is on cooldown - await new Promise((r) => setTimeout(r, 0)); + const beatmapHash = faker.string.uuid(); - let capacity = client.getCapacity( - ClientAbilities.GetBeatmapByHash, - ); + const { generateBeatmap } + = Mocker.getClientGenerateMethods(client); - expect(capacity.remaining).toBeLessThan(capacity.limit); + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: generateBeatmap({ + checksum: beatmapHash, + }), + status: 200, + headers: {}, + }, + ); - const awaitedResult = await request; + const request = mirrorsManager.getBeatmap({ + beatmapHash, + allowMissingNonBeatmapValues: true, + }); - expect(mockApiGet).toHaveBeenCalledTimes(1); + // Skip a tick to check if is on cooldown + await new Promise(r => setTimeout(r, 0)); - capacity = client.getCapacity( - ClientAbilities.GetBeatmapByHash, - ); + let capacity = client.getCapacity( + ClientAbilities.GetBeatmapByHash, + ); - expect(awaitedResult.status).toBe(200); - expect(awaitedResult.result).not.toBeNull(); + expect(capacity.remaining).toBeLessThan(capacity.limit); - expect(awaitedResult.result?.checksum).toBe(beatmapHash); + const awaitedResult = await request; - expect(capacity.remaining).toBeLessThan(capacity.limit); - }, - ); + expect(mockApiGet).toHaveBeenCalledTimes(1); - test.each(mirrors)( - `$name: Should successfully return 404 when beatmap is not found`, - async (mirror) => { - const client = getMirrorClient(mirror); - - const beatmapHash = faker.string.uuid(); - - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: null, - status: 404, - headers: {}, - }, - ); - - const request = mirrorsManager.getBeatmap({ - beatmapHash, - allowMissingNonBeatmapValues: true, - }); + capacity = client.getCapacity( + ClientAbilities.GetBeatmapByHash, + ); - const awaitedResult = await request; + expect(awaitedResult.status).toBe(200); + expect(awaitedResult.result).not.toBeNull(); - expect(mockApiGet).toHaveBeenCalledTimes(1); + expect(awaitedResult.result?.checksum).toBe(beatmapHash); - expect(awaitedResult.status).toBe(404); - expect(awaitedResult.result).toBeNull(); + expect(capacity.remaining).toBeLessThan(capacity.limit); }, - ); + ); - test.each(mirrors)( - `$name: Should successfully return 502 when API request fails and no other mirrors are available`, + test.each(mirrors)( + `$name: Should successfully return 404 when beatmap is not found`, async (mirror) => { - const client = getMirrorClient(mirror); - - const beatmapHash = faker.string.uuid(); - - const mockApiGet = Mocker.mockRequest( - client, - 'baseApi', - 'get', - { - data: null, - status: 500, - headers: {}, - }, - ); - - const request = mirrorsManager.getBeatmap({ - beatmapHash, - allowMissingNonBeatmapValues: true, - }); - - const awaitedResult = await request; - - expect(mockApiGet).toHaveBeenCalledTimes(1); - - expect(awaitedResult.status).toBe(502); - expect(awaitedResult.result).toBeNull(); - }, - ); - }); - }); - - describe('Specific cases', () => { - test('Use another mirror when the best one is on cooldown', async () => { - const minoClient = getMirrorClient(MinoClient, [BanchoClient]); - const banchoClient = getMirrorClient(BanchoClient); + const client = getMirrorClient(mirror); - const { mockBeatmap: mockBanchoBeatmapFunc } = - Mocker.getClientMockMethods(banchoClient); + const beatmapHash = faker.string.uuid(); - const { mockBeatmap: mockMinoBeatmapFunc } = - Mocker.getClientMockMethods(minoClient); + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: null, + status: 404, + headers: {}, + }, + ); - Mocker.mockSyncRequest(banchoClient, 'self', 'getCapacity', { - limit: 1, - remaining: 1, - }); + const request = mirrorsManager.getBeatmap({ + beatmapHash, + allowMissingNonBeatmapValues: true, + }); - Mocker.mockSyncRequest(minoClient, 'self', 'getCapacity', { - limit: 1, - remaining: 0, - }); + const awaitedResult = await request; - const mockMinoBeatmap = mockMinoBeatmapFunc({ - data: { - id: 1, - }, - }); + expect(mockApiGet).toHaveBeenCalledTimes(1); - const mockBanchoBeatmap = mockBanchoBeatmapFunc({ - data: { - id: 1, + expect(awaitedResult.status).toBe(404); + expect(awaitedResult.result).toBeNull(); }, - }); + ); - const result = await mirrorsManager.getBeatmap({ - beatmapId: 1, - }); - - expect(mockMinoBeatmap).toHaveBeenCalledTimes(0); - expect(mockBanchoBeatmap).toHaveBeenCalledTimes(1); + test.each(mirrors)( + `$name: Should successfully return 502 when API request fails and no other mirrors are available`, + async (mirror) => { + const client = getMirrorClient(mirror); - expect(result.status).toBe(200); - expect(result.result).not.toBeNull(); - expect(result.result?.id).toBe(1); + const beatmapHash = faker.string.uuid(); - Mocker.mockSyncRequest(banchoClient, 'self', 'getCapacity', { - limit: 1, - remaining: 0, - }); + const mockApiGet = Mocker.mockRequest( + client, + "baseApi", + "get", + { + data: null, + status: 500, + headers: {}, + }, + ); - Mocker.mockSyncRequest(minoClient, 'self', 'getCapacity', { - limit: 1, - remaining: 1, - }); + const request = mirrorsManager.getBeatmap({ + beatmapHash, + allowMissingNonBeatmapValues: true, + }); - const result2 = await mirrorsManager.getBeatmap({ - beatmapId: 1, - }); + const awaitedResult = await request; - expect(mockMinoBeatmap).toHaveBeenCalledTimes(1); - expect(mockBanchoBeatmap).toHaveBeenCalledTimes(1); + expect(mockApiGet).toHaveBeenCalledTimes(1); - expect(result2.status).toBe(200); - expect(result2.result).not.toBeNull(); - expect(result2.result?.id).toBe(1); - }); + expect(awaitedResult.status).toBe(502); + expect(awaitedResult.result).toBeNull(); + }, + ); + }); + }); + + describe("Specific cases", () => { + test("Use another mirror when the best one is on cooldown", async () => { + const minoClient = getMirrorClient(MinoClient, [BanchoClient]); + const banchoClient = getMirrorClient(BanchoClient); + + const { mockBeatmap: mockBanchoBeatmapFunc } + = Mocker.getClientMockMethods(banchoClient); + + const { mockBeatmap: mockMinoBeatmapFunc } + = Mocker.getClientMockMethods(minoClient); + + Mocker.mockSyncRequest(banchoClient, "self", "getCapacity", { + limit: 1, + remaining: 1, + }); + + Mocker.mockSyncRequest(minoClient, "self", "getCapacity", { + limit: 1, + remaining: 0, + }); + + const mockMinoBeatmap = mockMinoBeatmapFunc({ + data: { + id: 1, + }, + }); + + const mockBanchoBeatmap = mockBanchoBeatmapFunc({ + data: { + id: 1, + }, + }); + + const result = await mirrorsManager.getBeatmap({ + beatmapId: 1, + }); + + expect(mockMinoBeatmap).toHaveBeenCalledTimes(0); + expect(mockBanchoBeatmap).toHaveBeenCalledTimes(1); + + expect(result.status).toBe(200); + expect(result.result).not.toBeNull(); + expect(result.result?.id).toBe(1); + + Mocker.mockSyncRequest(banchoClient, "self", "getCapacity", { + limit: 1, + remaining: 0, + }); + + Mocker.mockSyncRequest(minoClient, "self", "getCapacity", { + limit: 1, + remaining: 1, + }); + + const result2 = await mirrorsManager.getBeatmap({ + beatmapId: 1, + }); + + expect(mockMinoBeatmap).toHaveBeenCalledTimes(1); + expect(mockBanchoBeatmap).toHaveBeenCalledTimes(1); + + expect(result2.status).toBe(200); + expect(result2.result).not.toBeNull(); + expect(result2.result?.id).toBe(1); + }); - test('DisableSafeRatelimitMode is set to false, should complete only 90% of the requests', async () => { - config.DisableSafeRatelimitMode = false; + test("DisableSafeRatelimitMode is set to false, should complete only 90% of the requests", async () => { + config.DisableSafeRatelimitMode = false; - const minoClient = getMirrorClient(MinoClient); + const minoClient = getMirrorClient(MinoClient); - const { mockBeatmap: mockMinoBeatmapFunc } = - Mocker.getClientMockMethods(minoClient); + const { mockBeatmap: mockMinoBeatmapFunc } + = Mocker.getClientMockMethods(minoClient); - const totalRequestsLimit = minoClient.getCapacity( - ClientAbilities.GetBeatmapById, - ).limit; + const totalRequestsLimit = minoClient.getCapacity( + ClientAbilities.GetBeatmapById, + ).limit; - const shouldStopAt = totalRequestsLimit; + const shouldStopAt = totalRequestsLimit; - expect(shouldStopAt).toBe( - Math.floor( - // @ts-expect-error skip type check due to protected property - minoClient.api._config.rateLimits.find((limit) => - limit.abilities.includes( - ClientAbilities.GetBeatmapById, - ), - )!.limit * 0.9, - ), - ); + expect(shouldStopAt).toBe( + Math.floor( + // @ts-expect-error skip type check due to protected property + minoClient.api._config.rateLimits.find(limit => + limit.abilities.includes( + ClientAbilities.GetBeatmapById, + ), + )!.limit * 0.9, + ), + ); - for (let i = 0; i < totalRequestsLimit; i++) { - const mockMinoBeatmap = mockMinoBeatmapFunc({ - data: { - id: 1, - }, - }); - - const result = await mirrorsManager.getBeatmap({ - beatmapId: 1, - }); - - expect(result.status).toBe(i < shouldStopAt ? 200 : 502); - expect(result.result?.id ?? null).toBe( - i < shouldStopAt ? 1 : null, - ); - expect(mockMinoBeatmap).toHaveBeenCalledTimes( - Math.min(i + 1, shouldStopAt), - ); - } + for (let i = 0; i < totalRequestsLimit; i++) { + const mockMinoBeatmap = mockMinoBeatmapFunc({ + data: { + id: 1, + }, }); - test('DisableDailyRateLimit is set to true, daily rate limit should be undefined', async () => { - config.DisableDailyRateLimit = true; - - const minoClient = getMirrorClient(MinoClient); - - // @ts-expect-error skip type check due to protected property - const dailyRateLimit = minoClient.api.config.dailyRateLimit; - - expect(dailyRateLimit).toBeUndefined(); + const result = await mirrorsManager.getBeatmap({ + beatmapId: 1, }); - test('DisableDailyRateLimit is set to false, daily rate limit should be defined', async () => { - config.DisableDailyRateLimit = false; - - const minoClient = getMirrorClient(MinoClient); + expect(result.status).toBe(i < shouldStopAt ? 200 : 502); + expect(result.result?.id ?? null).toBe( + i < shouldStopAt ? 1 : null, + ); + expect(mockMinoBeatmap).toHaveBeenCalledTimes( + Math.min(i + 1, shouldStopAt), + ); + } + }); - // @ts-expect-error skip type check due to protected property - const dailyRateLimit = minoClient.api.config.dailyRateLimit; + test("DisableDailyRateLimit is set to true, daily rate limit should be undefined", async () => { + config.DisableDailyRateLimit = true; - expect(dailyRateLimit).toBeDefined(); - }); + const minoClient = getMirrorClient(MinoClient); - test('DisableSafeRatelimitMode is set to true, should complete 100% of the requests', async () => { - config.DisableSafeRatelimitMode = true; + // @ts-expect-error skip type check due to protected property + const { dailyRateLimit } = minoClient.api.config; - const minoClient = getMirrorClient(MinoClient); + expect(dailyRateLimit).toBeUndefined(); + }); - const { mockBeatmap: mockMinoBeatmapFunc } = - Mocker.getClientMockMethods(minoClient); + test("DisableDailyRateLimit is set to false, daily rate limit should be defined", async () => { + config.DisableDailyRateLimit = false; - const totalRequestsLimit = minoClient.getCapacity( - ClientAbilities.GetBeatmapById, - ).limit; + const minoClient = getMirrorClient(MinoClient); - expect(totalRequestsLimit).toBe( - Math.floor( - // @ts-expect-error skip type check due to protected property - minoClient.api._config.rateLimits.find((limit) => - limit.abilities.includes( - ClientAbilities.GetBeatmapById, - ), - )!.limit, - ), - ); + // @ts-expect-error skip type check due to protected property + const { dailyRateLimit } = minoClient.api.config; - for (let i = 0; i < totalRequestsLimit; i++) { - const mockMinoBeatmap = mockMinoBeatmapFunc({ - data: { - id: 1, - }, - }); + expect(dailyRateLimit).toBeDefined(); + }); - const result = await mirrorsManager.getBeatmap({ - beatmapId: 1, - }); + test("DisableSafeRatelimitMode is set to true, should complete 100% of the requests", async () => { + config.DisableSafeRatelimitMode = true; + + const minoClient = getMirrorClient(MinoClient); + + const { mockBeatmap: mockMinoBeatmapFunc } + = Mocker.getClientMockMethods(minoClient); + + const totalRequestsLimit = minoClient.getCapacity( + ClientAbilities.GetBeatmapById, + ).limit; + + expect(totalRequestsLimit).toBe( + Math.floor( + // @ts-expect-error skip type check due to protected property + minoClient.api._config.rateLimits.find(limit => + limit.abilities.includes( + ClientAbilities.GetBeatmapById, + ), + )!.limit, + ), + ); + + for (let i = 0; i < totalRequestsLimit; i++) { + const mockMinoBeatmap = mockMinoBeatmapFunc({ + data: { + id: 1, + }, + }); - expect(result.status).toBe(200); - expect(result.result?.id ?? null).toBe(1); - expect(mockMinoBeatmap).toHaveBeenCalledTimes(i + 1); - } + const result = await mirrorsManager.getBeatmap({ + beatmapId: 1, }); - test('Should clear outdated requests in rate-limiter', async () => { - const minoClient = getMirrorClient(MinoClient); + expect(result.status).toBe(200); + expect(result.result?.id ?? null).toBe(1); + expect(mockMinoBeatmap).toHaveBeenCalledTimes(i + 1); + } + }); - const { mockBeatmap } = Mocker.getClientMockMethods(minoClient); + test("Should clear outdated requests in rate-limiter", async () => { + const minoClient = getMirrorClient(MinoClient); - // @ts-expect-error skip type check due to protected property - Mocker.mockSyncRequest( - minoClient, - 'api', - 'getRequestsArray', - new Map([ - ['/', new Date(Date.now() - 1000 * 65)], - ['/', new Date(Date.now() - 1000 * 55)], - ]), - ); - - const mockedBeatmap = mockBeatmap({ - data: { - id: 1, - }, - }); + const { mockBeatmap } = Mocker.getClientMockMethods(minoClient); - const result = await mirrorsManager.getBeatmap({ - beatmapId: 1, - }); + // @ts-expect-error skip type check due to protected property + Mocker.mockSyncRequest( + minoClient, + "api", + "getRequestsArray", + new Map([ + ["/", new Date(Date.now() - 1000 * 65)], + ["/", new Date(Date.now() - 1000 * 55)], + ]), + ); - expect(mockedBeatmap).toHaveBeenCalledTimes(1); + const mockedBeatmap = mockBeatmap({ + data: { + id: 1, + }, + }); - expect(result.status).toBe(200); - expect(result.result).not.toBeNull(); - expect(result.result?.id).toBe(1); + const result = await mirrorsManager.getBeatmap({ + beatmapId: 1, + }); - const currentRatelimit = minoClient.getCapacity( - ClientAbilities.GetBeatmapById, - ); + expect(mockedBeatmap).toHaveBeenCalledTimes(1); - // Should count one not outdated request, skip the outdated and add the new request - expect(currentRatelimit.remaining).toBe(currentRatelimit.limit - 2); - }); + expect(result.status).toBe(200); + expect(result.result).not.toBeNull(); + expect(result.result?.id).toBe(1); - test('Should clear outdated requests from memory', async () => { - const minoClient = getMirrorClient(MinoClient); + const currentRatelimit = minoClient.getCapacity( + ClientAbilities.GetBeatmapById, + ); - const { mockBeatmap } = Mocker.getClientMockMethods(minoClient); + // Should count one not outdated request, skip the outdated and add the new request + expect(currentRatelimit.remaining).toBe(currentRatelimit.limit - 2); + }); - let currentRatelimit = minoClient.getCapacity( - ClientAbilities.GetBeatmapById, - ); + test("Should clear outdated requests from memory", async () => { + const minoClient = getMirrorClient(MinoClient); - expect(currentRatelimit.remaining).toBe(currentRatelimit.limit); + const { mockBeatmap } = Mocker.getClientMockMethods(minoClient); - mockBeatmap({ - data: { - id: 1, - }, - }); + let currentRatelimit = minoClient.getCapacity( + ClientAbilities.GetBeatmapById, + ); - const result = await mirrorsManager.getBeatmap({ - beatmapId: 1, - }); + expect(currentRatelimit.remaining).toBe(currentRatelimit.limit); - expect(result.status).toBe(200); - expect(result.result).not.toBeNull(); - expect(result.result?.id).toBe(1); + mockBeatmap({ + data: { + id: 1, + }, + }); - currentRatelimit = minoClient.getCapacity( - ClientAbilities.GetBeatmapById, - ); + const result = await mirrorsManager.getBeatmap({ + beatmapId: 1, + }); - // @ts-expect-error skip type check due to protected property - const requests = Array.from(minoClient.api.requests.values()) - .filter((v) => v instanceof Map && v.size > 0) - .flatMap((v) => Array.from(v.entries())).length; + expect(result.status).toBe(200); + expect(result.result).not.toBeNull(); + expect(result.result?.id).toBe(1); - expect(requests).toBe(1); + currentRatelimit = minoClient.getCapacity( + ClientAbilities.GetBeatmapById, + ); - expect(currentRatelimit.remaining).toBe(currentRatelimit.limit - 1); + // @ts-expect-error skip type check due to protected property + const requests = Array.from(minoClient.api.requests.values()) + .filter(v => v instanceof Map && v.size > 0) + .flatMap(v => Array.from(v.entries())) + .length; - setSystemTime(new Date(Date.now() + 1000 * 60 * 60 * 24)); + expect(requests).toBe(1); - mockBeatmap({ - data: { - id: 1, - }, - }); + expect(currentRatelimit.remaining).toBe(currentRatelimit.limit - 1); - const result2 = await mirrorsManager.getBeatmap({ - beatmapId: 1, - }); + setSystemTime(new Date(Date.now() + 1000 * 60 * 60 * 24)); - expect(result2.status).toBe(200); - expect(result2.result).not.toBeNull(); - expect(result2.result?.id).toBe(1); + mockBeatmap({ + data: { + id: 1, + }, + }); - currentRatelimit = minoClient.getCapacity( - ClientAbilities.GetBeatmapById, - ); + const result2 = await mirrorsManager.getBeatmap({ + beatmapId: 1, + }); - // @ts-expect-error skip type check due to protected property - const requests2 = Array.from(minoClient.api.requests.values()) - .filter((v) => v instanceof Map && v.size > 0) - .flatMap((v) => Array.from(v.entries())).length; + expect(result2.status).toBe(200); + expect(result2.result).not.toBeNull(); + expect(result2.result?.id).toBe(1); - expect(requests2).toBe(1); + currentRatelimit = minoClient.getCapacity( + ClientAbilities.GetBeatmapById, + ); - expect(currentRatelimit.remaining).toBe(currentRatelimit.limit - 1); + // @ts-expect-error skip type check due to protected property + const requests2 = Array.from(minoClient.api.requests.values()) + .filter(v => v instanceof Map && v.size > 0) + .flatMap(v => Array.from(v.entries())) + .length; - setSystemTime(); - }); + expect(requests2).toBe(1); - test.each([502, 503, 504])( + expect(currentRatelimit.remaining).toBe(currentRatelimit.limit - 1); + + setSystemTime(); + }); + + test.each([502, 503, 504])( `Should successfully return 502 when API request sends 5xx error and send mirror to cooldown`, async (errorCode) => { - const minoClient = getMirrorClient(MinoClient); + const minoClient = getMirrorClient(MinoClient); - const beatmapId = faker.number.int({ - min: 1, - max: 1000000, - }); + const beatmapId = faker.number.int({ + min: 1, + max: 1000000, + }); - const mockApiGet = Mocker.mockRequest( - minoClient, - 'baseApi', - 'get', - { - data: null, - status: errorCode, - headers: {}, - }, - ); + const mockApiGet = Mocker.mockRequest( + minoClient, + "baseApi", + "get", + { + data: null, + status: errorCode, + headers: {}, + }, + ); - const request = await mirrorsManager.downloadOsuBeatmap({ - beatmapId, - }); + const request = await mirrorsManager.downloadOsuBeatmap({ + beatmapId, + }); - expect(mockApiGet).toHaveBeenCalledTimes(1); + expect(mockApiGet).toHaveBeenCalledTimes(1); - expect(request.status).toBe(502); - expect(request.result).toBeNull(); + expect(request.status).toBe(502); + expect(request.result).toBeNull(); - // Second request should send none requests due to cooldown + // Second request should send none requests due to cooldown - const request2 = await mirrorsManager.downloadOsuBeatmap({ - beatmapId, - }); + const request2 = await mirrorsManager.downloadOsuBeatmap({ + beatmapId, + }); - expect(mockApiGet).toHaveBeenCalledTimes(1); + expect(mockApiGet).toHaveBeenCalledTimes(1); - expect(request2.status).toBe(502); - expect(request2.result).toBeNull(); + expect(request2.status).toBe(502); + expect(request2.result).toBeNull(); }, - ); - }); + ); + }); }); diff --git a/server/tests/stats.endpoint.test.ts b/server/tests/stats.endpoint.test.ts index fbb1856..d365680 100644 --- a/server/tests/stats.endpoint.test.ts +++ b/server/tests/stats.endpoint.test.ts @@ -1,312 +1,313 @@ -import { beforeAll, beforeEach, describe, expect, jest, test } from 'bun:test'; -import { Elysia } from 'elysia'; -import { HttpStatusCode } from 'axios'; -import { Mocker } from './utils/mocker'; -import setup from '../src/setup'; - -describe('Stats Endpoint', () => { - let app: Elysia; - - beforeAll(async () => { - await Mocker.ensureDatabaseInitialized(); - }); - - beforeEach(async () => { - jest.restoreAllMocks(); - Mocker.mockMirrorsBenchmark(); - - app = (await setup()) as unknown as Elysia; - }); - - test('Should return 200 status code', async () => { - const response = await app.handle( - new Request('http://localhost/stats', { - method: 'GET', - }), - ); - - expect(response.status).toBe(HttpStatusCode.Ok); - }); - - test('Should return JSON response with server and manager stats', async () => { - const response = await app.handle( - new Request('http://localhost/stats', { - method: 'GET', - }), - ); - - expect(response.status).toBe(HttpStatusCode.Ok); - - const data = await response.json(); - - expect(data).toHaveProperty('data'); - expect(data.data).toHaveProperty('server'); - expect(data.data).toHaveProperty('manager'); - }); - - test('Should include server statistics with correct structure', async () => { - const response = await app.handle( - new Request('http://localhost/stats', { - method: 'GET', - }), - ); - - const data = await response.json(); - const serverStats = data.data.server; - - expect(serverStats).toHaveProperty('uptime'); - expect(serverStats.uptime).toHaveProperty('nanoseconds'); - expect(serverStats.uptime).toHaveProperty('pretty'); - expect(typeof serverStats.uptime.nanoseconds).toBe('number'); - expect(typeof serverStats.uptime.pretty).toBe('string'); - - expect(serverStats).toHaveProperty('memory'); - expect(serverStats.memory).toHaveProperty('rss'); - expect(serverStats.memory).toHaveProperty('heapTotal'); - expect(serverStats.memory).toHaveProperty('heapUsed'); - expect(serverStats.memory).toHaveProperty('external'); - expect(serverStats.memory).toHaveProperty('arrayBuffers'); - - expect(serverStats).toHaveProperty('pid'); - expect(typeof serverStats.pid).toBe('number'); - - expect(serverStats).toHaveProperty('version'); - expect(typeof serverStats.version).toBe('string'); - - expect(serverStats).toHaveProperty('revision'); - expect(typeof serverStats.revision).toBe('string'); - }); - - test('Should include manager statistics with correct structure', async () => { - const response = await app.handle( - new Request('http://localhost/stats', { - method: 'GET', - }), - ); - - const data = await response.json(); - const managerStats = data.data.manager; - - expect(managerStats).toHaveProperty('storage'); - expect(managerStats).toHaveProperty('mirrors'); - }); - - test('Should include storage statistics with correct structure', async () => { - const response = await app.handle( - new Request('http://localhost/stats', { - method: 'GET', - }), - ); - - const data = await response.json(); - const storageStats = data.data.manager.storage; - - expect(storageStats).toHaveProperty('database'); - expect(storageStats.database).toHaveProperty('beatmaps'); - expect(storageStats.database).toHaveProperty('beatmapSets'); - expect(storageStats.database).toHaveProperty('beatmapSetFile'); - expect(storageStats.database).toHaveProperty('beatmapOsuFile'); - expect(typeof storageStats.database.beatmaps).toBe('number'); - expect(typeof storageStats.database.beatmapSets).toBe('number'); - expect(typeof storageStats.database.beatmapSetFile).toBe('number'); - expect(typeof storageStats.database.beatmapOsuFile).toBe('number'); - - expect(storageStats).toHaveProperty('files'); - expect(storageStats.files).toHaveProperty('totalFiles'); - expect(storageStats.files).toHaveProperty('totalBytes'); - expect(typeof storageStats.files.totalFiles).toBe('number'); - expect(typeof storageStats.files.totalBytes).toBe('string'); - - expect(storageStats).toHaveProperty('cache'); - expect(storageStats.cache).toHaveProperty('beatmaps'); - expect(storageStats.cache).toHaveProperty('beatmapsets'); - expect(storageStats.cache).toHaveProperty('beatmapsetFiles'); - expect(storageStats.cache).toHaveProperty('beatmapOsuFiles'); - }); - - test('Should include cache statistics with correct structure', async () => { - const response = await app.handle( - new Request('http://localhost/stats', { - method: 'GET', - }), - ); - - const data = await response.json(); - const cacheStats = data.data.manager.storage.cache; - - expect(cacheStats.beatmaps).toHaveProperty('byId'); - expect(cacheStats.beatmaps).toHaveProperty('ids'); - expect(cacheStats.beatmaps.ids).toHaveProperty('byHash'); - expect(typeof cacheStats.beatmaps.byId).toBe('number'); - expect(typeof cacheStats.beatmaps.ids.byHash).toBe('number'); - - expect(cacheStats.beatmapsets).toHaveProperty('byId'); - expect(typeof cacheStats.beatmapsets.byId).toBe('number'); - - expect(cacheStats.beatmapsetFiles).toHaveProperty('byId'); - expect(typeof cacheStats.beatmapsetFiles.byId).toBe('number'); - - expect(cacheStats.beatmapOsuFiles).toHaveProperty('byId'); - expect(typeof cacheStats.beatmapOsuFiles.byId).toBe('number'); - }); - - test('Should include mirrors statistics with correct structure', async () => { - const response = await app.handle( - new Request('http://localhost/stats', { - method: 'GET', - }), - ); - - const data = await response.json(); - const mirrorsStats = data.data.manager.mirrors; - - expect(mirrorsStats).toHaveProperty('activeMirrors'); - expect(Array.isArray(mirrorsStats.activeMirrors)).toBe(true); - - expect(mirrorsStats).toHaveProperty('rateLimitsTotal'); - expect(typeof mirrorsStats.rateLimitsTotal).toBe('object'); - }); - - test('Should include active mirrors with correct structure', async () => { - const response = await app.handle( - new Request('http://localhost/stats', { - method: 'GET', - }), - ); - - const data = await response.json(); - const activeMirrors = data.data.manager.mirrors.activeMirrors; - - if (activeMirrors.length > 0) { - const mirror = activeMirrors[0]; - - expect(mirror).toHaveProperty('name'); - expect(mirror).toHaveProperty('url'); - expect(mirror).toHaveProperty('rateLimit'); - expect(mirror).toHaveProperty('requests'); - - expect(typeof mirror.name).toBe('string'); - expect(typeof mirror.url).toBe('string'); - - if ('onCooldownUntil' in mirror) { - expect( - mirror.onCooldownUntil === null || - typeof mirror.onCooldownUntil === 'number', - ).toBe(true); - } - expect(Array.isArray(mirror.rateLimit)).toBe(true); - expect(typeof mirror.requests).toBe('object'); - - expect(mirror.requests).toHaveProperty('lifetime'); - expect(mirror.requests).toHaveProperty('session'); - expect(mirror.requests).toHaveProperty('hour'); - expect(mirror.requests).toHaveProperty('day'); - expect(mirror.requests).toHaveProperty('week'); - expect(mirror.requests).toHaveProperty('month'); - - const timeRange = mirror.requests.lifetime; - expect(timeRange).toHaveProperty('total'); - expect(timeRange).toHaveProperty('successful'); - expect(timeRange).toHaveProperty('failed'); - expect(typeof timeRange.total).toBe('number'); - expect(typeof timeRange.successful).toBe('number'); - expect(typeof timeRange.failed).toBe('number'); - } - }); - - test('Should include rate limit information in active mirrors', async () => { - const response = await app.handle( - new Request('http://localhost/stats', { - method: 'GET', - }), - ); - - const data = await response.json(); - const activeMirrors = data.data.manager.mirrors.activeMirrors; - - if (activeMirrors.length > 0) { - const mirror = activeMirrors[0]; - const rateLimit = mirror.rateLimit; - - if (rateLimit.length > 0) { - const capacity = rateLimit[0]; - expect(capacity).toHaveProperty('ability'); - expect(capacity).toHaveProperty('limit'); - expect(capacity).toHaveProperty('remaining'); - expect(typeof capacity.ability).toBe('string'); - expect(typeof capacity.limit).toBe('number'); - expect(typeof capacity.remaining).toBe('number'); - } - } - }); - - test('Should return consistent response structure on multiple requests', async () => { - const response1 = await app.handle( - new Request('http://localhost/stats', { - method: 'GET', - }), - ); - const data1 = await response1.json(); - - const response2 = await app.handle( - new Request('http://localhost/stats', { - method: 'GET', - }), - ); - const data2 = await response2.json(); - - // Both should have the same structure - expect(data1).toHaveProperty('data'); - expect(data2).toHaveProperty('data'); - expect(data1.data).toHaveProperty('server'); - expect(data2.data).toHaveProperty('server'); - expect(data1.data).toHaveProperty('manager'); - expect(data2.data).toHaveProperty('manager'); - - // Server stats should have same structure (values may differ) - expect(Object.keys(data1.data.server)).toEqual( - Object.keys(data2.data.server), - ); - expect(Object.keys(data1.data.manager)).toEqual( - Object.keys(data2.data.manager), - ); - }); - - test('Should have valid uptime format', async () => { - const response = await app.handle( - new Request('http://localhost/stats', { - method: 'GET', - }), - ); - - const data = await response.json(); - const uptime = data.data.server.uptime; - - // Uptime nanoseconds should be a positive number - expect(uptime.nanoseconds).toBeGreaterThan(0); - - // Uptime pretty should match format like "0d 0h 0m 0s" - expect(uptime.pretty).toMatch(/^\d+d \d+h \d+m \d+s$/); - }); - - test('Should have valid memory values', async () => { - const response = await app.handle( - new Request('http://localhost/stats', { - method: 'GET', - }), - ); - - const data = await response.json(); - const memory = data.data.server.memory; - - // Memory values should be strings (human-readable format) - expect(typeof memory.rss).toBe('string'); - expect(typeof memory.heapTotal).toBe('string'); - expect(typeof memory.heapUsed).toBe('string'); - expect(typeof memory.external).toBe('string'); - expect(typeof memory.arrayBuffers).toBe('string'); - - // Memory values should end with "MB" or similar - expect(memory.rss).toMatch(/MB$/); - }); +import { HttpStatusCode } from "axios"; +import { beforeAll, beforeEach, describe, expect, jest, test } from "bun:test"; +import type { Elysia } from "elysia"; + +import setup from "../src/setup"; +import { Mocker } from "./utils/mocker"; + +describe("Stats Endpoint", () => { + let app: Elysia; + + beforeAll(async () => { + await Mocker.ensureDatabaseInitialized(); + }); + + beforeEach(async () => { + jest.restoreAllMocks(); + Mocker.mockMirrorsBenchmark(); + + app = (await setup()) as unknown as Elysia; + }); + + test("Should return 200 status code", async () => { + const response = await app.handle( + new Request("http://localhost/stats", { + method: "GET", + }), + ); + + expect(response.status).toBe(HttpStatusCode.Ok); + }); + + test("Should return JSON response with server and manager stats", async () => { + const response = await app.handle( + new Request("http://localhost/stats", { + method: "GET", + }), + ); + + expect(response.status).toBe(HttpStatusCode.Ok); + + const data = await response.json(); + + expect(data).toHaveProperty("data"); + expect(data.data).toHaveProperty("server"); + expect(data.data).toHaveProperty("manager"); + }); + + test("Should include server statistics with correct structure", async () => { + const response = await app.handle( + new Request("http://localhost/stats", { + method: "GET", + }), + ); + + const data = await response.json(); + const serverStats = data.data.server; + + expect(serverStats).toHaveProperty("uptime"); + expect(serverStats.uptime).toHaveProperty("nanoseconds"); + expect(serverStats.uptime).toHaveProperty("pretty"); + expect(typeof serverStats.uptime.nanoseconds).toBe("number"); + expect(typeof serverStats.uptime.pretty).toBe("string"); + + expect(serverStats).toHaveProperty("memory"); + expect(serverStats.memory).toHaveProperty("rss"); + expect(serverStats.memory).toHaveProperty("heapTotal"); + expect(serverStats.memory).toHaveProperty("heapUsed"); + expect(serverStats.memory).toHaveProperty("external"); + expect(serverStats.memory).toHaveProperty("arrayBuffers"); + + expect(serverStats).toHaveProperty("pid"); + expect(typeof serverStats.pid).toBe("number"); + + expect(serverStats).toHaveProperty("version"); + expect(typeof serverStats.version).toBe("string"); + + expect(serverStats).toHaveProperty("revision"); + expect(typeof serverStats.revision).toBe("string"); + }); + + test("Should include manager statistics with correct structure", async () => { + const response = await app.handle( + new Request("http://localhost/stats", { + method: "GET", + }), + ); + + const data = await response.json(); + const managerStats = data.data.manager; + + expect(managerStats).toHaveProperty("storage"); + expect(managerStats).toHaveProperty("mirrors"); + }); + + test("Should include storage statistics with correct structure", async () => { + const response = await app.handle( + new Request("http://localhost/stats", { + method: "GET", + }), + ); + + const data = await response.json(); + const storageStats = data.data.manager.storage; + + expect(storageStats).toHaveProperty("database"); + expect(storageStats.database).toHaveProperty("beatmaps"); + expect(storageStats.database).toHaveProperty("beatmapSets"); + expect(storageStats.database).toHaveProperty("beatmapSetFile"); + expect(storageStats.database).toHaveProperty("beatmapOsuFile"); + expect(typeof storageStats.database.beatmaps).toBe("number"); + expect(typeof storageStats.database.beatmapSets).toBe("number"); + expect(typeof storageStats.database.beatmapSetFile).toBe("number"); + expect(typeof storageStats.database.beatmapOsuFile).toBe("number"); + + expect(storageStats).toHaveProperty("files"); + expect(storageStats.files).toHaveProperty("totalFiles"); + expect(storageStats.files).toHaveProperty("totalBytes"); + expect(typeof storageStats.files.totalFiles).toBe("number"); + expect(typeof storageStats.files.totalBytes).toBe("string"); + + expect(storageStats).toHaveProperty("cache"); + expect(storageStats.cache).toHaveProperty("beatmaps"); + expect(storageStats.cache).toHaveProperty("beatmapsets"); + expect(storageStats.cache).toHaveProperty("beatmapsetFiles"); + expect(storageStats.cache).toHaveProperty("beatmapOsuFiles"); + }); + + test("Should include cache statistics with correct structure", async () => { + const response = await app.handle( + new Request("http://localhost/stats", { + method: "GET", + }), + ); + + const data = await response.json(); + const cacheStats = data.data.manager.storage.cache; + + expect(cacheStats.beatmaps).toHaveProperty("byId"); + expect(cacheStats.beatmaps).toHaveProperty("ids"); + expect(cacheStats.beatmaps.ids).toHaveProperty("byHash"); + expect(typeof cacheStats.beatmaps.byId).toBe("number"); + expect(typeof cacheStats.beatmaps.ids.byHash).toBe("number"); + + expect(cacheStats.beatmapsets).toHaveProperty("byId"); + expect(typeof cacheStats.beatmapsets.byId).toBe("number"); + + expect(cacheStats.beatmapsetFiles).toHaveProperty("byId"); + expect(typeof cacheStats.beatmapsetFiles.byId).toBe("number"); + + expect(cacheStats.beatmapOsuFiles).toHaveProperty("byId"); + expect(typeof cacheStats.beatmapOsuFiles.byId).toBe("number"); + }); + + test("Should include mirrors statistics with correct structure", async () => { + const response = await app.handle( + new Request("http://localhost/stats", { + method: "GET", + }), + ); + + const data = await response.json(); + const mirrorsStats = data.data.manager.mirrors; + + expect(mirrorsStats).toHaveProperty("activeMirrors"); + expect(Array.isArray(mirrorsStats.activeMirrors)).toBe(true); + + expect(mirrorsStats).toHaveProperty("rateLimitsTotal"); + expect(typeof mirrorsStats.rateLimitsTotal).toBe("object"); + }); + + test("Should include active mirrors with correct structure", async () => { + const response = await app.handle( + new Request("http://localhost/stats", { + method: "GET", + }), + ); + + const data = await response.json(); + const { activeMirrors } = data.data.manager.mirrors; + + if (activeMirrors.length > 0) { + const mirror = activeMirrors[0]; + + expect(mirror).toHaveProperty("name"); + expect(mirror).toHaveProperty("url"); + expect(mirror).toHaveProperty("rateLimit"); + expect(mirror).toHaveProperty("requests"); + + expect(typeof mirror.name).toBe("string"); + expect(typeof mirror.url).toBe("string"); + + if ("onCooldownUntil" in mirror) { + expect( + mirror.onCooldownUntil === null + || typeof mirror.onCooldownUntil === "number", + ).toBe(true); + } + expect(Array.isArray(mirror.rateLimit)).toBe(true); + expect(typeof mirror.requests).toBe("object"); + + expect(mirror.requests).toHaveProperty("lifetime"); + expect(mirror.requests).toHaveProperty("session"); + expect(mirror.requests).toHaveProperty("hour"); + expect(mirror.requests).toHaveProperty("day"); + expect(mirror.requests).toHaveProperty("week"); + expect(mirror.requests).toHaveProperty("month"); + + const timeRange = mirror.requests.lifetime; + expect(timeRange).toHaveProperty("total"); + expect(timeRange).toHaveProperty("successful"); + expect(timeRange).toHaveProperty("failed"); + expect(typeof timeRange.total).toBe("number"); + expect(typeof timeRange.successful).toBe("number"); + expect(typeof timeRange.failed).toBe("number"); + } + }); + + test("Should include rate limit information in active mirrors", async () => { + const response = await app.handle( + new Request("http://localhost/stats", { + method: "GET", + }), + ); + + const data = await response.json(); + const { activeMirrors } = data.data.manager.mirrors; + + if (activeMirrors.length > 0) { + const mirror = activeMirrors[0]; + const { rateLimit } = mirror; + + if (rateLimit.length > 0) { + const capacity = rateLimit[0]; + expect(capacity).toHaveProperty("ability"); + expect(capacity).toHaveProperty("limit"); + expect(capacity).toHaveProperty("remaining"); + expect(typeof capacity.ability).toBe("string"); + expect(typeof capacity.limit).toBe("number"); + expect(typeof capacity.remaining).toBe("number"); + } + } + }); + + test("Should return consistent response structure on multiple requests", async () => { + const response1 = await app.handle( + new Request("http://localhost/stats", { + method: "GET", + }), + ); + const data1 = await response1.json(); + + const response2 = await app.handle( + new Request("http://localhost/stats", { + method: "GET", + }), + ); + const data2 = await response2.json(); + + // Both should have the same structure + expect(data1).toHaveProperty("data"); + expect(data2).toHaveProperty("data"); + expect(data1.data).toHaveProperty("server"); + expect(data2.data).toHaveProperty("server"); + expect(data1.data).toHaveProperty("manager"); + expect(data2.data).toHaveProperty("manager"); + + // Server stats should have same structure (values may differ) + expect(Object.keys(data1.data.server)).toEqual( + Object.keys(data2.data.server), + ); + expect(Object.keys(data1.data.manager)).toEqual( + Object.keys(data2.data.manager), + ); + }); + + test("Should have valid uptime format", async () => { + const response = await app.handle( + new Request("http://localhost/stats", { + method: "GET", + }), + ); + + const data = await response.json(); + const { uptime } = data.data.server; + + // Uptime nanoseconds should be a positive number + expect(uptime.nanoseconds).toBeGreaterThan(0); + + // Uptime pretty should match format like "0d 0h 0m 0s" + expect(uptime.pretty).toMatch(/^\d+d \d+h \d+m \d+s$/); + }); + + test("Should have valid memory values", async () => { + const response = await app.handle( + new Request("http://localhost/stats", { + method: "GET", + }), + ); + + const data = await response.json(); + const { memory } = data.data.server; + + // Memory values should be strings (human-readable format) + expect(typeof memory.rss).toBe("string"); + expect(typeof memory.heapTotal).toBe("string"); + expect(typeof memory.heapUsed).toBe("string"); + expect(typeof memory.external).toBe("string"); + expect(typeof memory.arrayBuffers).toBe("string"); + + // Memory values should end with "MB" or similar + expect(memory.rss).toMatch(/MB$/); + }); }); diff --git a/server/tests/utils/faker.generator.ts b/server/tests/utils/faker.generator.ts index 41e868e..53e73bc 100644 --- a/server/tests/utils/faker.generator.ts +++ b/server/tests/utils/faker.generator.ts @@ -1,223 +1,223 @@ -import { jest, mock } from 'bun:test'; +import { faker } from "@faker-js/faker"; +import { mock } from "bun:test"; -import { faker } from '@faker-js/faker'; -import { User } from '../../src/types/general/user'; -import { Beatmap, Beatmapset } from '../../src/types/general/beatmap'; -import { RankStatus } from '../../src/types/general/rankStatus'; -import { GameMode } from '../../src/types/general/gameMode'; -import { - MinoBeatmap, - MinoBeatmapset, -} from '../../src/core/domains/catboy.best/mino-client.types'; -import { - OsulabsBeatmap, - OsulabsBeatmapset, -} from '../../src/core/domains/beatmaps.download/osulabs-client.types'; -import { DeepPartial } from '../../src/types/utils'; +import type { + OsulabsBeatmap, + OsulabsBeatmapset, +} from "../../src/core/domains/beatmaps.download/osulabs-client.types"; +import type { + MinoBeatmap, + MinoBeatmapset, +} from "../../src/core/domains/catboy.best/mino-client.types"; +import type { Beatmap, Beatmapset } from "../../src/types/general/beatmap"; +import { GameMode } from "../../src/types/general/gameMode"; +import { RankStatus } from "../../src/types/general/rankStatus"; +import type { User } from "../../src/types/general/user"; +import type { DeepPartial } from "../../src/types/utils"; function autoMock(base: DeepPartial): T { - return new Proxy(base as T, { - get(target: any, prop: string | symbol) { - if (prop in target) { - return target[prop]; - } + return new Proxy(base as T, { + get(target: any, prop: string | symbol) { + if (prop in target) { + return target[prop]; + } - return mock(async (...args: any[]) => null); - }, - }) as unknown as T; + return mock(async (..._args: any[]) => null); + }, + }) as unknown as T; } -export class FakerGenerator { - static generateOsuUser(options?: DeepPartial): User { - return autoMock({ - id: faker.number.int({ min: 1, max: 1000000 }), - username: faker.internet.username(), - country_code: faker.location.countryCode(), - country: { - code: faker.location.countryCode(), - name: faker.location.country(), - }, - cover: { - custom_url: null, - id: null, - url: 'https://placehold.co/1200x300', - }, - discord: null, - has_supported: faker.datatype.boolean(), - interests: null, - is_restricted: false, - join_date: new Date().toISOString(), - location: null, - max_blocks: faker.number.int({ min: 0, max: 100 }), - max_friends: faker.number.int({ min: 0, max: 100 }), - monthly_playcounts: [], - occupation: null, - previous_usernames: [], - profile_colour: null, - ...(options as any), - }); - } +export const FakerGenerator = { + generateOsuUser(options?: DeepPartial): User { + return autoMock({ + id: faker.number.int({ min: 1, max: 1000000 }), + username: faker.internet.username(), + country_code: faker.location.countryCode(), + country: { + code: faker.location.countryCode(), + name: faker.location.country(), + }, + cover: { + custom_url: null, + id: null, + url: "https://placehold.co/1200x300", + }, + discord: null, + has_supported: faker.datatype.boolean(), + interests: null, + is_restricted: false, + join_date: new Date().toISOString(), + location: null, + max_blocks: faker.number.int({ min: 0, max: 100 }), + max_friends: faker.number.int({ min: 0, max: 100 }), + monthly_playcounts: [], + occupation: null, + previous_usernames: [], + profile_colour: null, + ...(options as any), + }); + }, - static generateOsulabsBeatmapset( - options?: DeepPartial, - ): OsulabsBeatmapset { - return this.generateBeatmapset({ - last_updated: new Date().getTime() as unknown as string, // number is expected by the service - beatmaps: options?.beatmaps ?? [ - FakerGenerator.generateOsulabsBeatmap({ - set: null, - convert: false, - }), - FakerGenerator.generateOsulabsBeatmap({ - set: null, - convert: false, - }), - ], - converts: options?.converts ?? [ - FakerGenerator.generateOsulabsBeatmap({ - convert: true, - set: null, - }), - FakerGenerator.generateOsulabsBeatmap({ - convert: true, - set: null, - }), - ], - ...options, - }) as OsulabsBeatmapset; - } + generateOsulabsBeatmapset( + options?: DeepPartial, + ): OsulabsBeatmapset { + return this.generateBeatmapset({ + last_updated: Date.now() as unknown as string, // number is expected by the service + beatmaps: options?.beatmaps ?? [ + FakerGenerator.generateOsulabsBeatmap({ + set: null, + convert: false, + }), + FakerGenerator.generateOsulabsBeatmap({ + set: null, + convert: false, + }), + ], + converts: options?.converts ?? [ + FakerGenerator.generateOsulabsBeatmap({ + convert: true, + set: null, + }), + FakerGenerator.generateOsulabsBeatmap({ + convert: true, + set: null, + }), + ], + ...options, + }) as OsulabsBeatmapset; + }, - static generateOsulabsBeatmap( - options?: DeepPartial, - ): OsulabsBeatmap { - return this.generateBeatmap({ - last_updated: new Date().getTime() as unknown as string, // number is expected by the service - ...options, - set: + generateOsulabsBeatmap( + options?: DeepPartial, + ): OsulabsBeatmap { + return this.generateBeatmap({ + last_updated: Date.now() as unknown as string, // number is expected by the service + ...options, + set: options?.set !== undefined - ? options.set - : FakerGenerator.generateOsulabsBeatmapset({ - beatmaps: [ - FakerGenerator.generateOsulabsBeatmap({ - set: null, - }) as any, - FakerGenerator.generateOsulabsBeatmap({ - set: null, - }) as any, - ], - converts: [ - FakerGenerator.generateOsulabsBeatmap({ - convert: true, - set: null, - }) as any, - FakerGenerator.generateOsulabsBeatmap({ - convert: true, - set: null, - }) as any, - ], - }), - ...options, - }) as OsulabsBeatmap; - } + ? options.set + : FakerGenerator.generateOsulabsBeatmapset({ + beatmaps: [ + FakerGenerator.generateOsulabsBeatmap({ + set: null, + }) as any, + FakerGenerator.generateOsulabsBeatmap({ + set: null, + }) as any, + ], + converts: [ + FakerGenerator.generateOsulabsBeatmap({ + convert: true, + set: null, + }) as any, + FakerGenerator.generateOsulabsBeatmap({ + convert: true, + set: null, + }) as any, + ], + }), + ...options, + }) as OsulabsBeatmap; + }, - static generateMinoBeatmapset( - options?: DeepPartial, - ): MinoBeatmapset { - return this.generateBeatmapset({ - last_updated: new Date().getTime() as unknown as string, // number is expected by the service - beatmaps: options?.beatmaps ?? [ - FakerGenerator.generateMinoBeatmap({ convert: false }), - FakerGenerator.generateMinoBeatmap({ convert: false }), - ], - converts: options?.converts ?? [ - FakerGenerator.generateMinoBeatmap({ convert: true }), - FakerGenerator.generateMinoBeatmap({ convert: true }), - ], - ...options, - }) as MinoBeatmapset; - } + generateMinoBeatmapset( + options?: DeepPartial, + ): MinoBeatmapset { + return this.generateBeatmapset({ + last_updated: Date.now() as unknown as string, // number is expected by the service + beatmaps: options?.beatmaps ?? [ + FakerGenerator.generateMinoBeatmap({ convert: false }), + FakerGenerator.generateMinoBeatmap({ convert: false }), + ], + converts: options?.converts ?? [ + FakerGenerator.generateMinoBeatmap({ convert: true }), + FakerGenerator.generateMinoBeatmap({ convert: true }), + ], + ...options, + }) as MinoBeatmapset; + }, - static generateMinoBeatmap( - options?: DeepPartial, - ): MinoBeatmap { - return this.generateBeatmap({ - last_updated: new Date().getTime() as unknown as string, // number is expected by the service - set: + generateMinoBeatmap( + options?: DeepPartial, + ): MinoBeatmap { + return this.generateBeatmap({ + last_updated: Date.now() as unknown as string, // number is expected by the service + set: options?.set !== undefined - ? options.set - : FakerGenerator.generateMinoBeatmapset({ - beatmaps: [ - FakerGenerator.generateMinoBeatmap({ - set: null, - }) as any, - FakerGenerator.generateMinoBeatmap({ - set: null, - }) as any, - ], - converts: [ - FakerGenerator.generateMinoBeatmap({ - convert: true, - set: null, - }) as any, - FakerGenerator.generateMinoBeatmap({ - convert: true, - set: null, - }) as any, - ], - }), - ...options, - }) as MinoBeatmap; - } + ? options.set + : FakerGenerator.generateMinoBeatmapset({ + beatmaps: [ + FakerGenerator.generateMinoBeatmap({ + set: null, + }) as any, + FakerGenerator.generateMinoBeatmap({ + set: null, + }) as any, + ], + converts: [ + FakerGenerator.generateMinoBeatmap({ + convert: true, + set: null, + }) as any, + FakerGenerator.generateMinoBeatmap({ + convert: true, + set: null, + }) as any, + ], + }), + ...options, + }) as MinoBeatmap; + }, - static generateBeatmapset(options?: DeepPartial): Beatmapset { - return autoMock({ - id: faker.number.int({ min: 1, max: 1000000 }), - artist: faker.music.artist(), - artist_unicode: faker.music.artist(), - creator: faker.internet.username(), - creator_id: faker.number.int({ min: 1, max: 100000 }), - beatmap_nominator_user: undefined, - beatmaps: [ - FakerGenerator.generateBeatmap(), - FakerGenerator.generateBeatmap(), - ], - converts: [ - FakerGenerator.generateBeatmap({ convert: true }), - FakerGenerator.generateBeatmap({ convert: true }), - ], - last_updated: new Date().toISOString(), - ...(options as any), - }); - } + generateBeatmapset(options?: DeepPartial): Beatmapset { + return autoMock({ + id: faker.number.int({ min: 1, max: 1000000 }), + artist: faker.music.artist(), + artist_unicode: faker.music.artist(), + creator: faker.internet.username(), + creator_id: faker.number.int({ min: 1, max: 100000 }), + beatmap_nominator_user: undefined, + beatmaps: [ + FakerGenerator.generateBeatmap(), + FakerGenerator.generateBeatmap(), + ], + converts: [ + FakerGenerator.generateBeatmap({ convert: true }), + FakerGenerator.generateBeatmap({ convert: true }), + ], + last_updated: new Date().toISOString(), + ...(options as any), + }); + }, - static generateBeatmap(options?: DeepPartial): Beatmap { - return autoMock({ - id: faker.number.int({ min: 1, max: 1000000 }), - beatmapset_id: faker.number.int({ min: 1, max: 100000 }), - checksum: faker.string.alphanumeric(32), - version: faker.word.adjective(), - status: RankStatus.RANKED, - difficulty_rating: faker.number.float({ min: 1, max: 10 }), - total_length: faker.number.int({ min: 60, max: 600 }), - max_combo: faker.number.int({ min: 100, max: 2000 }), - accuracy: faker.number.float({ min: 1, max: 10 }), - ar: faker.number.float({ min: 1, max: 10 }), - bpm: faker.number.float({ min: 80, max: 200 }), - convert: false, - count_circles: faker.number.int({ min: 100, max: 1000 }), - count_sliders: faker.number.int({ min: 50, max: 500 }), - count_spinners: faker.number.int({ min: 0, max: 10 }), - cs: faker.number.float({ min: 1, max: 10 }), - deleted_at: null, - drain: faker.number.float({ min: 1, max: 10 }), - hit_length: faker.number.int({ min: 60, max: 600 }), - is_scoreable: true, - last_updated: new Date().toISOString(), - mode_int: 0, - mode: GameMode.OSU_STANDARD, - ranked: 1, - user_id: faker.number.int({ min: 1, max: 100000 }), - ...(options as any), - }); - } -} + generateBeatmap(options?: DeepPartial): Beatmap { + return autoMock({ + id: faker.number.int({ min: 1, max: 1000000 }), + beatmapset_id: faker.number.int({ min: 1, max: 100000 }), + checksum: faker.string.alphanumeric(32), + version: faker.word.adjective(), + status: RankStatus.RANKED, + difficulty_rating: faker.number.float({ min: 1, max: 10 }), + total_length: faker.number.int({ min: 60, max: 600 }), + max_combo: faker.number.int({ min: 100, max: 2000 }), + accuracy: faker.number.float({ min: 1, max: 10 }), + ar: faker.number.float({ min: 1, max: 10 }), + bpm: faker.number.float({ min: 80, max: 200 }), + convert: false, + count_circles: faker.number.int({ min: 100, max: 1000 }), + count_sliders: faker.number.int({ min: 50, max: 500 }), + count_spinners: faker.number.int({ min: 0, max: 10 }), + cs: faker.number.float({ min: 1, max: 10 }), + deleted_at: null, + drain: faker.number.float({ min: 1, max: 10 }), + hit_length: faker.number.int({ min: 60, max: 600 }), + is_scoreable: true, + last_updated: new Date().toISOString(), + mode_int: 0, + mode: GameMode.OSU_STANDARD, + ranked: 1, + user_id: faker.number.int({ min: 1, max: 100000 }), + ...(options as any), + }); + }, +}; diff --git a/server/tests/utils/mocker.ts b/server/tests/utils/mocker.ts index 7505a63..c1e6cfa 100644 --- a/server/tests/utils/mocker.ts +++ b/server/tests/utils/mocker.ts @@ -1,254 +1,257 @@ -import { spyOn, Mock } from 'bun:test'; -import { BanchoClient } from '../../src/core/domains'; -import { ApiRateLimiter } from '../../src/core/abstracts/ratelimiter/rate-limiter.abstract'; -import { BaseApi } from '../../src/core/abstracts/api/base-api.abstract'; -import { BaseClient } from '../../src/core/abstracts/client/base-client.abstract'; -import { BanchoService } from '../../src/core/domains/osu.ppy.sh/bancho-client.service'; - -import path from 'path'; -import { migrate } from 'drizzle-orm/node-postgres/migrator'; -import { db } from '../../src/database/client'; -import { MirrorsManagerService } from '../../src/core/managers/mirrors/mirrors-manager.service'; -import { MinoClient } from '../../src/core/domains/catboy.best/mino.client'; -import { ClientAbilities } from '../../src/core/abstracts/client/base-client.types'; -import { GatariClient } from '../../src/core/domains/gatari.pw/gatari.client'; -import { NerinyanClient } from '../../src/core/domains/nerinyan.moe/nerinyan.client'; -import { OsulabsClient } from '../../src/core/domains/beatmaps.download/osulabs.client'; -import { FakerGenerator } from './faker.generator'; -import { - MinoBeatmap, - MinoBeatmapset, -} from '../../src/core/domains/catboy.best/mino-client.types'; -import { Beatmap, Beatmapset } from '../../src/types/general/beatmap'; -import { - OsulabsBeatmap, - OsulabsBeatmapset, -} from '../../src/core/domains/beatmaps.download/osulabs-client.types'; -import { DeepPartial } from '../../src/types/utils'; -import { RedisInstance } from '../../src/plugins/redisInstance'; -import { AxiosResponse } from 'axios'; +import path from "node:path"; + +import type { AxiosResponse } from "axios"; +import type { Mock } from "bun:test"; +import { spyOn } from "bun:test"; +import { migrate } from "drizzle-orm/node-postgres/migrator"; + +import { BaseApi } from "../../src/core/abstracts/api/base-api.abstract"; +import type { BaseClient } from "../../src/core/abstracts/client/base-client.abstract"; +import type { ApiRateLimiter } from "../../src/core/abstracts/ratelimiter/rate-limiter.abstract"; +import { BanchoClient } from "../../src/core/domains"; +import { OsulabsClient } from "../../src/core/domains/beatmaps.download/osulabs.client"; +import type { + OsulabsBeatmap, + OsulabsBeatmapset, +} from "../../src/core/domains/beatmaps.download/osulabs-client.types"; +import { MinoClient } from "../../src/core/domains/catboy.best/mino.client"; +import type { + MinoBeatmap, + MinoBeatmapset, +} from "../../src/core/domains/catboy.best/mino-client.types"; +import type { BanchoService } from "../../src/core/domains/osu.ppy.sh/bancho-client.service"; +import { MirrorsManagerService } from "../../src/core/managers/mirrors/mirrors-manager.service"; +import { db } from "../../src/database/client"; +import { RedisInstance } from "../../src/plugins/redisInstance"; +import type { Beatmap, Beatmapset } from "../../src/types/general/beatmap"; +import type { DeepPartial } from "../../src/types/utils"; +import { FakerGenerator } from "./faker.generator"; type MockAxiosResponse = Partial> & { - data: T; - status: number; - headers?: Record; + data: T; + status: number; + headers?: Record; }; +// eslint-disable-next-line unicorn/no-static-only-class -- Used for methods overloading export class Mocker { - static mockRequest( - baseClient: BaseClient, - service: 'self', - mockedEndpointMethod: keyof BaseClient, - data: MockAxiosResponse, - ): Mock; - static mockRequest( - baseClient: BaseClient, - service: 'api', - mockedEndpointMethod: keyof ApiRateLimiter, - data: MockAxiosResponse, - ): Mock; - static mockRequest( - baseClient: BaseClient, - service: 'baseApi', - mockedEndpointMethod: keyof BaseApi, - data: MockAxiosResponse, - ): Mock; - static mockRequest( - baseClient: BaseClient, - service: 'banchoService', - mockedEndpointMethod: keyof BanchoService, - data: string, - ): Mock; - static mockRequest( - baseClient: BaseClient, - service: 'self' | 'api' | 'baseApi' | 'banchoService', - mockedEndpointMethod: - | keyof BaseClient - | keyof BaseApi - | keyof ApiRateLimiter - | keyof BanchoService, - data: MockAxiosResponse | string, - ) { - if (service === 'api') { - return spyOn( - // @ts-expect-error ignore protected property - baseClient.api, - mockedEndpointMethod as keyof ApiRateLimiter, - ).mockResolvedValue(data as never); - } - - if (service === 'baseApi') { - return spyOn( - // @ts-expect-error ignore protected property - baseClient.api.api, - mockedEndpointMethod as keyof BaseApi, - ).mockResolvedValue(data as never); - } - - if (service === 'self') { - return spyOn( - baseClient, - mockedEndpointMethod as keyof BaseClient, - ).mockResolvedValue(data as never); - } - - if (service === 'banchoService' && baseClient instanceof BanchoClient) { - return spyOn( - // @ts-expect-error ignore protected property - baseClient.banchoService, - mockedEndpointMethod as keyof BanchoService, - ).mockResolvedValue(data as never); - } - - throw new Error('Invalid service to mock'); + static mockRequest( + baseClient: BaseClient, + service: "self", + mockedEndpointMethod: keyof BaseClient, + data: MockAxiosResponse, + ): Mock; + static mockRequest( + baseClient: BaseClient, + service: "api", + mockedEndpointMethod: keyof ApiRateLimiter, + data: MockAxiosResponse, + ): Mock; + static mockRequest( + baseClient: BaseClient, + service: "baseApi", + mockedEndpointMethod: keyof BaseApi, + data: MockAxiosResponse, + ): Mock; + static mockRequest<_T>( + baseClient: BaseClient, + service: "banchoService", + mockedEndpointMethod: keyof BanchoService, + data: string, + ): Mock; + static mockRequest( + baseClient: BaseClient, + service: "self" | "api" | "baseApi" | "banchoService", + mockedEndpointMethod: + | keyof BaseClient + | keyof BaseApi + | keyof ApiRateLimiter + | keyof BanchoService, + data: MockAxiosResponse | string, + ) { + if (service === "api") { + return spyOn( + // @ts-expect-error ignore protected property + baseClient.api, + mockedEndpointMethod as keyof ApiRateLimiter, + ).mockResolvedValue(data as never); + } + + if (service === "baseApi") { + return spyOn( + // @ts-expect-error ignore protected property + baseClient.api.api, + mockedEndpointMethod as keyof BaseApi, + ).mockResolvedValue(data as never); } - static mockSyncRequest( - baseClient: BaseClient, - service: 'self', - mockedEndpointMethod: keyof BaseClient, - data: T, - ): Mock; - static mockSyncRequest( - baseClient: BaseClient, - service: 'api', - mockedEndpointMethod: keyof ApiRateLimiter, - data: T, - ): Mock; - static mockSyncRequest( - baseClient: BaseClient, - service: 'baseApi', - mockedEndpointMethod: keyof BaseApi, - data: T, - ): Mock; - static mockSyncRequest( - baseClient: BaseClient, - service: 'self' | 'api' | 'baseApi', - mockedEndpointMethod: - | keyof BaseClient - | keyof BaseApi - | keyof ApiRateLimiter, - data: T, - ) { - if (service === 'api') { - return spyOn( - // @ts-expect-error ignore protected property - baseClient.api, - mockedEndpointMethod as keyof ApiRateLimiter, - ).mockReturnValue(data as never); - } - - if (service === 'baseApi') { - return spyOn( - // @ts-expect-error ignore protected property - baseClient.api.api, - mockedEndpointMethod as keyof BaseApi, - ).mockReturnValue(data as never); - } - - if (service === 'self') { - return spyOn( - baseClient, - mockedEndpointMethod as keyof BaseClient, - ).mockReturnValue(data as never); - } - - throw new Error('Invalid service to mock'); + if (service === "self") { + return spyOn( + baseClient, + mockedEndpointMethod as keyof BaseClient, + ).mockResolvedValue(data as never); } - static mockApiRequestForAllClients( - mockedEndpointMethod: keyof BaseApi, - data: AxiosResponse, - ) { - return spyOn(BaseApi.prototype, mockedEndpointMethod).mockResolvedValue( - data, - ); + if (service === "banchoService" && baseClient instanceof BanchoClient) { + return spyOn( + // @ts-expect-error ignore protected property + baseClient.banchoService, + mockedEndpointMethod as keyof BanchoService, + ).mockResolvedValue(data as never); } - // TODO: Add ability to mock database for unit tests - static async ensureDatabaseInitialized() { - const migrationPath = path.join( - process.cwd(), - 'server/src/database/migrations', - ); + throw new Error("Invalid service to mock"); + } - try { - await migrate(db, { - migrationsFolder: migrationPath, - }); - } catch (_) {} + static mockSyncRequest( + baseClient: BaseClient, + service: "self", + mockedEndpointMethod: keyof BaseClient, + data: T, + ): Mock; + static mockSyncRequest( + baseClient: BaseClient, + service: "api", + mockedEndpointMethod: keyof ApiRateLimiter, + data: T, + ): Mock; + static mockSyncRequest( + baseClient: BaseClient, + service: "baseApi", + mockedEndpointMethod: keyof BaseApi, + data: T, + ): Mock; + static mockSyncRequest( + baseClient: BaseClient, + service: "self" | "api" | "baseApi", + mockedEndpointMethod: + | keyof BaseClient + | keyof BaseApi + | keyof ApiRateLimiter, + data: T, + ) { + if (service === "api") { + return spyOn( + // @ts-expect-error ignore protected property + baseClient.api, + mockedEndpointMethod as keyof ApiRateLimiter, + ).mockReturnValue(data as never); + } - await RedisInstance.flushdb(); + if (service === "baseApi") { + return spyOn( + // @ts-expect-error ignore protected property + baseClient.api.api, + mockedEndpointMethod as keyof BaseApi, + ).mockReturnValue(data as never); } - static mockMirrorsBenchmark() { - spyOn( - MirrorsManagerService.prototype, - 'fetchMirrorsData', - ).mockResolvedValue(); + if (service === "self") { + return spyOn( + baseClient, + mockedEndpointMethod as keyof BaseClient, + ).mockReturnValue(data as never); } - static getClientGenerateMethods(client: BaseClient) { - switch (client.constructor) { - case MinoClient: - return { - generateBeatmap: (options?: DeepPartial) => - FakerGenerator.generateMinoBeatmap(options), - generateBeatmapset: ( - options?: DeepPartial, - ) => FakerGenerator.generateMinoBeatmapset(options), - generateArrayBuffer: () => new ArrayBuffer(1024), - }; - case OsulabsClient: - return { - generateBeatmap: (options?: DeepPartial) => - FakerGenerator.generateOsulabsBeatmap(options), - generateBeatmapset: ( - options?: DeepPartial, - ) => FakerGenerator.generateOsulabsBeatmapset(options), - generateArrayBuffer: () => new ArrayBuffer(1024), - }; - default: - return { - generateBeatmap: (options?: DeepPartial) => - FakerGenerator.generateBeatmap(options), - generateBeatmapset: (options?: DeepPartial) => - FakerGenerator.generateBeatmapset(options), - generateArrayBuffer: () => new ArrayBuffer(1024), - }; - } + throw new Error("Invalid service to mock"); + } + + static mockApiRequestForAllClients( + mockedEndpointMethod: keyof BaseApi, + data: AxiosResponse, + ) { + return spyOn(BaseApi.prototype, mockedEndpointMethod).mockResolvedValue( + data, + ); + } + + // TODO: Add ability to mock database for unit tests + static async ensureDatabaseInitialized() { + const migrationPath = path.join( + process.cwd(), + "server/src/database/migrations", + ); + + try { + await migrate(db, { + migrationsFolder: migrationPath, + }); } + catch { + throw new Error("Failed to migrate the database for testing"); + } + + await RedisInstance.flushdb(); + } - static getClientMockMethods(client: BaseClient) { - const generators = this.getClientGenerateMethods(client); + static mockMirrorsBenchmark() { + spyOn( + MirrorsManagerService.prototype, + "fetchMirrorsData", + ).mockResolvedValue(); + } + static getClientGenerateMethods(client: BaseClient) { + switch (client.constructor) { + case MinoClient: + return { + generateBeatmap: (options?: DeepPartial) => + FakerGenerator.generateMinoBeatmap(options), + generateBeatmapset: ( + options?: DeepPartial, + ) => FakerGenerator.generateMinoBeatmapset(options), + generateArrayBuffer: () => new ArrayBuffer(1024), + }; + case OsulabsClient: return { - mockBeatmap: (options?: Partial>) => - this.mockRequest(client, 'baseApi', 'get', { - status: 200, - headers: {}, - ...options, - data: generators.generateBeatmap(options?.data), - }), - mockBeatmapset: (options?: Partial>) => - this.mockRequest(client, 'baseApi', 'get', { - status: 200, - headers: {}, - ...options, - data: generators.generateBeatmapset(options?.data), - }), - mockArrayBuffer: () => - this.mockRequest(client, 'baseApi', 'get', { - status: 200, - headers: {}, - data: generators.generateArrayBuffer(), - }), + generateBeatmap: (options?: DeepPartial) => + FakerGenerator.generateOsulabsBeatmap(options), + generateBeatmapset: ( + options?: DeepPartial, + ) => FakerGenerator.generateOsulabsBeatmapset(options), + generateArrayBuffer: () => new ArrayBuffer(1024), + }; + default: + return { + generateBeatmap: (options?: DeepPartial) => + FakerGenerator.generateBeatmap(options), + generateBeatmapset: (options?: DeepPartial) => + FakerGenerator.generateBeatmapset(options), + generateArrayBuffer: () => new ArrayBuffer(1024), }; } + } + + static getClientMockMethods(client: BaseClient) { + const generators = this.getClientGenerateMethods(client); + + return { + mockBeatmap: (options?: Partial>) => + this.mockRequest(client, "baseApi", "get", { + status: 200, + headers: {}, + ...options, + data: generators.generateBeatmap(options?.data), + }), + mockBeatmapset: (options?: Partial>) => + this.mockRequest(client, "baseApi", "get", { + status: 200, + headers: {}, + ...options, + data: generators.generateBeatmapset(options?.data), + }), + mockArrayBuffer: () => + this.mockRequest(client, "baseApi", "get", { + status: 200, + headers: {}, + data: generators.generateArrayBuffer(), + }), + }; + } } type MockApiRequestOptions = { - data: DeepPartial; - status: number; - headers: Record; + data: DeepPartial; + status: number; + headers: Record; }; From 23b4e4115915a5ffa04dc092cbd5561923552553 Mon Sep 17 00:00:00 2001 From: richardscull <106016833+richardscull@users.noreply.github.com> Date: Thu, 25 Dec 2025 08:32:49 +0200 Subject: [PATCH 3/3] feat: do lint on ci --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index db6cc6c..f947c6e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,5 +1,5 @@ on: [push] -name: Run Tests +name: Run Tests & Lint concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -14,4 +14,5 @@ jobs: - run: docker compose -f docker-compose.tests.yml up --detach - run: bun install --frozen-lockfile + - run: bun lint - run: bun test